Skip to content

Commit

Permalink
Merge pull request #634 from nestjs/feat/plugin-code-first
Browse files Browse the repository at this point in the history
feat(): add plugin, implement the isolated schema generator
  • Loading branch information
kamilmysliwiec authored Mar 13, 2020
2 parents bdf1780 + e814d54 commit 466dc80
Show file tree
Hide file tree
Showing 174 changed files with 5,561 additions and 583 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-types': 'off',
},
};
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ test-schema.graphql
*.test-definitions.ts

# dist
/lib/src
/dist
17 changes: 17 additions & 0 deletions lib/decorators/args-type.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';

/**
* Decorator that marks a class as a resolver arguments type.
*/
export function ArgsType(): ClassDecorator {
return (target: Function) => {
const metadata = {
name: target.name,
target,
};
LazyMetadataStorage.store(() =>
TypeMetadataStorage.addArgsMetadata(metadata),
);
};
}
121 changes: 91 additions & 30 deletions lib/decorators/args.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,76 @@ import { PipeTransform, Type } from '@nestjs/common';
import { isObject, isString } from '@nestjs/common/utils/shared.utils';
import 'reflect-metadata';
import { GqlParamtype } from '../enums/gql-paramtype.enum';
import { BasicOptions } from '../external/type-graphql.types';
import { lazyMetadataStorage } from '../storages/lazy-metadata.storage';
import { BaseTypeOptions } from '../interfaces';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { reflectTypeFromMetadata } from '../utils/reflection.utilts';
import { addPipesMetadata } from './param.utils';

let TypeGqlArg, TypeGqlArgs;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const TypeGql = require('type-graphql');
TypeGqlArg = TypeGql.Arg;
TypeGqlArgs = TypeGql.Args;
} catch (e) {}

