Skip to content

Commit a969ad5

Browse files
author
Sylvain Lebresne
committed
Attempt at providing optional typechecking for directives
1 parent f5f54d9 commit a969ad5

File tree

3 files changed

+67
-23
lines changed

3 files changed

+67
-23
lines changed

core-js/src/__tests__/definitions.test.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ declare global {
3030
namespace jest {
3131
interface Matchers<R> {
3232
toHaveField(name: string, type?: Type): R;
33-
toHaveDirective(directive: DirectiveDefinition, args?: Record<string, any>): R;
33+
toHaveDirective<TArgs extends {[key: string]: any}>(directive: DirectiveDefinition<TArgs>, args?: TArgs): R;
3434
}
3535
}
3636
}
@@ -97,8 +97,8 @@ test('building a simple schema programatically', () => {
9797
const schema = new Schema(federationBuiltIns);
9898
const queryType = schema.schemaDefinition.setRoot('query', schema.addType(new ObjectType('Query')));
9999
const typeA = schema.addType(new ObjectType('A'));
100-
const inaccessible = schema.directive('inaccessible')!;
101-
const key = schema.directive('key')!;
100+
const inaccessible = federationBuiltIns.inaccessibleDirective(schema);
101+
const key = federationBuiltIns.keyDirective(schema);
102102

103103
queryType.addField('a', typeA);
104104
typeA.addField('q', queryType);
@@ -137,7 +137,7 @@ type MyQuery {
137137
expect(schema.schemaDefinition.root('query')).toBe(queryType);
138138
expect(queryType).toHaveField('a', typeA);
139139
const f2 = typeA.field('f2');
140-
expect(f2).toHaveDirective(schema.directive('inaccessible')!);
140+
expect(f2).toHaveDirective(federationBuiltIns.inaccessibleDirective(schema));
141141
expect(printSchema(schema)).toBe(sdl);
142142

143143
expect(typeA).toHaveField('f1');
@@ -224,7 +224,7 @@ test('removal of all inacessible elements of a schema', () => {
224224
`, federationBuiltIns);
225225

226226
for (const element of schema.allSchemaElement()) {
227-
if (element.appliedDirectivesOf(schema.directive('inaccessible')!).length > 0) {
227+
if (element.appliedDirectivesOf(federationBuiltIns.inaccessibleDirective(schema)).length > 0) {
228228
element.remove();
229229
}
230230
}
@@ -425,9 +425,5 @@ interface Product {
425425
expectInterfaceType(product);
426426
expect(product.field('description')!.description).toBe(longComment);
427427

428-
const directive = schema.directive('Important')!;
429-
if (directive.repeatable === false)
430-
product.appliedDirectivesOf(directive);
431-
432428
expect(printSchema(schema)).toBe(sdl);
433429
});

core-js/src/definitions.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export abstract class SchemaElement<TParent extends SchemaElement<any, any> | Sc
125125
}
126126

127127
appliedDirectivesOf(name: string): Directive[];
128-
appliedDirectivesOf(definition: DirectiveDefinition): Directive[];
128+
appliedDirectivesOf<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(definition: DirectiveDefinition<TApplicationArgs>): Directive<TApplicationArgs>[];
129129
appliedDirectivesOf(nameOrDefinition: string | DirectiveDefinition): Directive[] {
130130
const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
131131
return this._appliedDirectives.filter(d => d.name == directiveName);
@@ -156,6 +156,7 @@ export abstract class SchemaElement<TParent extends SchemaElement<any, any> | Sc
156156
Directive.prototype['setParent'].call(toAdd, this);
157157
toAdd.source = source;
158158
}
159+
// TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
159160
this._appliedDirectives.push(toAdd);
160161
DirectiveDefinition.prototype['addReferencer'].call(toAdd.definition!, toAdd);
161162
return toAdd;
@@ -351,6 +352,33 @@ export class BuiltIns {
351352
protected addBuiltInDirective(schema: Schema, name: string): DirectiveDefinition {
352353
return schema.addDirectiveDefinition(new DirectiveDefinition(name, true));
353354
}
355+
356+
protected getTypedDirective<TApplicationArgs extends {[key: string]: any}>(
357+
schema: Schema,
358+
name: string
359+
): DirectiveDefinition<TApplicationArgs> {
360+
const directive = schema.directive(name);
361+
if (!directive) {
362+
throw new Error(`The provided schema has not be built with the ${name} directive built-in`);
363+
}
364+
return directive as DirectiveDefinition<TApplicationArgs>;
365+
}
366+
367+
includeDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
368+
return this.getTypedDirective(schema, 'include');
369+
}
370+
371+
skipDirective(schema: Schema): DirectiveDefinition<{if: boolean}> {
372+
return this.getTypedDirective(schema, 'skip');
373+
}
374+
375+
deprecatedDirective(schema: Schema): DirectiveDefinition<{reason?: string}> {
376+
return this.getTypedDirective(schema, 'deprecated');
377+
}
378+
379+
specifiedByDirective(schema: Schema): DirectiveDefinition<{url: string}> {
380+
return this.getTypedDirective(schema, 'specifiedBy');
381+
}
354382
}
355383

356384
export class Schema {
@@ -1054,13 +1082,13 @@ export class EnumValue extends BaseNamedElement<EnumType, never> {
10541082
}
10551083
}
10561084

1057-
export class DirectiveDefinition extends BaseNamedElement<Schema, Directive> {
1085+
export class DirectiveDefinition<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}> extends BaseNamedElement<Schema, Directive> {
10581086
readonly kind: 'DirectiveDefinition' = 'DirectiveDefinition';
10591087

10601088
private readonly _args: Map<string, ArgumentDefinition<DirectiveDefinition>> = new Map();
10611089
repeatable: boolean = false;
10621090
private readonly _locations: DirectiveLocationEnum[] = [];
1063-
private readonly _referencers: Set<Directive> = new Set();
1091+
private readonly _referencers: Set<Directive<TApplicationArgs>> = new Set();
10641092

10651093
constructor(name: string, readonly isBuiltIn: boolean = false) {
10661094
super(name);
@@ -1132,7 +1160,7 @@ export class DirectiveDefinition extends BaseNamedElement<Schema, Directive> {
11321160
return this;
11331161
}
11341162

1135-
private addReferencer(referencer: Directive) {
1163+
private addReferencer(referencer: Directive<TApplicationArgs>) {
11361164
assert(referencer, 'Referencer should exists');
11371165
this._referencers.add(referencer);
11381166
}
@@ -1170,11 +1198,11 @@ export class DirectiveDefinition extends BaseNamedElement<Schema, Directive> {
11701198
// value if `x` has one and wasn't explicitly set in the application. This would make code usage more pleasant. Should
11711199
// `arguments()` also return those though? Maybe have an option to both method to say if it should include them or not.
11721200
// (The question stands for matchArguments() as well though).
1173-
export class Directive implements Named {
1201+
export class Directive<TArgs extends {[key: string]: any} = {[key: string]: any}> implements Named {
11741202
private _parent?: SchemaElement<any, any>;
11751203
source?: ASTNode;
11761204

1177-
constructor(readonly name: string, private _args: Record<string, any>) {}
1205+
constructor(readonly name: string, private _args: TArgs) {}
11781206

11791207
schema(): Schema | undefined {
11801208
return this._parent?.schema();
@@ -1194,14 +1222,10 @@ export class Directive implements Named {
11941222
return doc?.directive(this.name);
11951223
}
11961224

1197-
get arguments() : Record<string, any> {
1225+
get arguments() : TArgs {
11981226
return this._args;
11991227
}
12001228

1201-
argument(name: string): any {
1202-
return this._args.get(name);
1203-
}
1204-
12051229
matchArguments(expectedArgs: Record<string, any>): boolean {
12061230
const entries = Object.entries(this._args);
12071231
if (entries.length !== Object.keys(expectedArgs).length) {

core-js/src/federation.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BuiltIns, Schema } from "./definitions";
1+
import { BuiltIns, Schema, DirectiveDefinition, NonNullType } from "./definitions";
22

33
// TODO: Need a way to deal with the fact that the _Entity type is built after validation.
44
export class FederationBuiltIns extends BuiltIns {
@@ -15,7 +15,7 @@ export class FederationBuiltIns extends BuiltIns {
1515

1616
this.addBuiltInDirective(schema, 'key')
1717
.addLocations('OBJECT', 'INTERFACE')
18-
.addArgument('fields', schema.stringType());
18+
.addArgument('fields', new NonNullType(schema.stringType()));
1919

2020
this.addBuiltInDirective(schema, 'extends')
2121
.addLocations('OBJECT', 'INTERFACE');
@@ -26,12 +26,36 @@ export class FederationBuiltIns extends BuiltIns {
2626
for (const name of ['requires', 'provides']) {
2727
this.addBuiltInDirective(schema, name)
2828
.addLocations('FIELD_DEFINITION')
29-
.addArgument('fields', schema.stringType());
29+
.addArgument('fields', new NonNullType(schema.stringType()));
3030
}
3131

3232
this.addBuiltInDirective(schema, 'inaccessible')
3333
.addAllLocations();
3434
}
35+
36+
keyDirective(schema: Schema): DirectiveDefinition<{fields: string}> {
37+
return this.getTypedDirective(schema, 'key');
38+
}
39+
40+
extendsDirective(schema: Schema): DirectiveDefinition<{}> {
41+
return this.getTypedDirective(schema, 'extends');
42+
}
43+
44+
externalDirective(schema: Schema): DirectiveDefinition<{}> {
45+
return this.getTypedDirective(schema, 'external');
46+
}
47+
48+
requiresDirective(schema: Schema): DirectiveDefinition<{fields: string}> {
49+
return this.getTypedDirective(schema, 'requires');
50+
}
51+
52+
providesDirective(schema: Schema): DirectiveDefinition<{fields: string}> {
53+
return this.getTypedDirective(schema, 'provides');
54+
}
55+
56+
inaccessibleDirective(schema: Schema): DirectiveDefinition<{}> {
57+
return this.getTypedDirective(schema, 'inaccessible');
58+
}
3559
}
3660

3761
export const federationBuiltIns = new FederationBuiltIns();

0 commit comments

Comments
 (0)