Skip to content

Commit 1afa497

Browse files
feature(plugin) add introspection data to the Field decorator if presented
1 parent 4ab789a commit 1afa497

File tree

4 files changed

+371
-255
lines changed

4 files changed

+371
-255
lines changed

packages/graphql/lib/plugin/utils/ast-utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export type PrimitiveObject = {
291291
function isNode(value: any): value is ts.Node {
292292
return typeof value === 'object' && value.constructor.name === 'NodeObject';
293293
}
294+
294295
export function serializePrimitiveObjectToAst(
295296
f: ts.NodeFactory,
296297
object: PrimitiveObject,
@@ -346,3 +347,46 @@ export function safelyMergeObjects(
346347
]);
347348
}
348349
}
350+
351+
export function updateDecoratorArguments<T extends ts.ClassDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration>(
352+
f: ts.NodeFactory,
353+
node: T,
354+
decoratorName: string,
355+
replaceFn: (decoratorArguments: ts.NodeArray<ts.Expression>) => ts.Expression[]
356+
): T {
357+
let updated = false;
358+
359+
const decorators = node.decorators.map((decorator) => {
360+
if (getDecoratorName(decorator) !== decoratorName) {
361+
return decorator;
362+
}
363+
364+
const decoratorExpression = decorator.expression as ts.CallExpression;
365+
updated = true;
366+
return f.updateDecorator(
367+
decorator,
368+
f.updateCallExpression(
369+
decoratorExpression,
370+
decoratorExpression.expression,
371+
decoratorExpression.typeArguments,
372+
replaceFn(decoratorExpression.arguments),
373+
),
374+
);
375+
});
376+
377+
if (!updated) {
378+
return node;
379+
}
380+
381+
if (ts.isClassDeclaration(node)) {
382+
return f.updateClassDeclaration(node, decorators, node.modifiers, node.name, node.typeParameters, node.heritageClauses, node.members) as T;
383+
}
384+
385+
if (ts.isPropertyDeclaration(node)) {
386+
return f.updatePropertyDeclaration(node, decorators, node.modifiers, node.name, node.questionToken, node.type, node.initializer) as T;
387+
}
388+
389+
if (ts.isGetAccessorDeclaration(node)) {
390+
return f.updateGetAccessorDeclaration(node, decorators, node.modifiers, node.name, node.parameters, node.type, node.body) as T;
391+
}
392+
}