export interface ArgsOptions extends BasicOptions {
/**
* Interface defining options that can be passed to `@Args()` decorator.
*/
export interface ArgsOptions extends BaseTypeOptions {
/**
* Name of the argument.
*/
name?: string;
/**
* Description of the argument.
*/
description?: string;
/**
* Function that returns a reference to the arguments host class.
*/
type: () => any;
}
export function Args();
export function Args(...pipes: (Type<PipeTransform> | PipeTransform)[]);

/**
* Resolver method parameter decorator. Extracts the arguments
* object from the underlying platform and populates the decorated
* parameter with the value of either all arguments or a single specified argument.
*/
export function Args(): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the arguments
* object from the underlying platform and populates the decorated
* parameter with the value of either all arguments or a single specified argument.
*/
export function Args(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the arguments
* object from the underlying platform and populates the decorated
* parameter with the value of either all arguments or a single specified argument.
*/
export function Args(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
);
): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the arguments
* object from the underlying platform and populates the decorated
* parameter with the value of either all arguments or a single specified argument.
*/
export function Args(
options: ArgsOptions,
...pipes: (Type<PipeTransform> | PipeTransform)[]
);
): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the arguments
* object from the underlying platform and populates the decorated
* parameter with the value of either all arguments or a single specified argument.
*/
export function Args(
propertyOrOptions?:
| string
| (Type<PipeTransform> | PipeTransform)
| ArgsOptions,
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
): ParameterDecorator {
let typeFn = undefined;
let argOptions = {} as BasicOptions;
let argOptions = {} as Omit<ArgsOptions, 'type'>;
let property = propertyOrOptions;

if (
Expand All @@ -46,23 +81,49 @@ export function Args(
) {
property = (propertyOrOptions as Record<string, any>).name;
typeFn = (propertyOrOptions as Record<string, any>).type;

const basicOptions = propertyOrOptions as ArgsOptions;
argOptions = {
description: (propertyOrOptions as BasicOptions).description,
nullable: (propertyOrOptions as BasicOptions).nullable,
defaultValue: (propertyOrOptions as BasicOptions).defaultValue,
description: basicOptions.description,
nullable: basicOptions.nullable,
defaultValue: basicOptions.defaultValue,
};
}

return (target, key, index) => {
return (target: Object, key: string, index: number) => {
addPipesMetadata(GqlParamtype.ARGS, property, pipes, target, key, index);
property && isString(property)
? TypeGqlArg &&
lazyMetadataStorage.store(() =>
TypeGqlArg(property, typeFn, argOptions)(target, key, index),
)
: TypeGqlArgs &&
lazyMetadataStorage.store(() =>
TypeGqlArgs(typeFn)(target, key, index),
);

LazyMetadataStorage.store(target.constructor as Type<unknown>, () => {
const { typeFn: reflectedTypeFn, options } = reflectTypeFromMetadata({
metadataKey: 'design:paramtypes',
prototype: target,
propertyKey: key,
index: index,
explicitTypeFn: typeFn,
typeOptions: argOptions,
});

const metadata = {
target: target.constructor,
methodName: key,
typeFn: reflectedTypeFn,
index,
options,
};

if (property && isString(property)) {
TypeMetadataStorage.addMethodParamMetadata({
kind: 'arg',
name: property,
description: argOptions.description,
...metadata,
});
} else {
TypeMetadataStorage.addMethodParamMetadata({
kind: 'args',
...metadata,
});
}
});
};
}
30 changes: 26 additions & 4 deletions lib/decorators/context.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,37 @@ import 'reflect-metadata';
import { GqlParamtype } from '../enums/gql-paramtype.enum';
import { createGqlPipesParamDecorator } from './param.utils';

export function Context();
export function Context(...pipes: (Type<PipeTransform> | PipeTransform)[]);
/**
* Resolver method parameter decorator. Extracts the `Context`
* object from the underlying platform and populates the decorated
* parameter with the value of `Context`.
*/
export function Context(): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the `Context`
* object from the underlying platform and populates the decorated
* parameter with the value of `Context`.
*/
export function Context(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the `Context`
* object from the underlying platform and populates the decorated
* parameter with the value of `Context`.
*/
export function Context(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
);
): ParameterDecorator;
/**
* Resolver method parameter decorator. Extracts the `Context`
* object from the underlying platform and populates the decorated
* parameter with the value of `Context`.
*/
export function Context(
property?: string | (Type<PipeTransform> | PipeTransform),
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
): ParameterDecorator {
return createGqlPipesParamDecorator(GqlParamtype.CONTEXT)(property, ...pipes);
}
3 changes: 0 additions & 3 deletions lib/decorators/delegate-property.decorator.ts

This file was deleted.

38 changes: 38 additions & 0 deletions lib/decorators/directive.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { parse } from 'graphql';
import { DirectiveParsingError } from '../schema-builder/errors/directive-parsing.error';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';

/**
* Adds a directive to specified field, type, or handler.
*/
export function Directive(
sdl: string,
): MethodDecorator & PropertyDecorator & ClassDecorator {
return (target: Function | Object, key?: string | symbol) => {
validateDirective(sdl);

LazyMetadataStorage.store(() => {
if (key) {
TypeMetadataStorage.addDirectivePropertyMetadata({
target: target.constructor,
fieldName: key as string,
sdl,
});
} else {
TypeMetadataStorage.addDirectiveMetadata({
target: target as Function,
sdl,
});
}
});
};
}

function validateDirective(sdl: string) {
try {
parse(`type String ${sdl}`);
} catch (err) {
throw new DirectiveParsingError(sdl);
}
}
124 changes: 124 additions & 0 deletions lib/decorators/field.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* The API surface of this module has been heavily inspired by the "type-graphql" library (https://github.com/MichalLytek/type-graphql), originally designed & released by Michal Lytek.
* In the v6 major release of NestJS, we introduced the code-first approach as a compatibility layer between this package and the `@nestjs/graphql` module.
* Eventually, our team decided to reimplement all the features from scratch due to a lack of flexibility.
* To avoid numerous breaking changes, the public API is backward-compatible and may resemble "type-graphql".
*/

import { isFunction } from '@nestjs/common/utils/shared.utils';
import { BaseTypeOptions } from '../interfaces/base-type-options.interface';
import { ReturnTypeFunc } from '../interfaces/return-type-func.interface';
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
import { reflectTypeFromMetadata } from '../utils/reflection.utilts';

/**
* Interface defining options that can be passed to `@InputType()` decorator.
*/
export interface FieldOptions extends BaseTypeOptions {
/**
* Name of the field.
*/
name?: string;
/**
* Description of the field.
*/
description?: string;
/**
* Field deprecation reason (if deprecated).
*/
deprecationReason?: string;
}

/**
* @Field() decorator is used to mark a specific class property as a GraphQL field.
* Only properties decorated with this decorator will be defined in the schema.
*/
export function Field(): PropertyDecorator & MethodDecorator;
/**
* @Field() decorator is used to mark a specific class property as a GraphQL field.
* Only properties decorated with this decorator will be defined in the schema.
*/
export function Field(
options: FieldOptions,
): PropertyDecorator & MethodDecorator;
/**
* @Field() decorator is used to mark a specific class property as a GraphQL field.
* Only properties decorated with this decorator will be defined in the schema.
*/
export function Field(
returnTypeFunction?: ReturnTypeFunc,
options?: FieldOptions,
): PropertyDecorator & MethodDecorator;
/**
* @Field() decorator is used to mark a specific class property as a GraphQL field.
* Only properties decorated with this decorator will be defined in the schema.
*/
export function Field(
typeOrOptions?: ReturnTypeFunc | FieldOptions,
fieldOptions?: FieldOptions,
): PropertyDecorator & MethodDecorator {
return (
prototype: Object,
propertyKey?: string,
descriptor?: TypedPropertyDescriptor<any>,
) => {
addFieldMetadata(
typeOrOptions,
fieldOptions,
prototype,
propertyKey,
descriptor,
);
};
}

export function addFieldMetadata(
typeOrOptions: ReturnTypeFunc | FieldOptions,
fieldOptions: FieldOptions,
prototype: Object,
propertyKey?: string,
descriptor?: TypedPropertyDescriptor<any>,
loadEagerly?: boolean,
) {
const [typeFunc, options = {}] = isFunction(typeOrOptions)
? [typeOrOptions, fieldOptions]
: [undefined, typeOrOptions as any];

const applyMetadataFn = () => {
const isResolver = !!descriptor;
const isResolverMethod = !!(descriptor && descriptor.value);

const { typeFn: getType, options: typeOptions } = reflectTypeFromMetadata({
metadataKey: isResolverMethod ? 'design:returntype' : 'design:type',
prototype,
propertyKey,
explicitTypeFn: typeFunc as ReturnTypeFunc,
typeOptions: options,
});

TypeMetadataStorage.addClassFieldMetadata({
name: propertyKey,
schemaName: options.name || propertyKey,
typeFn: getType,
options: typeOptions,
target: prototype.constructor,
description: options.description,
deprecationReason: options.deprecationReason,
});

if (isResolver) {
TypeMetadataStorage.addResolverPropertyMetadata({
kind: 'internal',
methodName: propertyKey,
schemaName: options.name || propertyKey,
target: prototype.constructor,
});
}
};
if (loadEagerly) {
applyMetadataFn();
} else {
LazyMetadataStorage.store(applyMetadataFn);
}
}
Loading

0 comments on commit 466dc80

Please sign in to comment.