Skip to content

Commit cfb04bb

Browse files
committed
feat: decorator support
1 parent 87c6683 commit cfb04bb

File tree

15 files changed

+410
-12
lines changed

15 files changed

+410
-12
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"postinstall": "node ./dist/postinstall || exit 0"
1919
},
2020
"peerDependencies": {
21-
"@types/graphql": "^14.0.7",
21+
"@types/graphql": "^14.1.1",
2222
"graphql": "^14.1.1"
2323
},
2424
"dependencies": {
@@ -33,9 +33,10 @@
3333
"tslib": "^1.9.3"
3434
},
3535
"devDependencies": {
36+
"@apollo/federation": "^0.6.8",
3637
"@types/express": "^4.16.1",
3738
"@types/express-graphql": "^0.6.2",
38-
"@types/graphql": "^14.0.7",
39+
"@types/graphql": "^14.1.1",
3940
"@types/gulp": "^4.0.6",
4041
"@types/gulp-replace": "0.0.31",
4142
"@types/gulp-shell": "0.0.31",

src/decorators/Directive.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { MethodAndPropDecorator } from "./types";
2+
import { SymbolKeysNotSupportedError } from "../errors";
3+
import { getMetadataStorage } from "../metadata/getMetadataStorage";
4+
5+
export interface DirectiveArgs {
6+
[arg: string]: any;
7+
}
8+
9+
export function Directive(name: string, args?: DirectiveArgs): ClassDecorator;
10+
export function Directive(name: string, args?: DirectiveArgs): MethodAndPropDecorator;
11+
export function Directive(
12+
name: string,
13+
args?: DirectiveArgs,
14+
): MethodDecorator | PropertyDecorator | ClassDecorator {
15+
return (targetOrPrototype, propertyKey, descriptor) => {
16+
if (!propertyKey) {
17+
getMetadataStorage().collectDirectiveClassMetadata({
18+
target: targetOrPrototype as Function,
19+
name,
20+
args,
21+
});
22+
23+
return;
24+
}
25+
26+
if (typeof propertyKey === "symbol") {
27+
throw new SymbolKeysNotSupportedError();
28+
}
29+
30+
getMetadataStorage().collectDirectiveFieldMetadata({
31+
target: targetOrPrototype.constructor,
32+
field: propertyKey,
33+
name,
34+
args,
35+
});
36+
};
37+
}

src/decorators/ObjectType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getMetadataStorage } from "../metadata/getMetadataStorage";
22
import { getNameDecoratorParams } from "../helpers/decorators";
33
import { DescriptionOptions, AbstractClassOptions } from "./types";
4+
import { ObjectClassMetadata } from "../metadata/definitions/object-class-metdata";
45

56
export type ObjectOptions = DescriptionOptions &
67
AbstractClassOptions & {

src/decorators/federation.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MethodAndPropDecorator } from "./types";
2+
import { Directive } from "./Directive";
3+
4+
export interface FieldOptions {
5+
fields: string;
6+
}
7+
8+
export function FederationExtends(): ClassDecorator {
9+
return Directive("extends");
10+
}
11+
12+
export function FederationKey(opts: FieldOptions): ClassDecorator {
13+
return Directive("key", opts);
14+
}
15+
16+
export function FederationExternal(): MethodAndPropDecorator;
17+
export function FederationExternal(): MethodDecorator | PropertyDecorator {
18+
return Directive("external") as MethodDecorator | PropertyDecorator;
19+
}
20+
21+
export function FederationRequires(opts: FieldOptions): MethodAndPropDecorator;
22+
export function FederationRequires(opts: FieldOptions): MethodDecorator | PropertyDecorator {
23+
return Directive("requires", opts) as MethodDecorator | PropertyDecorator;
24+
}
25+
26+
export function FederationProvides(opts: FieldOptions): MethodAndPropDecorator;
27+
export function FederationProvides(opts: FieldOptions): MethodDecorator | PropertyDecorator {
28+
return Directive("provides", opts) as MethodDecorator | PropertyDecorator;
29+
}

src/decorators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export { Root } from "./Root";
2020
export { Subscription } from "./Subscription";
2121
export { createUnionType } from "./unions";
2222
export { UseMiddleware } from "./UseMiddleware";
23+
export * from "./federation";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { FieldMetadata } from "./field-metadata";
2+
import { DirectiveClassMetadata } from "./directive-metadata";
23