packages/graphql/lib/plugin/visitors/model-class.visitor.ts

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
PrimitiveObject,
2626
createImportEquals,
2727
hasImport,
28-
createNamedImport,
28+
createNamedImport, updateDecoratorArguments,
2929
} from '../utils/ast-utils';
3030
import {
3131
getTypeReferenceAsString,
@@ -34,7 +34,7 @@ import {
3434
import { EnumMetadataValuesMapOptions } from '../../schema-builder/metadata';
3535
import { EnumOptions } from '../../type-factories';
3636

37-
const ALLOWED_DECORATORS = [
37+
const CLASS_DECORATORS = [
3838
ObjectType.name,
3939
InterfaceType.name,
4040
InputType.name,
@@ -77,19 +77,28 @@ export class ModelClassVisitor {
7777
const visitNode = (node: ts.Node): ts.Node => {
7878
if (
7979
ts.isClassDeclaration(node) &&
80-
hasDecorators(node.decorators, ALLOWED_DECORATORS)
80+
hasDecorators(node.decorators, CLASS_DECORATORS)
8181
) {
82-
const metadata = this.collectMetadataFromClassMembers(
82+
const members = this.amendFieldsDecorators(
8383
factory,
8484
node.members,
8585
pluginOptions,
8686
sourceFile.fileName,
8787
typeChecker,
8888
);
8989

90+
const metadata = this.collectMetadataFromClassMembers(
91+
factory,
92+
members,
93+
pluginOptions,
94+
sourceFile.fileName,
95+
typeChecker,
96+
);
97+
9098
return this.updateClassDeclaration(
9199
factory,
92100
node,
101+
members,
93102
metadata,
94103
pluginOptions,
95104
);
@@ -273,7 +282,7 @@ export class ModelClassVisitor {
273282

274283
// get one of allowed decorators from list
275284
return node.decorators.map((decorator) => {
276-
if (!ALLOWED_DECORATORS.includes(getDecoratorName(decorator))) {
285+
if (!CLASS_DECORATORS.includes(getDecoratorName(decorator))) {
277286
return decorator;
278287
}
279288

@@ -399,12 +408,49 @@ export class ModelClassVisitor {
399408
return inlineEnumName;
400409
}
401410

402-
private collectMetadataFromClassMembers(
411+
private amendFieldsDecorators(
403412
f: ts.NodeFactory,
404413
members: ts.NodeArray<ts.ClassElement>,
405414
pluginOptions: PluginOptions,
406415
hostFilename: string, // sourceFile.fileName,
407416
typeChecker: ts.TypeChecker | undefined,
417+
): ts.ClassElement[] {
418+
return members.map((member) => {
419+
if (
420+
(ts.isPropertyDeclaration(member) || ts.isGetAccessor(member))
421+
&& hasDecorators(member.decorators, [Field.name])
422+
) {
423+
try {
424+
return updateDecoratorArguments(f, member, Field.name, (decoratorArguments) => {
425+
const options = this.getOptionsFromFieldDecoratorOrUndefined(decoratorArguments);
426+
427+
const { type, ...metadata } = this.createFieldMetadata(
428+
f,
429+
member,
430+
typeChecker,
431+
hostFilename,
432+
pluginOptions,
433+
this.getTypeFromFieldDecoratorOrUndefined(decoratorArguments),
434+
);
435+
436+
const serializedMetadata = serializePrimitiveObjectToAst(f, metadata as any);
437+
return [type, options ? safelyMergeObjects(f, serializedMetadata, options) : serializedMetadata]
438+
})
439+
} catch (e) {
440+
// omit error
441+
}
442+
}
443+
444+
return member;
445+
});
446+
}
447+
448+
private collectMetadataFromClassMembers(
449+
f: ts.NodeFactory,
450+
members: ts.ClassElement[],
451+
pluginOptions: PluginOptions,
452+
hostFilename: string, // sourceFile.fileName,
453+
typeChecker: ts.TypeChecker | undefined,
408454
): ts.ObjectLiteralExpression {
409455
const properties: ts.PropertyAssignment[] = [];
410456

@@ -415,10 +461,10 @@ export class ModelClassVisitor {
415461
ts.SyntaxKind.StaticKeyword,
416462
ts.SyntaxKind.PrivateKeyword,
417463
]) &&
418-
!hasDecorators(member.decorators, [HideField.name])
464+
!hasDecorators(member.decorators, [HideField.name, Field.name])
419465
) {
420466
try {
421-
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
467+
const metadata = this.createFieldMetadata(
422468
f,
423469
member,
424470
typeChecker,
@@ -429,7 +475,7 @@ export class ModelClassVisitor {
429475
properties.push(
430476
f.createPropertyAssignment(
431477
f.createIdentifier(member.name.getText()),
432-
objectLiteralExpr,
478+
serializePrimitiveObjectToAst(f, metadata),
433479
),
434480
);
435481
} catch (e) {
@@ -444,6 +490,7 @@ export class ModelClassVisitor {
444490
private updateClassDeclaration(
445491
f: ts.NodeFactory,
446492
node: ts.ClassDeclaration,
493+
members: ts.ClassElement[],
447494
propsMetadata: ts.ObjectLiteralExpression,
448495
pluginOptions: PluginOptions,
449496
) {
@@ -470,54 +517,49 @@ export class ModelClassVisitor {
470517
node.name,
471518
node.typeParameters,
472519
node.heritageClauses,
473-
[...node.members, method],
520+
[...members, method],
474521
);
475522
}
476523

477-
private getExplicitTypeInDecoratorOrNull(
478-
member: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
479-
): ts.ArrowFunction {
480-
const fieldDecorator = member.decorators?.find(
481-
(decorator) => getDecoratorName(decorator) === Field.name,
482-
);
483-
484-
if (!fieldDecorator) {
485-
return null;
524+
private getOptionsFromFieldDecoratorOrUndefined(
525+
decoratorArguments: ts.NodeArray<ts.Expression>,
526+
): ts.Expression | undefined {
527+
if (decoratorArguments.length > 1) {
528+
return decoratorArguments[1];
486529
}
487530

488-
const expression = fieldDecorator.expression as ts.CallExpression;
531+
if (decoratorArguments.length === 1 && !ts.isArrowFunction(decoratorArguments[0])) {
532+
return decoratorArguments[0];
533+
}
534+
}
489535

536+
private getTypeFromFieldDecoratorOrUndefined(
537+
decoratorArguments: ts.NodeArray<ts.Expression>,
538+
): ts.ArrowFunction | undefined {
490539
if (
491-
expression.arguments.length > 0
492-
&& ts.isArrowFunction(expression.arguments[0])
540+
decoratorArguments.length > 0 && ts.isArrowFunction(decoratorArguments[0])
493541
) {
494-
return expression.arguments[0];
542+
return decoratorArguments[0];
495543
}
496-
497-
return null
498544
}
499545

500-
private createDecoratorObjectLiteralExpr(
546+
private createFieldMetadata(
501547
f: ts.NodeFactory,
502548
node: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
503549
typeChecker: ts.TypeChecker,
504550
hostFilename = '',
505551
pluginOptions?: PluginOptions,
506-
): ts.ObjectLiteralExpression {
552+
typeArrowFunction?: ts.ArrowFunction,
553+
) {
507554
const type = typeChecker.getTypeAtLocation(node);
508555
const isNullable =
509556
!!node.questionToken || isNull(type) || isUndefined(type);
510557

511-
let typeArrowFunction: ts.ArrowFunction;
512-
const t = this.getExplicitTypeInDecoratorOrNull(node);
513-
514-
if (t) {
515-
typeArrowFunction = t;
516-
} else {
558+
if (!typeArrowFunction) {
517559
const inlineStringEnumTypeName =
518560
this.getInlineStringEnumTypeOrUndefined(node);
519561

520-
typeArrowFunction = f.createArrowFunction(
562+
typeArrowFunction = typeArrowFunction || f.createArrowFunction(
521563
undefined,
522564
undefined,
523565
[],
@@ -542,14 +584,13 @@ export class ModelClassVisitor {
542584
? getJsDocDeprecation(node)
543585
: undefined;
544586

545-
const objectLiteral = serializePrimitiveObjectToAst(f, {
587+
588+
return {
546589
nullable: isNullable || undefined,
547590
type: typeArrowFunction,
548591
description,
549592
deprecationReason,
550-
});
551-
552-
return objectLiteral;
593+
};
553594
}
554595

555596
private getTypeUsingTypeChecker(

0 commit comments

Comments
 (0)