To improve decoupling the business logic from framework/graphql stuffs (resolvers as services), TypeGraphQL should allow to register param decorators to extract some data from context, root/source, etc. It could be for example @CurrentUser decorator:
const CurrentUser = createParamDecorator(({ context }) => context.user);
class SampleResolver {
@Mutation()
rate(@CurrentUser() user: User, @Arg("rate") rate: RateEnum) {
this.ratingsService.addRate({ user, rate });
}
}
Also it should be possible to create field/method decorators to provide some metadata or capture requests, eg. @CacheControl:
class SampleResolver {
@Query()
@CacheControl(5000)
sampleQuery(): boolean {
// will be called at most once per 5s
return this.database.performCostlyOperation();
}
}
This feature would be a syntax sugar over middlewares (#14) feature.
Good! I've been using routing controller for a year and now I'm planning to try type-graphql because of these familiar features. Make things a lot easier.
I suppose this can also support something such as: #47
As #14 has landed, field/method decorators are supported now :tada:
https://19majkel94.github.io/type-graphql/docs/middlewares.html#custom-decorators
@CacheControl(5000)
Is this something that's built in? I'd like to use it in my project
With #329, now it's possible to create own custom param decorators 馃殌
@prilutskiy
See https://github.com/19majkel94/type-graphql/blob/fdb7e57b8e08fddb0535dd601f1c5505b3a95fb3/examples/apollo-engine/cache-control.ts
I have a problem with custom ClassDecorators, which blocks me now.
// The class decorator
function baseConstructor(constParamsIndex: number = 0) {
const original = ctor;
// the new constructor behavior
const f: any = function newConstructor(...args: any[]) {
const instance = new original(...args);
// Do magic here
return instance;
};
// copy prototype so intanceof operator still works
f.prototype = original.prototype;
// Copy static members
Object.keys(original).forEach((name: string) => { f[name] = (<any>original)[name]; });
// return new constructor (will override original)
return f;
}
// The ObjectModel
@baseConstructor() @ObjectType()
export class TestModel {
@Field((_type) => ID) public id: string = '0';
@Field() public title: string = 'test';
// Some magic here with at least one field
}
// A resolver
@Resolver(TestModel)
export default class TestResolver extends BaseResolver {
@Query(() => TestModel)
public lalala(@Arg("id") id: string): TestModel {
return new TestModel({
id: id,
title: "test"
});
}
}
The error message will be cannot determine output type of lalala when the shema will be created.
Is there a trick or isn't it possible at the moment?
@Eluminati
The baseConstructor has to be of type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;. Where the ctor variable comes from?
Maybe it's easier to just create a new class that extends the old one?
function BaseConstructor(constParamsIndex: number = 0): ClassDecorator {
return Target => class extends Target {
constructor(...args) {
super(...args);
// Do magic here
}
};
}
Maybe it's easier to just create a new class that extends the old one?
@19majkel94 Sadly this is not possible because I want to implement a lifecycle that starts after the assignment of properties in the constructor.
ctor is your Target. I sadly can't do that like you because if I do so, every clas becomes the type BaseConstructor which could be confusing while debugging.
If it helps, here is the complete code:
export function baseConstructor(constParamsIndex: number = 0) {
/**
* Implements the life cycle of all decorated objects
*
* @param {*} object
* @param {any[]} args
*/
const lifeCycle = (object: any, args: any[]) => {
let constParams = args[constParamsIndex];
if (!(constParams instanceof Object)) constParams = {};
merge(object, constParams);
if ("constructedCallback" in object) object.constructedCallback(...args);
};
return <T extends Constructor>(ctor: T): T => {
// If the ctor is an HTMLElement, it is necessary to extend the class
// because web components have a special construction behavior.
// Otherwise the constructor will be wrapped to make sure to see the
// right object in console for better debugging
if (isBrowser() && ctor.prototype instanceof HTMLElement) {
/**
* Constructs an object with its constParams with position constParamsIndex
*
* @class BaseConstructor
* @extends {ctor}
*/
return class extends ctor {
constructor(...args: any[]) {
super(...args);
lifeCycle(this, args);
}
};
} else {
const original = ctor;
// the new constructor behavior
const f: any = function newConstructor(...args: any[]) {
const instance = new original(...args);
lifeCycle(instance, args);
return instance;
};
// copy prototype so instanceof operator still works
f.prototype = original.prototype;
// Copy static members
Object.keys(original).forEach((name: string) => { f[name] = (<any>original)[name]; });
// return new constructor (will override original)
return f;
}
};
}
You will notice, that there are 2 different ways to "wrap" the constructor. This is because I am using webcomponents which have special construction and "normal" object which will become all of the same type if I just extend the class. This hocus pocus wrapping in the else will ensure to keep type information like it should.
By the way @19majkel94 your suggestion is not possible. I found a way to wrap the Webcomponent with the same code example from you:
function BaseConstructor(constParamsIndex: number = 0): ClassDecorator {
return Target => class extends Target {
constructor(...args) {
super(...args);
// Do magic here
}
};
}
The same error appears. I think this should be possible because I saw many people doing this and then they can't use this beautiful library.
I think this should be possible
So what change to you propose to support this pattern? How TypeGraphQL can collect the proper class target?
A solution could be to provide a "typeCollectionFunction" option. Maybe globally maybe locally for each decorator which implies type collection by TypeGraphQL. So you can avoid problems with any weird pattern.
Example:
This would determine the type of every ObjectType() globally.
buildSchema({
typeCollectionFunctions: {
objectType: (prototype, key, descriptor) => {
const myProto = Object.getPrototypeOf(prototype)
if(myProto.name === "SomeWhat") return myProto;
return prototype;
}
}
}))
The same option should be provided in the ObjectType() decoreter and all other decorators should have the same possibilities (globally and locally). If both is given, the locally is prefered.
I know there is already something simmilar like @Field(type => [Rate]) but this is the type for the schema itself not for TypeGraphQL.
I solved my problen now in a simmilar way. I store the original type in a static property "graphQLObjectType" of the new created class and I give my resolver this type @query((_returns) => Test.graphQLObjectType). This seems to work well and is in fact the same procedure like my suggestion.
// My own decorator
export function baseConstructor(name?: nameOrOptsOrIndex, options?: optsOrIndex, constParamsIndex: number = 0) {
// Some ObjectType() integration logic here
return (ctor: any) => {
const prototype = Object.getPrototypeOf(ctor);
if (prototype.name === "BaseConstructor") { // The part which should go into the typeCollectionFunction
Object.setPrototypeOf(ctor, Object.getPrototypeOf(prototype));
}
return class BaseConstructor extends ctor {
public static readonly graphQLType: any = ctor; // <= This is important!
constructor(...params: any[]) {
super(...params);
// Some magic here
}
};
};
}
// My TestResolver
@resolver(Test1)
export default class TestResolver extends BaseResolver {
@query((_returns) => Test1.graphQLType) // <= The "original" type
public lalala(@arg("id") id: string): Test1 {
return new Test1({
id,
title: "TestTitle",
description: "TestDescription"
});
}
}
The same error appears.
Maybe you should apply this decorator before the TypeGraphQL one? This way as you return the new constructor, it will register the new one.
Hmm ok. Let's say the resolver looks like this:
@resolver(Model)
export default class ModelResolver {
@query((_returns) => Model)
public resolverMethod(): Model{
return new Model();
}
}
Now let's think about possible scenarios:
@baseConstructor() @ObjectType()
export class Model extends BaseModel {
@Field() myField: string = "value"
}
// => Error: Cannot determine type for resolverMethod
@ObjectType() @baseConstructor()
export class Model extends BaseModel {
@Field() myField: string = "value"
}
// => Error: BaseConstructor must contain at least one field
// If you solve this error in a very hacky way...
// => Error: Schema must contain uniquely named types but contains multiple types named "BaseConstructor" (thrown by graphql js) This is because I have multiple models with this constructor.
I think there is no other way...
Oh, so basically when you overwrite the constructor in class decorator, it will still call property/method decorators with the old target, right?
Yes, this seems to be the case.
@Eluminati
Ok, I've checked that:
type Constructor<T = any> = new (...args: any[]) => T;
let collectTarget;
const CollectClassDecorator: ClassDecorator = target => {
collectTarget = target;
};
let originalTarget;
let changedTarget;
const ChangeClassDecorator = <T extends Constructor>(Target: T) => {
originalTarget = Target;
changedTarget = class extends Target {
constructor(...args: any[]) {
super(...args);
console.log("Changed constructor");
}
};
return changedTarget;
};
let propertyPrototype;
const CollectProperty: PropertyDecorator = (prototype, propertyKey) => {
propertyPrototype = prototype;
console.log("collect", propertyKey);
};
@CollectClassDecorator
@ChangeClassDecorator
class SampleClass {
@CollectProperty
sampleProp: string;
}
const sample = new SampleClass();
// TypeGraphQL registers the modified constructor as a class
console.log(collectTarget === SampleClass);
console.log(originalTarget === SampleClass);
console.log(changedTarget === SampleClass);
console.log(collectTarget === changedTarget);
// TypeGraphQL registers the original constructor as property prototype
console.log(propertyPrototype === SampleClass.prototype);
console.log(propertyPrototype === collectTarget.prototype);
console.log(propertyPrototype === originalTarget.prototype);
console.log(propertyPrototype === changedTarget.prototype);
So this is how legacy decorators are designed to work and I can't do nothing about it. Changing the constructor/prototype chain will break all other libraries like TypeORM or class-validator.
And I won't add weird API or helpers to workaround this problem. We have to wait for the new decorators in TypeScript as it will introduce a huge breaking change:
https://github.com/tc39/proposal-decorators
Closing as both createMethodDecorator and createParamDecorator are supported now 馃挭
Most helpful comment
Good! I've been using routing controller for a year and now I'm planning to try type-graphql because of these familiar features. Make things a lot easier.