34
export interface ClassMetadata {
45
name: string;
56
target: Function;
67
fields?: FieldMetadata[];
78
description?: string;
89
isAbstract?: boolean;
10+
directives?: DirectiveClassMetadata[];
911
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface DirectiveMetadata {
2+
name: string;
3+
args?: { [key: string]: any };
4+
}
5+
6+
export type DirectiveClassMetadata = DirectiveMetadata & { target: Function };
7+
export type DirectiveFieldMetadata = DirectiveMetadata & { target: Function; field: string };

src/metadata/definitions/field-metadata.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ParamMetadata } from "./param-metadata";
22
import { TypeValueThunk, TypeOptions } from "../../decorators/types";
33
import { Middleware } from "../../interfaces/Middleware";
44
import { Complexity } from "../../interfaces";
5+
import { DirectiveFieldMetadata } from "./directive-metadata";
56

67
export interface FieldMetadata {
78
target: Function;
@@ -15,4 +16,5 @@ export interface FieldMetadata {
1516
params?: ParamMetadata[];
1617
roles?: any[];
1718
middlewares?: Array<Middleware<any>>;
19+
directives?: DirectiveFieldMetadata[];
1820
}

src/metadata/metadata-storage.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from "./utils";
2424
import { ObjectClassMetadata } from "./definitions/object-class-metdata";
2525
import { InterfaceClassMetadata } from "./definitions/interface-class-metadata";
26+
import { DirectiveClassMetadata, DirectiveFieldMetadata } from "./definitions/directive-metadata";
2627

2728
export class MetadataStorage {
2829
queries: ResolverMetadata[] = [];
@@ -37,6 +38,8 @@ export class MetadataStorage {
3738
enums: EnumMetadata[] = [];
3839
unions: UnionMetadataWithSymbol[] = [];
3940
middlewares: MiddlewareMetadata[] = [];
41+
classDirectives: DirectiveClassMetadata[] = [];
42+
fieldDirectives: DirectiveFieldMetadata[] = [];
4043

4144
private resolverClasses: ResolverClassMetadata[] = [];
4245
private fields: FieldMetadata[] = [];
@@ -98,6 +101,13 @@ export class MetadataStorage {
98101
this.params.push(definition);
99102
}
100103

104+
collectDirectiveClassMetadata(definition: DirectiveClassMetadata) {
105+
this.classDirectives.push(definition);
106+
}
107+
collectDirectiveFieldMetadata(definition: DirectiveFieldMetadata) {
108+
this.fieldDirectives.push(definition);
109+
}
110+
101111
build() {
102112
// TODO: disable next build attempts
103113

@@ -134,7 +144,7 @@ export class MetadataStorage {
134144
this.params = [];
135145
}
136146

137-
private buildClassMetadata(definitions: ClassMetadata[]) {
147+
private buildClassMetadata(definitions: ClassMetadata[], withFederation: boolean = false) {
138148
definitions.forEach(def => {
139149
const fields = this.fields.filter(field => field.target === def.target);
140150
fields.forEach(field => {
@@ -147,8 +157,10 @@ export class MetadataStorage {
147157
middleware => middleware.target === field.target && middleware.fieldName === field.name,
148158
),
149159
);
160+
field.directives = this.findFieldDirectives(field.target, field.name);
150161
});
151162
def.fields = fields;
163+
def.directives = this.findClassDirectives(def.target);
152164
});
153165
}
154166

@@ -253,4 +265,15 @@ export class MetadataStorage {
253265
}
254266
return authorizedField.roles;
255267
}
268+
269+
private findClassDirectives(target: ClassMetadata["target"]): DirectiveClassMetadata[] {
270+
return this.classDirectives.filter(it => it.target === target);
271+
}
272+
273+
private findFieldDirectives(
274+
target: FieldMetadata["target"],
275+
name: FieldMetadata["name"],
276+
): DirectiveFieldMetadata[] {
277+
return this.fieldDirectives.filter(field => field.target === target && field.field === name);
278+
}
256279
}

src/schema/schema-generator.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ import {
1515
GraphQLEnumValueConfigMap,
1616
GraphQLUnionType,
1717
GraphQLTypeResolver,
18+
GraphQLDirective,
19+
GraphQLNonNull,
20+
DirectiveNode,
21+
ObjectTypeExtensionNode,
22+
ObjectTypeDefinitionNode,
23+
FieldDefinitionNode,
24+
specifiedDirectives,
25+
ArgumentNode,
1826
} from "graphql";
1927
import { withFilter, ResolverFn } from "graphql-subscriptions";
2028

@@ -40,9 +48,13 @@ import {
4048
ConflictingDefaultValuesError,
4149
InterfaceResolveTypeError,
4250
} from "../errors";
43-
import { ResolverFilterData, ResolverTopicData, TypeResolver } from "../interfaces";
51+
import { ResolverFilterData, ResolverTopicData, TypeResolver, Maybe } from "../interfaces";
4452
import { getFieldMetadataFromInputType, getFieldMetadataFromObjectType } from "./utils";
4553
import { ensureInstalledCorrectGraphQLPackage } from "../utils/graphql-version";
54+
import {
55+
DirectiveClassMetadata,
56+
DirectiveFieldMetadata,
57+
} from "../metadata/definitions/directive-metadata";
4658

4759
interface AbstractInfo {
4860
isAbstract: boolean;
@@ -73,6 +85,7 @@ export interface SchemaGeneratorOptions extends BuildContextOptions {
7385
* Disable checking on build the correctness of a schema
7486
*/
7587
skipCheck?: boolean;
88+
directives?: GraphQLDirective[];
7689
}
7790

7891
export abstract class SchemaGenerator {
@@ -104,6 +117,7 @@ export abstract class SchemaGenerator {
104117
mutation: this.buildRootMutationType(),
105118
subscription: this.buildRootSubscriptionType(),
106119
types: this.buildOtherTypes(),
120+
directives: options.directives,
107121
});
108122

109123
BuildContext.reset();
@@ -255,12 +269,66 @@ export abstract class SchemaGenerator {
255269
return superClassTypeInfo ? superClassTypeInfo.type : undefined;
256270
};
257271
const interfaceClasses = objectType.interfaceClasses || [];
272+
273+
const classDirectiveAstNodes = (
274+
name: string,
275+
classMetas?: DirectiveClassMetadata[],
276+
): ObjectTypeDefinitionNode | undefined => {
277+
if (!classMetas || !classMetas.length) {
278+
return;
279+
}
280+
281+
const directives: DirectiveNode[] = classMetas.map(meta =>
282+
this.createDirective(meta.name, meta.args),
283+
);
284+
285+
return {
286+
kind: "ObjectTypeDefinition",
287+
name: {
288+
kind: "Name",
289+
value: name,
290+
},
291+
interfaces: [],
292+
directives,
293+
};
294+
};
295+
296+
const fieldDirectiveAstNodes = (
297+
name: string,
298+
fieldMetas?: DirectiveFieldMetadata[],
299+
): FieldDefinitionNode | undefined => {
300+
if (!fieldMetas || !fieldMetas.length) {
301+
return;
302+
}
303+
304+
const directives: DirectiveNode[] = fieldMetas.map(meta =>
305+
this.createDirective(meta.name, meta.args),
306+
);
307+
308+
return {
309+
kind: "FieldDefinition",
310+
type: {
311+
kind: "NamedType",
312+
name: {
313+
kind: "Name",
314+
value: name,
315+
},
316+
},
317+
name: {
318+
kind: "Name",
319+
value: name,
320+
},
321+
directives,
322+
};
323+
};
324+
258325
return {
259326
target: objectType.target,
260327
isAbstract: objectType.isAbstract || false,
261328
type: new GraphQLObjectType({
262329
name: objectType.name,
263330
description: objectType.description,
331+
astNode: classDirectiveAstNodes(objectType.name, objectType.directives),
264332
interfaces: () => {
265333
let interfaces = interfaceClasses.map<GraphQLInterfaceType>(
266334
interfaceClass =>
@@ -295,6 +363,7 @@ export abstract class SchemaGenerator {
295363
: createSimpleFieldResolver(field),
296364
description: field.description,
297365
deprecationReason: field.deprecationReason,
366+
astNode: fieldDirectiveAstNodes(field.name, field.directives),
298367
};
299368
return fieldsMap;
300369
},
@@ -604,4 +673,34 @@ export abstract class SchemaGenerator {
604673
return this.objectTypesInfo.find(objectType => objectType.target === resolvedType)!.type;
605674
};
606675
}
676+
677+
private static createDirective(name: string, args?: { [name: string]: any }): DirectiveNode {
678+
const directiveArguments: ArgumentNode[] = [];
679+
680+
if (args) {
681+
Object.keys(args).forEach(arg => {
682+
directiveArguments.push({
683+
kind: "Argument",
684+
name: {
685+
kind: "Name",
686+
value: arg,
687+
},
688+
value: {
689+
kind: "StringValue",
690+
value: args[arg],
691+
block: false,
692+
},
693+
});
694+
});
695+
}
696+
697+
return {
698+
kind: "Directive",
699+
name: {
700+
kind: "Name",
701+
value: name,
702+
},
703+
arguments: directiveArguments,
704+
};
705+
}
607706
}

0 commit comments

Comments
 (0)