Skip to content

Commit 6f2860c

Browse files
Merge branch 'feature/graphql-plugin' of github.com:thekip/graphql into thekip-feature/graphql-plugin
2 parents 2a5b51e + 1afa497 commit 6f2860c

21 files changed

+1558
-368
lines changed

.prettierignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
packages/**/tests/generated-definitions/*.ts
2-
packages/**/tests/**/*.fixture.ts
2+
packages/**/tests/**/*.fixture.ts
3+
4+
packages/**/tests/cases/**/*.ts
5+
packages/**/tests/cases/**/*.ts

packages/graphql/lib/plugin/merge-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { isString } from '@nestjs/common/utils/shared.utils';
33
export interface PluginOptions {
44
typeFileNameSuffix?: string | string[];
55
introspectComments?: boolean;
6+
autoRegisterEnums?: boolean;
67
}
78

89
const defaultOptions: PluginOptions = {
910
typeFileNameSuffix: ['.input.ts', '.args.ts', '.entity.ts', '.model.ts'],
1011
introspectComments: false,
12+
autoRegisterEnums: false,
1113
};
1214

1315
export const mergePluginOptions = (

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

Lines changed: 226 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ import {
1212
TypeChecker,
1313
TypeFlags,
1414
TypeFormatFlags,
15-
SourceFile,
16-
CommentRange,
17-
getLeadingCommentRanges,
18-
getTrailingCommentRanges,
1915
UnionTypeNode,
2016
TypeNode,
17+
JSDoc,
18+
getTextOfJSDocComment,
19+
getJSDocDeprecatedTag,
20+
ModifiersArray,
21+
NodeArray,
22+
getJSDocTags,
2123
} from 'typescript';
2224
import { isDynamicallyAdded } from './plugin-utils';
25+
import * as ts from 'typescript';
2326

2427
export function isArray(type: Type) {
2528
const symbol = type.getSymbol();
@@ -144,7 +147,7 @@ export function getDecoratorName(decorator: Decorator) {
144147

145148
function getIdentifierFromName(expression: LeftHandSideExpression) {
146149
const identifier = getNameFromExpression(expression);
147-
if (identifier && identifier.kind !== SyntaxKind.Identifier) {
150+
if (expression && expression.kind !== SyntaxKind.Identifier) {
148151
throw new Error();
149152
}
150153
return identifier;
@@ -157,39 +160,28 @@ function getNameFromExpression(expression: LeftHandSideExpression) {
157160
return expression;
158161
}
159162

160-
export function getDescriptionOfNode(
161-
node: Node,
162-
sourceFile: SourceFile,
163-
): string {
164-
const sourceText = sourceFile.getFullText();
165-
// in case we decide to include "// comments"
166-
const replaceRegex =
167-
/^ *\** *@.*$|^ *\/\*+ *|^ *\/\/+.*|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
168-
//const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
169-
170-
const description = [];
171-
const introspectCommentsAndExamples = (comments?: CommentRange[]) =>
172-
comments?.forEach((comment) => {
173-
const commentSource = sourceText.substring(comment.pos, comment.end);
174-
const oneComment = commentSource.replace(replaceRegex, '').trim();
175-
if (oneComment) {
176-
description.push(oneComment);
177-
}
178-
});
163+
export function getJSDocDescription(node: Node): string {
164+
const jsDoc: JSDoc[] = (node as any).jsDoc;
179165

180-
const leadingCommentRanges = getLeadingCommentRanges(
181-
sourceText,
182-
node.getFullStart(),
183-
);
184-
introspectCommentsAndExamples(leadingCommentRanges);
185-
if (!description.length) {
186-
const trailingCommentRanges = getTrailingCommentRanges(
187-
sourceText,
188-
node.getFullStart(),
189-
);
190-
introspectCommentsAndExamples(trailingCommentRanges);
166+
if (!jsDoc) {
167+
return undefined;
168+
}
169+
170+
return getTextOfJSDocComment(jsDoc[0].comment);
171+
}
172+
173+
export function hasJSDocTags(node: Node, tagName: string[]): boolean {
174+
const tags = getJSDocTags(node);
175+
return tags.some((tag) => tagName.includes(tag.tagName.text));
176+
// return jsDoc;
177+
}
178+
179+
export function getJsDocDeprecation(node: Node): string {
180+
const deprecatedTag = getJSDocDeprecatedTag(node);
181+
if (!deprecatedTag) {
182+
return undefined;
191183
}
192-
return description.join('\n');
184+
return getTextOfJSDocComment(deprecatedTag.comment) || 'deprecated';
193185
}
194186

195187
export function findNullableTypeFromUnion(
@@ -200,3 +192,201 @@ export function findNullableTypeFromUnion(
200192
hasFlag(typeChecker.getTypeAtLocation(tNode), TypeFlags.Null),
201193
);
202194
}
195+
196+
export function hasModifiers(
197+
modifiers: ModifiersArray,
198+
toCheck: SyntaxKind[],
199+
): boolean {
200+
if (!modifiers) {
201+
return false;
202+
}
203+
return modifiers.some((modifier) => toCheck.includes(modifier.kind));
204+
}
205+
206+
export function hasDecorators(
207+
decorators: NodeArray<Decorator>,
208+
toCheck: string[],
209+
): boolean {
210+
if (!decorators) {
211+
return false;
212+
}
213+
214+
return decorators.some((decorator) => {
215+
return toCheck.includes(getDecoratorName(decorator));
216+
});
217+
}
218+
219+
export function hasImport(sf: ts.SourceFile, what: string): boolean {
220+
for (const statement of sf.statements) {
221+
if (
222+
ts.isImportDeclaration(statement) &&
223+
ts.isNamedImports(statement.importClause.namedBindings)
224+
) {
225+
const bindings = statement.importClause.namedBindings.elements;
226+
227+
for (const namedBinding of bindings) {
228+
if (namedBinding.name.text === what) {
229+
return true;
230+
}
231+
}
232+
}
233+
}
234+
return false;
235+
}
236+
237+
export function createImportEquals(
238+
f: ts.NodeFactory,
239+
identifier: ts.Identifier | string,
240+
from: string,
241+
): ts.ImportEqualsDeclaration {
242+
const [major, minor] = ts.versionMajorMinor?.split('.').map((x) => +x);
243+
244+
if (major == 4 && minor >= 2) {
245+
// support TS v4.2+
246+
return f.createImportEqualsDeclaration(
247+
undefined,
248+
undefined,
249+
false,
250+
identifier,
251+
f.createExternalModuleReference(f.createStringLiteral(from)),
252+
);
253+
}
254+
return (f.createImportEqualsDeclaration as any)(
255+
undefined,
256+
undefined,
257+
identifier,
258+
f.createExternalModuleReference(f.createStringLiteral(from)),
259+
);
260+
}
261+
262+
export function createNamedImport(
263+
f: ts.NodeFactory,
264+
what: string[],
265+
from: string,
266+
) {
267+
return f.createImportDeclaration(
268+
undefined,
269+
undefined,
270+
f.createImportClause(
271+
false,
272+
undefined,
273+
f.createNamedImports(
274+
what.map((name) =>
275+
f.createImportSpecifier(false, undefined, f.createIdentifier(name)),
276+
),
277+
),
278+
),
279+
f.createStringLiteral(from),
280+
);
281+
}
282+
283+
export function isCallExpressionOf(name: string, node: ts.CallExpression) {
284+
return ts.isIdentifier(node.expression) && node.expression.text === name;
285+
}
286+
287+
export type PrimitiveObject = {
288+
[key: string]: string | boolean | ts.Node | PrimitiveObject;
289+
};
290+
291+
function isNode(value: any): value is ts.Node {
292+
return typeof value === 'object' && value.constructor.name === 'NodeObject';
293+
}
294+
295+
export function serializePrimitiveObjectToAst(
296+
f: ts.NodeFactory,
297+
object: PrimitiveObject,
298+
): ts.ObjectLiteralExpression {
299+
const properties = [];
300+
301+
Object.keys(object).forEach((key) => {
302+
const value = object[key];
303+
304+
if (value === undefined) {
305+
return;
306+
}
307+
308+
let initializer: ts.Expression;
309+
if (isNode(value)) {
310+
initializer = value as ts.Expression;
311+
} else if (typeof value === 'string') {
312+
initializer = f.createStringLiteral(value);
313+
} else if (typeof value === 'boolean') {
314+
initializer = value ? f.createTrue() : f.createFalse();
315+
} else if (typeof value === 'object') {
316+
initializer = serializePrimitiveObjectToAst(f, value);
317+
}
318+
319+
properties.push(f.createPropertyAssignment(key, initializer));
320+
});
321+
322+
return f.createObjectLiteralExpression(properties);
323+
}
324+
325+
export function safelyMergeObjects(
326+
f: ts.NodeFactory,
327+
a: ts.Expression,
328+
b: ts.Expression,
329+
) {
330+
// if both of objects are ObjectLiterals, so merge property by property in compile time
331+
// if one or both of expressions not an object literal, produce rest spread and merge in runtime
332+
if (ts.isObjectLiteralExpression(a) && ts.isObjectLiteralExpression(b)) {
333+
const aMap = a.properties.reduce((acc, prop) => {
334+
acc[(prop.name as ts.Identifier).text] = prop;
335+
return acc;
336+
}, {} as { [propName: string]: ts.ObjectLiteralElementLike });
337+
338+
b.properties.forEach((prop) => {
339+
aMap[(prop.name as ts.Identifier).text] = prop;
340+
}, {});
341+
342+
return f.createObjectLiteralExpression(Object.values(aMap));
343+
} else {
344+
return f.createObjectLiteralExpression([
345+
f.createSpreadAssignment(a),
346+
f.createSpreadAssignment(b),
347+
]);
348+
}
349+
}
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/utils/plugin-utils.ts

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { head } from 'lodash';
22
import { posix } from 'path';
33
import * as ts from 'typescript';
44
import {
5-
getDecoratorName,
65
getText,
76
getTypeArguments,
87
isArray,
@@ -13,15 +12,6 @@ import {
1312
isString,
1413
} from './ast-utils';
1514

16-
export function getDecoratorOrUndefinedByNames(
17-
names: string[],
18-
decorators: ts.NodeArray<ts.Decorator>,
19-
): ts.Decorator | undefined {
20-
return (decorators || ts.createNodeArray()).find((item) =>
21-
names.includes(getDecoratorName(item)),
22-
);
23-
}
24-
2515
export function getTypeReferenceAsString(
2616
type: ts.Type,
2717
typeChecker: ts.TypeChecker,
@@ -107,22 +97,13 @@ export function isPromiseOrObservable(type: string) {
10797
return type.includes('Promise') || type.includes('Observable');
10898
}
10999

110-
export function hasPropertyKey(
111-
key: string,
112-
properties: ts.NodeArray<ts.PropertyAssignment>,
113-
): boolean {
114-
return properties
115-
.filter((item) => !isDynamicallyAdded(item))
116-
.some((item) => item.name.getText() === key);
117-
}
118-
119100
export function replaceImportPath(typeReference: string, fileName: string) {
120101
if (!typeReference.includes('import')) {
121-
return typeReference;
102+
return { typeReference, importPath: null };
122103
}
123-
let importPath = /\(\"([^)]).+(\")/.exec(typeReference)[0];
104+
let importPath = /\("([^)]).+(")/.exec(typeReference)[0];
124105
if (!importPath) {
125-
return undefined;
106+
return { typeReference: undefined, importPath: null };
126107
}
127108
importPath = convertPath(importPath);
128109
importPath = importPath.slice(2, importPath.length - 1);
@@ -153,7 +134,9 @@ export function replaceImportPath(typeReference: string, fileName: string) {
153134
}
154135

155136
typeReference = typeReference.replace(importPath, relativePath);
156-
return typeReference.replace('import', 'require');
137+
typeReference = typeReference.replace('import', 'require');
138+
139+
return { typeReference, importPath: relativePath };
157140
}
158141

159142
export function isDynamicallyAdded(identifier: ts.Node) {
@@ -234,23 +217,6 @@ export function isAutoGeneratedTypeUnion(type: ts.Type): boolean {
234217
return false;
235218
}
236219

237-
export function extractTypeArgumentIfArray(type: ts.Type) {
238-
if (isArray(type)) {
239-
type = getTypeArguments(type)[0];
240-
if (!type) {
241-
return undefined;
242-
}
243-
return {
244-
type,
245-
isArray: true,
246-
};
247-
}
248-
return {
249-
type,
250-
isArray: false,
251-
};
252-
}
253-
254220
/**
255221
* when "strict" mode enabled, TypeScript transform optional boolean properties to "boolean | undefined"
256222
* @param text

0 commit comments

Comments
 (0)