sikanhe / gqtx Goto Github PK
View Code? Open in Web Editor NEWCode-first Typescript GraphQL Server without codegen or metaprogramming
Code-first Typescript GraphQL Server without codegen or metaprogramming
Line 2 in 8fddbe3
Right now package.json says "graphql": "^15.1.0",
. Does that mean 16 is not supported?
How do you run the examples?
Fields defined with t.ID
should be able to take either a string or a number, as the default behavior is to stringify the value, but currently it expects it to be a string. The readme example runs into this problem
type User = {
id: number;
role: Role;
name: string;
};
const UserType = t.objectType<User>({
name: 'User',
description: 'A User',
fields: () => [
t.defaultField('id', t.NonNull(t.ID)) // Type 'string | null' is not assignable to type 'number'.
...
Thanks for this nice and simple library. It would be great if you'd provide an ESM build of this library as well. (In case you're not familiar with how to do this, you can reference how Nexus is built.)
Problem: while defining some fields, is not possible to use fields defined later in the file.
Possible solution: make field declaration lazy using a function and not a plain array
Example:
const Query = t.queryType({
fields: [
t.field('myType', {
type: myType, // ts(2454): Variable 'event' is used before being assigned.
// ...
})
]
})
const myType = t.objectType({ ... })
const Query = t.queryType({
fields: () => [
t.field('myType', {
type: myType, // It works
// ...
})
]
})
const myType = t.objectType({ ... })
I can submit a PR for this.
PS: in case this would be accepted, we should revert #46
See https://unpkg.com/browse/[email protected]/dist/
@sikanhe It seems like
Line 31 in 800c992
I have a very strange case, I'm converting a application that uses remote schemas and extends them.
I have moved most of my remote schemas to my "main" graphql service but I have one that is very large and not really feasible to move currently.
Can I still extend types from it with gqtx?
Thanks for the help!
Currently, only args, context and info are passed into the subscribe function. The root/srcc should be accessible as well.
If my gql type defined with t.objectType references itself it TS type is any
// typeof GqlUser is any
const GqlUser = t.objectType<User>({
name: 'User',
fields: () => [
t.defaultField('id', t.NonNull(t.ID)),
t.defaultField('name', t.NonNull(t.String)),
t.field('parent', {
type: GqlUser,
resolve: (parent, args, ctx, info) => {
return 5; // accepts any value since GqlUser is any
},
}),
],
});
Here's an example of how we we gqtx today to be able to avoid duplicating code between implementers of the ILedger
interface.
const LedgerInterface = t.interfaceType<Raw.Ledger>({
name: 'ILedger',
fields: () => [
t.abstractField({name: 'id', type: t.String}),
t.abstractField({name: 'name', type: t.String}),
t.abstractField({name: 'users', type: t.List(LedgerUserType)}),
],
})
/**
* Way to share implemention of the LedgerInterface
* Need to figure out how to share the `users` implementation between books and tabs
* Or perhaps better to not differentiate at all?
*/
function makeLedgerSubtype(options: {
name: string
isTypeOf: (l: Raw.Ledger) => boolean
fields: () => Array<Field<GQLContext, Raw.Ledger, unknown, {}>>
}) {
return t.objectType<Raw.Ledger>({
name: options.name,
interfaces: [LedgerInterface],
isTypeOf: options.isTypeOf,
fields: () => [
t.field({name: 'id', type: t.String}),
t.field({name: 'name', type: StringType}),
t.field({
name: 'users',
type: t.List(UserType),
// resolve: (ledger, __, ctx) =>
}),
...options.fields(),
],
})
}
It's a bit cumbersome, but still better than duplicating the code. In an ideal world the LedgerInterface would just be able to contain the implementation details (optionally) and avoid needing to duplicate all together.
What sets gqtx apart from NexusJS and Pothos (aka. GiraphQL)?
https://github.com/graphql-nexus/nexus
https://github.com/hayes/pothos
I couldnโt discern it from the Readme. The approaches look very similar if not identical.
For interfaces, the best thing to do is using isTypeOf
.
E.g.
const isTypeOf = (src: any) => src?.type === "Operator"
With a library such as io-ts
the stuff can be even "more" save:
import { flow } from "fp-ts/lib/function";
import * as t from "io-ts";
import * as E from "fp-ts/lib/Either";
const NoteModel = t.type({
id: t.string,
title: t.string,
content: t.string,
createdAt: t.number,
updatedAt: t.number,
});
const isTypeOf = flow(
NoteModel.decode,
E.fold(
() => false,
() => true
)
);
The drawback of this is, that the type-safety does happen while executing the code not while compiling it. I wonder whether there are other solutions worth exploring.
For a union type we have the same "issue":
const resolveType = (input) => {
if (isTypeOfNote(input)) {
return GraphQLNoteType;
} else if (isTypeOfImage(input)) {
return GraphQLImageType;
}
throw new Error("Could not resolve type.");
}
I mentioned some more stuff related to this in #9
When we try to directly use the example from README.md
const UserType = t.objectType<User>({
name: 'User',
description: 'A User',
fields: () => [
t.field({ name: 'id', type: t.NonNull(t.ID) }),
t.field({ name: 'role', type: t.NonNull(RoleEnum) }),
t.field({ name: 'name', type: t.NonNull(t.String) }),
],
});
The Typescript compiler starts giving this error - Expected 2 arguments, but got 1.ts(2554)
Looking into the types in node_modules, It looks like it has been changed to -
field(name: string, { type, args, resolve, description, deprecationReason, }
And when I try to use it, compiler starts giving warning that resolve needs to be added.
I'm not sure how should be handled since even in docs resolve has only been added in queryType not in objectType. How it should be handled?
In the following code,
type SearchCriteria = {
minPrice?: number,
maxPrice?: number,
}
const SearchCriteria = t.objectType<SearchCriteria>({
name: "SearchCriteria",
fields: () => [
t.field({ name: 'minPrice', type: t.Int , resolve: ({minPrice}) => minPrice }),
t.field({ name: 'maxPrice', type: t.Int , resolve: ({maxPrice}) => maxPrice }),
]
})
The compiler throws an error if I don't provide the resolve
function. The reason is that t.Int
is of type Scalar<number | null>
, while my SearchCriteria.minPrice
is of type number | undefined
, so the later doesn't extend the former.
It would be great if the built in scalars (if they're not non-nullable) supported the undefined
value too, like so Int: Scalar<number | null | undefined>
.
Currently t.ID
is inferred as any
, but the GraphQL spec says:
The ID type is serialized in the same way as a String [...]. While it is often numeric, it should always serialize as a String.
andInput Coercion
When expected as an input type, any string (such as "4") or integer (such as 4) input value should be coerced to ID as appropriate for the ID formats a given GraphQL server expects. Any other input value, including float input values (such as 4.0), must raise a query error indicating an incorrect type.
My proposal is to change t.ID
from any
to string | number
PS: Thanks for the amazing library.
It would be nice if extensions could be specified for the fields/types. We could make it type-safe by allowing a generic extension type on the createTypesFactory
function.
I noticed mutations fields
requires a callback, but subscription and query fields not.
Line 483 in 7cf0d3a
Removing the callback from the public api the callback could be an implementation detail here:
Line 489 in 7cf0d3a
There are reasons that I am missing? In case this is not intended, I can submit a PR.
I have this field:
t.field("me", {
type: UserType,
resolve: (_, _args, ctx) => {
return ctx.loggedInUserId
? ctx.dataLoaders.userById.load(ctx.loggedInUserId)
: null;
},
}),
I'd expect this to work - I haven't marked UserType
as non-nullable with t.NonNull(UserType)
, but this still errors with:
Type 'Promise<User | null> | null' is not assignable to type 'User | Promise<User>'.
Am I missing something in how this works, or have I found some form of bug?
All of those are objectType, so there is not really a requirement for having different ways of constructing those.
Changesets is a great tool for publishing new versions and preserving a changelog. https://github.com/atlassian/changesets
It can be used for tracking the major/minor/patch changes of individual contributors and automatically release them.
How does it work?
Contributors have to run a yarn changeset
command locally for generating a .md
file where they document their pull request changes in the .changesets
folder. The changeset contains a human-readable message as well as the information on whether the change is major/minor/patch
. The changeset must be committed to git.
A Github action will create a pull request on the master branch that removes the *.md
files and bumps the package version according to the documented changes. The pull request will be updated every time the master branch is updated.
Once the pull request is merged the GitHub action will automatically publish the version to npm.
So as a maintainer releasing a new version is as easy as merging a pull request.
I see that directives
can be passed into buildGraphQLSchema()
but I haven't yet figured out how to use a directive on anything. e.g. I'd like to use @oneof
on an Object Input Type or @key
on an Object Type (for @apollo/subgraph
).
Can some directive examples be added to the examples
folder and/or to the README/docs?
After reading #12, I can try to add some integration tests with graphql-js
in order to test the correctness of the schema generated.
This can lead to:
- add a CI
- increment the package quality
- self-documenting some package aspects
- clean up dependencies currently needed only for examples
Is this the correct way?
t.field("chatMessages", {
type: t.NonNull(GraphQLChatMessageConnectionType),
args: {
first: t.arg(t.Int),
},
resolve: (_, args, ctx) => {
args.first;
return {
edges: [],
pageInfo: { hasNextPage: false },
};
},
}),
I also noticed that the typing of args.first is wrong. It is number
but should actually be number | undefined | null
.
I usually try to split up my schema into modules. Is there any way for adding fields to the Query or Mutation types from within another file?
Causes no TypeScript error:
export type IMessageType = {
id: string;
authorName: string;
rawContent: string;
createdAt: Date;
};
export const GraphQLMessageType = t.objectType<IMessageType>({
name: "Message",
fields: () => [
t.defaultField("id", t.NonNull(t.ID)),
t.defaultField("authorName", t.NonNull(t.String)),
t.defaultField("rawContent", t.NonNull(t.String)),
t.field("createdAt", {
type: t.NonNull(t.String),
resolve: message => String(message.createdAt)
})
]
});
export type LiveSubscriptionType = {
query: {};
};
const GraphQLLiveSubscriptionType = t.objectType<
LiveSubscriptionType,
IContextType
>({
name: "LiveSubscription",
fields: () => [
t.field("query", {
type: GraphQLMessageType,
resolve: i => i.query
})
]
});
Causes TypeScript error:
export type IMessageType = {
id: string;
authorName: string;
rawContent: string;
createdAt: Date;
};
export const GraphQLMessageType = t.objectType<IMessageType>({
name: "Message",
fields: () => [
t.defaultField("id", t.NonNull(t.ID)),
t.defaultField("authorName", t.NonNull(t.String)),
t.defaultField("rawContent", t.NonNull(t.String)),
t.field("createdAt", {
type: t.NonNull(t.String),
resolve: message => String(message.createdAt)
})
]
});
export type LiveSubscriptionType = {
- query: {};
+ query: { lel: string };
};
const GraphQLLiveSubscriptionType = t.objectType<
LiveSubscriptionType,
IContextType
>({
name: "LiveSubscription",
fields: () => [
t.field("query", {
type: GraphQLMessageType,
resolve: i => i.query
})
]
});
Error Message:
ype '{ lel: string; }' is not assignable to type 'IMessageType | Promise<IMessageType>'.
Type '{ lel: string; }' is missing the following properties from type 'Promise<IMessageType>': then, catch, [Symbol.toStringTag], finally
Expected behavior:
Empty object definition: {}
should also cause a TypeScript Error.
export type IMessageType = {
id: string;
authorName: string;
rawContent: string;
createdAt: Date;
};
export type IRootValueType = {
messages: IMessageType[];
};
export type IGetRootValueType = () => RootValueType;
export type IContextType = {
eventEmitter: EventEmitter;
mutateState: (producer: (root: IRootValueType) => void) => void;
addMessage: (message: IMessageType) => void;
};
const Query = t.queryType<IContextType, IRootValueType>({
fields: [...MessageModule.queryFields]
});
export type LiveSubscriptionType = {
query: { swag: string };
};
const GraphQLLiveSubscriptionType = t.objectType<
LiveSubscriptionType,
IContextType
>({
name: "LiveSubscription",
// does not raise an error but should because LiveSubscriptionType["query"] is not of type `IRootValueType`
fields: () => [t.defaultField("query", Query)]
});
The following correctly raises a type error:
export type LiveSubscriptionType = {
query: { swag: string };
};
const GraphQLLiveSubscriptionType = t.objectType<
LiveSubscriptionType,
IContextType
>({
name: "LiveSubscription",
fields: () => [
t.field("query", {
type: Query,
resolve: i => i.query
})
]
});
Type '{ swag: string; }' is not assignable to type 'IRootValueType | Promise<IRootValueType>'.
Type '{ swag: string; }' is missing the following properties from type 'Promise<IRootValueType>': then, catch, [Symbol.toStringTag], finally
I'd like to be able to build multiple GraphQL schemas that have different available contexts to them in the same codebase. Currently this doesn't appear possible as context typing is done through module type augmentation.
const GraphQLChatMessageInterfaceType = t.interfaceType<ChatMessageType>({
name: "ChatMessage",
fields: () => [
t.abstractField("id", t.NonNull(t.ID)),
t.abstractField(
"content",
t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode)))
),
t.abstractField("createdAt", t.NonNull(t.String)),
t.abstractField("containsDiceRoll", t.NonNull(t.Boolean)),
],
});
const GraphQLOperationalChatMessageType = t.objectType<
OperationalChatMessageType
>({
interfaces: [GraphQLChatMessageInterfaceType],
name: "OperationalChatMessage",
fields: () => [
t.field("id", {
type: t.NonNull(t.ID),
resolve: (message) => message.id,
}),
t.field("content", {
type: t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode))),
resolve: (message) => message.content,
}),
t.field("createdAt", {
type: t.NonNull(t.String),
resolve: (message) => new Date(message.createdAt).toISOString(),
}),
t.field("containsDiceRoll", {
type: t.NonNull(t.Boolean),
resolve: (message) =>
message.content.some((node) => node.type === "DICE_ROLL"),
}),
],
});
const GraphQLUserChatMessageType = t.objectType<UserChatMessageType>({
interfaces: [GraphQLChatMessageInterfaceType],
name: "UserChatMessage",
description: "A chat message",
fields: () => [
t.field("id", {
type: t.NonNull(t.ID),
resolve: (message) => message.id,
}),
t.field("authorName", {
type: t.NonNull(t.String),
resolve: (message) => message.authorName,
}),
t.field("content", {
type: t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode))),
resolve: (message) => message.content,
}),
t.field("createdAt", {
type: t.NonNull(t.String),
resolve: (message) => new Date(message.createdAt).toISOString(),
}),
t.field("containsDiceRoll", {
type: t.NonNull(t.Boolean),
resolve: (message) =>
message.content.some((node) => node.type === "DICE_ROLL"),
}),
],
});
Unless I have no field that "consumes" the type GraphQLUserChatMessageType
or GraphQLOperationalChatMessageType
the both types are not available on the GraphQL Schema.
Also, I cannot find an option (in the TS Typings) for specifying to which actual object type the interface type resolves to.
I assumed there would be a similar API like with the union type e.g.:
const GraphQLDiceRollDetailNode = t.unionType<DiceRollDetail>({
name: "DiceRollDetail",
types: [
GraphQLDiceRollOperatorNode,
GraphQLDiceRollConstantNode,
GraphQLDiceRollDiceRollNode,
GraphQLDiceRollOpenParenNode,
GraphQLDiceRollCloseParenNode,
],
resolveType: (obj) => {
if (obj.type === "Operator") return GraphQLDiceRollOperatorNode;
else if (obj.type === "Constant") return GraphQLDiceRollConstantNode;
else if (obj.type === "DiceRoll") return GraphQLDiceRollDiceRollNode;
else if (obj.type === "OpenParen") return GraphQLDiceRollOpenParenNode;
else if (obj.type === "CloseParen") return GraphQLDiceRollCloseParenNode;
throw new Error("Invalid type");
},
});
Tools like graphql-tools do the same (https://www.graphql-tools.com/docs/resolvers#unions-and-interfaces).
Also in addition I want to mention that the union types + their implementations could be a bit more type-safe by requiring to return a tuple of the type + the required object for that object type. in the resolveType
function (See #12).
export type DiceRollDetail =
| {
type: "DiceRoll";
content: string;
detail: {
max: number;
min: number;
};
rolls: Array<number>;
}
| {
type: "Constant";
content: string;
}
| {
type: "Operator";
content: string;
}
| {
type: "OpenParen";
content: string;
}
| {
type: "CloseParen";
content: string;
};
const GraphQLDiceRollOperatorNode = t.objectType<
Extract<DiceRollDetail, { type: "Operator" }>
>({
name: "DiceRollOperatorNode",
fields: () => [
t.field("content", {
type: t.NonNull(t.String),
resolve: (object) => object.content,
}),
],
});
const GraphQLDiceRollDetailNode = t.unionType<DiceRollDetail>({
name: "DiceRollDetail",
types: [
GraphQLDiceRollOperatorNode,
GraphQLDiceRollConstantNode,
GraphQLDiceRollDiceRollNode,
GraphQLDiceRollOpenParenNode,
GraphQLDiceRollCloseParenNode,
],
resolveType: (obj) => {
if (obj.type === "Operator") {
obj // is Extract<DiceRollDetail, { type: "Operator" }>
return [GraphQLDiceRollOperatorNode, obj]
}
else if (obj.type === "Constant") {
obj // is Extract<DiceRollDetail, { type: "Constant" }>
return [GraphQLDiceRollConstantNode, obj];
}
else if (obj.type === "DiceRoll") {
obj // is Extract<DiceRollDetail, { type: "DiceRoll" }>
return GraphQLDiceRollDiceRollNode;
}
else if (obj.type === "OpenParen") {
obj // is Extract<DiceRollDetail, { type: "OpenParen" }>
return [GraphQLDiceRollOpenParenNode, obj];
}
else if (obj.type === "CloseParen") {
obj // is Extract<DiceRollDetail, { type: "CloseParen" }>
return [GraphQLDiceRollCloseParenNode, obj];
}
throw new Error("Invalid type");
},
});
When I try to import gqtx in a TypeScript project with the moduleResolution
option set to nodenext
, TypeScript can't find the correct type definitions: https://stackblitz.com/edit/typescript-dfnpmb?file=index.ts
I think this is because the package.json file for this library has "type": "module"
, but the TypeScript definition files use extensionless imports.
I was thinking about an implicit resolution of field
in order to have the same behavior as GraphQL.
At the moment there are two ways of resolving fields with the same result in return:
Explicit:
const droidType = t.objectType<Droid>({
// ...
fields: () => [
t.field('id', {
// Please note type is under a "configuration" object
type: t.NonNull(t.ID),
resolve: (src) => src.id
}),
// ...
],
});
Implicit:
const droidType = t.objectType<Droid>({
// ...
fields: () => [
// Please note the type is mandatory
t.defaultField('id', t.NonNull(t.ID), {
resolve: (src) => src.id
}),
// ...
],
});
To simplify the API having one way to obtain the same result in different ways, I did experiment with this approach:
field
becomes the only way to resolve an object; if no resolve is provided, it will resolve using the defaultField
approachThe code will be something like:
const droidType = t.objectType<Droid>({
// ...
fields: () => [
// Explicit resolver if needed
t.field('id', t.NonNull(t.ID), {
resolve: (src) => src.id
}),
// Implicit resolver if we want to resolve source's 'name' property
t.field('name', t.NonNull(t.String)),
// ...
],
});
What do you folks think?
Would love to have type safe relay helpers.
@n1ru4l It seems like you use relay in your project - any input on this?
I don't understand how to create a nullable input field
const t = createTypesFactory<ResolverContext>();
t.inputObjectType<{ asdf: string }>({
name: 'test',
fields: () => ({
asdf: {
type: t.NonNullInput(t.String),
description: 'asdf',
},
}),
});
Am I missing something or is the function or type t.Input(t.String),
just missing?
t.field("notes", {
type: t.NonNull(t.String),
args: {
first: t.arg(t.Int),
after: t.arg(t.String)
},
resolve: (_, args, context) => {
args.after // is typed as string | null but is actually undefined | null | string
},
})
This library should support GraphQL.js Version 15
With a
const query = t.queryType({ ... });
const queryable = t.interfaceType({
name: "Queryable",
fields: () => [
t.abstractField({
name: "query",
type: t.NonNull(query),
}),
],
});
how do I define a resolver for:
const mutationPayload = t.objectType({
name: 'MyMutationPayload',
interfaces: [queryable],
fields: () => [
t.field({ name: 'query', type: t.NonNull(query) })
]
});
const MyMutation = t.field({
name: "MyMutation",
type: mutationPayload,
async resolve(src, args, ctx) {
// .. stuff
return { /* do I need to put query here somehow? */ }
}
})
t.mutationType({
fields: () => [MyMutation],
})
mutation DoThing {
MyMutation {
query {
hello # ๐ get this resolved from the root.
}
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.