Giter Club home page Giter Club logo

gqtx's People

Contributors

andrew0 avatar emilios1995 avatar ericnr avatar federicobiccheddu avatar n1ru4l avatar paralax avatar sikanhe avatar zachasme avatar zth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

gqtx's Issues

Support for graphql 16

Right now package.json says "graphql": "^15.1.0",. Does that mean 16 is not supported?

README example is broken

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'.
...

Please provide an ESM build

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.)

Lazy fields declaration

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

`relay.js` missing from last beta release

Hi! Eager to try the new beta with Relay helpers! ๐Ÿ˜„ However, it seems like the actual relay.js file isn't in the shipped package, but only the type definition is.

image

Remote schemas and extending the types

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!

Self referencing objectType has type any

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
      },
    }),
  ],
});

Allow interface to contain implementation

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.

How can the resolveType function of unionType or interfaces isTypeOf be made more type-safe?

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

Docs update needed.

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?

Resolve function is necessary when a built-in scalar can be undefined

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>.

t.ID should be string | number, not any

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.
and

Input 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.

Extensions support

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.

Mutation `fields` signature

I noticed mutations fields requires a callback, but subscription and query fields not.

fields: () => Array<Field<Ctx, RootSrc, any>>;

Removing the callback from the public api the callback could be an implementation detail here:

fieldsFn: fields,

There are reasons that I am missing? In case this is not intended, I can submit a PR.

Nullability of fields returning object types

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?

proposal: use changesets for versioning

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.

directive examples

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?

Add tests

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

documentation for field arguments is outdated

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.

extending types

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?

defaultField does not raise a TypeError if the Resolver input type does not match the object source/output type

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

Interface implementation object types are not exposed on schema when not used by any field

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");
  },
});

Add implicit field resolution without the need of `defaultField`

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:

  • the object type is now always provided as second argument
  • field becomes the only way to resolve an object; if no resolve is provided, it will resolve using the defaultField approach

The 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?

Add Relay helpers

Would love to have type safe relay helpers.

@n1ru4l It seems like you use relay in your project - any input on this?

Nullable input type

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?

non required field arguments are improperly typed

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
    },
  })

Return the root query type

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],
})

Usecase

mutation DoThing {
	MyMutation {
		query {
			hello # ๐Ÿ‘ˆ get this resolved from the root.
		}
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.