prismake / typegql

Create GraphQL schema with TypeScript classes.
https://prismake.github.io/typegql/
MIT License
423 stars 21 forks source link

allow to register Arg at runtime dynamically #51

Open capaj opened 6 years ago

capaj commented 6 years ago

it is currently possible to register a Field onto a class at runtime, by invoking manually like this:

const decoratorFunction = Field({
            type,
            name: relationName,
            isNullable
          })
          decoratorFunction(prototype, fieldName)

however this is not possible with Arg because for dynamically created method, we don't have any metadata. The compileFieldArgs doesn't register any arguments, because calling

var inferedRawArgs = Reflect.getMetadata('design:paramtypes', target.prototype, fieldName);

obviously returns undefined and the whole compileFieldArgs function just returns without doing anything more.

It would be great If we could have some kind of

{registerDynamicArgument} from 'typegql'

registerDynamicArgument(myClass, "myMethod", "myArgumentName", {
            isNullable: true,
            type: String
          })
pie6k commented 6 years ago

Could you describe use case for this a bit more?

pie6k commented 6 years ago

Also, note you could just:

import { Arg } from 'typegql'

const paramIndex = 3;

Arg({
  type: String,
  isNullable: true
})(targetClass, fieldName, paramIndex);

And I think it's very similar to what you've written in your example :) The only difference is that you'd need to set paramIndex as number instead of name.

capaj commented 6 years ago

@pie6k I tried that exact thing on a dynamically added property and it did not work. It might work if I call it on a property which is defined at design time. I'd like to add both the property and the param decorator at runtime.

I will put together a testcase.

pie6k commented 6 years ago

Could you please describe why exactly you need this?

capaj commented 6 years ago

sure. For our production API at @leaplabs we use objection.js as our abstraction over a database. We have many models-around 40 with many relations between them. Usually a model has at least 1, but we have many which have 4-6 and more. We could have gone and added a relation @Field() resolver for each relation in every model, but that would just be copy pasting the same code all over the place. So instead we've got a custom class decorator which we put on a objection.js class. This decorator at runtime adds @Field() resolver for each relation in that class by inspecting static relationMappings on that class. It basically adds a @Field() which gives us the relations themselves and it adds another @Field() for getting a count of those related entities. For these fields which are added at runtime I would like to have Arguments as well and that's where I face this issue.

pie6k commented 6 years ago

I've added test case and it works:

it('Will allow registering argument at runtime', () => {
  @ObjectType()
  class Foo {
    @Field()
    bar(
      baz: string,
      bazRequired: string,
    ): string {
      return baz;
    }
  }

  Arg({type: String, isNullable: true})(Foo.prototype, 'bar', 0);
  Arg({type: String, isNullable: false})(Foo.prototype, 'bar', 1);

  const [bazArg, bazRequiredArg] = compileObjectType(
    Foo,
  ).getFields().bar.args;

  expect(bazArg.type).toBe(GraphQLString);
  expect(bazRequiredArg.type).toEqual(new GraphQLNonNull(GraphQLString));
});

Note that decorator is fired on target.prototype and it might be reason it didnt work for you :)

capaj commented 6 years ago

@pie6k that works indeed, but my issue is when I have a Field added at runtime also. So it would need to be like:

it('Will allow registering a Field and Arg at runtime', () => {
  @ObjectType()
  class Foo { }
  Foo.prototype.bar = function(baz: string, bazRequired: string) {}
  Field({
            type: String,
            isNullable
          })(Foo.prototype, 'bar')
  Arg({type: String, isNullable: true})(Foo.prototype, 'bar', 0);
  Arg({type: String, isNullable: false})(Foo.prototype, 'bar', 1);

  const [bazArg, bazRequiredArg] = compileObjectType(
    Foo,
  ).getFields().bar.args;

  expect(bazArg.type).toBe(GraphQLString);
  expect(bazRequiredArg.type).toEqual(new GraphQLNonNull(GraphQLString));
});

this is failing for me.