Skip to content

Commit d6faef0

Browse files
committed
feat(custom decorators): Allow attach @Directive()
1 parent a2a8d46 commit d6faef0

9 files changed

+221
-130
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ module.exports = {
107107
'sonarjs/no-identical-functions': 0,
108108
'consistent-return': 0,
109109
'max-lines': 0,
110+
'regexp/strict': 0,
110111
'@typescript-eslint/no-explicit-any': 0,
111112
'@typescript-eslint/no-unsafe-member-access': 0,
112113
'@typescript-eslint/no-floating-promises': 0,

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,34 @@ export class User {
571571
}
572572
```
573573
574+
### @Directive()
575+
576+
Allow attach `@Directive` decorator from `@nestjs/graphql`
577+
578+
GraphQL federation example:
579+
580+
```
581+
/// @Directive({ arguments: ['@extends'] })
582+
/// @Directive({ arguments: ['@key(fields: "id")'] })
583+
model User {
584+
/// @Directive({ arguments: ['@external'] })
585+
id String @id
586+
}
587+
```
588+
589+
May generate:
590+
591+
```ts
592+
@ObjectType()
593+
@Directive('@extends')
594+
@Directive('@key(fields: "id")')
595+
export class User {
596+
@Field(() => ID, { nullable: false })
597+
@Directive('@external')
598+
id!: string;
599+
}
600+
```
601+
574602
#### @ObjectType()
575603
576604
Allow rename type in schema and mark as abstract.

package.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,23 @@
6767
"devDependencies": {
6868
"@commitlint/cli": "^13.2.1",
6969
"@commitlint/config-conventional": "^13.2.0",
70-
"@nestjs/common": "^8.0.11",
71-
"@nestjs/core": "^8.0.11",
70+
"@nestjs/common": "^8.1.1",
71+
"@nestjs/core": "^8.1.1",
7272
"@nestjs/graphql": "^9.0.6",
73-
"@nestjs/platform-express": "^8.0.11",
73+
"@nestjs/platform-express": "^8.1.1",
7474
"@paljs/plugins": "^4.0.8",
7575
"@prisma/client": "^3.2.1",
7676
"@semantic-release/changelog": "^6.0.0",
7777
"@semantic-release/git": "^10.0.0",
7878
"@types/flat": "^5.0.2",
7979
"@types/lodash": "^4.14.175",
8080
"@types/mocha": "^9.0.0",
81-
"@types/node": "^16.10.4",
81+
"@types/node": "^16.11.0",
8282
"@types/pluralize": "^0.0.29",
8383
"@typescript-eslint/eslint-plugin": "^4.33.0",
8484
"@typescript-eslint/parser": "^4.33.0",
85-
"apollo-server-express": "^3.3.0",
86-
"c8": "^7.9.0",
85+
"apollo-server-express": "^3.4.0",
86+
"c8": "^7.10.0",
8787
"class-transformer": "^0.4.0",
8888
"class-validator": "^0.13.1",
8989
"commitizen": "^4.2.4",
@@ -92,7 +92,7 @@
9292
"eslint": "^7.32.0",
9393
"eslint-import-resolver-node": "^0.3.6",
9494
"eslint-plugin-etc": "^1.5.4",
95-
"eslint-plugin-import": "^2.25.1",
95+
"eslint-plugin-import": "^2.25.2",
9696
"eslint-plugin-only-warn": "^1.0.3",
9797
"eslint-plugin-prettier": "^4.0.0",
9898
"eslint-plugin-promise": "^5.1.0",
@@ -107,9 +107,9 @@
107107
"ghooks": "^2.0.4",
108108
"git-branch-is": "^4.0.0",
109109
"graphql": "^15.6.1",
110-
"graphql-scalars": "^1.11.1",
110+
"graphql-scalars": "^1.12.0",
111111
"graphql-type-json": "^0.3.2",
112-
"mocha": "^9.1.2",
112+
"mocha": "^9.1.3",
113113
"ololog": "^1.1.175",
114114
"precise-commits": "^1.0.2",
115115
"prettier": "^2.4.1",

src/handlers/input-type.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -165,18 +165,22 @@ export function inputType(
165165

166166
if (isCustomsApplicable) {
167167
for (const options of settings || []) {
168-
if (!options.input || options.kind !== 'Decorator') {
169-
continue;
168+
if (
169+
(options.kind === 'Decorator' &&
170+
options.input &&
171+
options.match?.(name)) ??
172+
true
173+
) {
174+
property.decorators.push({
175+
name: options.name,
176+
arguments: options.arguments as string[],
177+
});
178+
ok(
179+
options.from,
180+
"Missed 'from' part in configuration or field setting",
181+
);
182+
importDeclarations.create(options);
170183
}
171-
property.decorators.push({
172-
name: options.name,
173-
arguments: options.arguments as string[],
174-
});
175-
ok(
176-
options.from,
177-
"Missed 'from' part in configuration or field setting",
178-
);
179-
importDeclarations.create(options);
180184
}
181185
}
182186

src/handlers/model-output-type.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -178,18 +178,20 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) {
178178
});
179179

180180
for (const setting of settings || []) {
181-
if (!shouldBeDecorated(setting)) {
182-
continue;
181+
if (
182+
shouldBeDecorated(setting) &&
183+
(setting.match?.(field.name) ?? true)
184+
) {
185+
property.decorators.push({
186+
name: setting.name,
187+
arguments: setting.arguments as string[],
188+
});
189+
ok(
190+
setting.from,
191+
"Missed 'from' part in configuration or field setting",
192+
);
193+
importDeclarations.create(setting);
183194
}
184-
property.decorators.push({
185-
name: setting.name,
186-
arguments: setting.arguments as string[],
187-
});
188-
ok(
189-
setting.from,
190-
"Missed 'from' part in configuration or field setting",
191-
);
192-
importDeclarations.create(setting);
193195
}
194196

195197
for (const decorate of config.decorate) {
@@ -217,14 +219,14 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) {
217219

218220
// Generate class decorators from model settings
219221
for (const setting of modelSettings || []) {
220-
if (!shouldBeDecorated(setting)) {
221-
continue;
222+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
223+
if (shouldBeDecorated(setting)) {
224+
classStructure.decorators.push({
225+
name: setting.name,
226+
arguments: setting.arguments as string[],
227+
});
228+
importDeclarations.create(setting);
222229
}
223-
classStructure.decorators.push({
224-
name: setting.name,
225-
arguments: setting.arguments as string[],
226-
});
227-
importDeclarations.create(setting);
228230
}
229231

230232
if (exportDeclaration) {

src/handlers/output-type.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,22 @@ export function outputType(outputType: OutputType, args: EventArguments) {
147147

148148
if (isCustomsApplicable) {
149149
for (const options of settings || []) {
150-
if (!options.output || options.kind !== 'Decorator') {
151-
continue;
150+
if (
151+
(options.kind === 'Decorator' &&
152+
options.output &&
153+
options.match?.(field.name)) ??
154+
true
155+
) {
156+
property.decorators.push({
157+
name: options.name,
158+
arguments: options.arguments as string[],
159+
});
160+
ok(
161+
options.from,
162+
"Missed 'from' part in configuration or field setting",
163+
);
164+
importDeclarations.create(options);
152165
}
153-
property.decorators.push({
154-
name: options.name,
155-
arguments: options.arguments as string[],
156-
});
157-
ok(
158-
options.from,
159-
"Missed 'from' part in configuration or field setting",
160-
);
161-
importDeclarations.create(options);
162166
}
163167
}
164168
}

src/helpers/object-settings.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ export function createObjectSettings(args: {
148148
name: options.name,
149149
isAbstract: options.isAbstract,
150150
};
151+
} else if (name === 'Directive' && match.groups?.args) {
152+
const options = customType(match.groups.args);
153+
merge(element, { model: true, from: '@nestjs/graphql' }, options, {
154+
name,
155+
namespace: false,
156+
kind: 'Decorator',
157+
arguments: Array.isArray(options.arguments)
158+
? options.arguments.map(s => JSON5.stringify(s))
159+
: options.arguments,
160+
});
151161
} else {
152162
const namespace = getNamespace(name);
153163
element.namespaceImport = namespace;
@@ -158,7 +168,7 @@ export function createObjectSettings(args: {
158168
.map(s => trim(s))
159169
.filter(Boolean),
160170
};
161-
merge(element, config.fields[namespace], options);
171+
merge(element, namespace && config.fields[namespace], options);
162172
}
163173
result.push(element);
164174
}
@@ -234,10 +244,14 @@ function parseArgs(string: string): Record<string, unknown> | string {
234244
}
235245
}
236246

237-
function getNamespace(name: unknown) {
247+
function getNamespace(name: unknown): string | undefined {
248+
if (name === undefined) {
249+
return undefined;
250+
}
238251
let result = String(name);
239252
if (result.includes('.')) {
240253
[result] = result.split('.');
241254
}
255+
// eslint-disable-next-line consistent-return
242256
return result;
243257
}

src/test/custom-decorators.spec.ts

+62-10
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@ import { Project, SourceFile } from 'ts-morph';
44
import { testSourceFile } from './helpers';
55
import { testGenerate } from './test-generate';
66

7-
let sourceFile: SourceFile;
87
let project: Project;
9-
let sourceFiles: SourceFile[];
108

119
describe('custom decorators namespace both input and output', () => {
1210
before(async () => {
13-
({ project, sourceFiles } = await testGenerate({
11+
({ project } = await testGenerate({
1412
schema: `
1513
model User {
1614
id Int @id
@@ -153,7 +151,7 @@ describe('custom decorators namespace both input and output', () => {
153151

154152
describe('fieldtype disable output', () => {
155153
before(async () => {
156-
({ project, sourceFiles } = await testGenerate({
154+
({ project } = await testGenerate({
157155
schema: `
158156
model User {
159157
id String @id @default(cuid())
@@ -182,7 +180,7 @@ describe('fieldtype disable output', () => {
182180

183181
describe('custom decorators and description', () => {
184182
before(async () => {
185-
({ project, sourceFiles } = await testGenerate({
183+
({ project } = await testGenerate({
186184
schema: `
187185
model User {
188186
/// user id really
@@ -225,7 +223,7 @@ describe('custom decorators and description', () => {
225223

226224
describe('custom decorators default import', () => {
227225
before(async () => {
228-
({ project, sourceFiles } = await testGenerate({
226+
({ project } = await testGenerate({
229227
schema: `
230228
model User {
231229
id Int @id
@@ -265,7 +263,7 @@ describe('custom decorators default import', () => {
265263

266264
describe('default import alternative syntax', () => {
267265
before(async () => {
268-
({ project, sourceFiles } = await testGenerate({
266+
({ project } = await testGenerate({
269267
schema: `
270268
model User {
271269
id Int @id
@@ -303,7 +301,7 @@ describe('default import alternative syntax', () => {
303301

304302
describe('custom decorators field custom type namespace', () => {
305303
before(async () => {
306-
({ project, sourceFiles } = await testGenerate({
304+
({ project } = await testGenerate({
307305
schema: `
308306
model User {
309307
id Int @id
@@ -368,7 +366,7 @@ describe('custom decorators field custom type namespace', () => {
368366

369367
describe('decorate option', () => {
370368
before(async () => {
371-
({ project, sourceFiles } = await testGenerate({
369+
({ project } = await testGenerate({
372370
schema: `
373371
model User {
374372
id Int @id @default(autoincrement())
@@ -460,7 +458,7 @@ describe('decorate option', () => {
460458

461459
describe('model decorate', () => {
462460
before(async () => {
463-
({ project, sourceFiles } = await testGenerate({
461+
({ project } = await testGenerate({
464462
schema: `
465463
/// @NG.Directive('@extends')
466464
/// @NG.Directive('@key(fields: "id")')
@@ -511,3 +509,57 @@ describe('model decorate', () => {
511509
expect(s.propertyDecorators?.find(d => d.name === 'Directive')).toBeFalsy();
512510
});
513511
});
512+
513+
describe('model directive', () => {
514+
before(async () => {
515+
({ project } = await testGenerate({
516+
schema: `
517+
/// @Directive({ arguments: ['@extends'] })
518+
/// @Directive({ arguments: ['@key(fields: "id")'] })
519+
model User {
520+
/// @Directive({ arguments: ['@external'] })
521+
id String @id
522+
}`,
523+
options: [`outputFilePattern = "{name}.{type}.ts"`],
524+
}));
525+
});
526+
527+
it('user model id property', () => {
528+
const s = testSourceFile({
529+
project,
530+
file: 'user.model.ts',
531+
property: 'id',
532+
});
533+
console.log('s.sourceText', s.sourceText);
534+
expect(s.propertyDecorators?.find(d => d.name === 'Directive')).toBeTruthy();
535+
expect(
536+
s.propertyDecorators?.find(d => d.name === 'Directive')?.arguments?.[0],
537+
).toBe("'@external'");
538+
});
539+
540+
it('user model class', () => {
541+
const s = testSourceFile({
542+
project,
543+
file: 'user.model.ts',
544+
});
545+
expect(s.namedImports).toContainEqual({
546+
name: 'Directive',
547+
specifier: '@nestjs/graphql',
548+
});
549+
expect(s.classFile.getDecorator('Directive')).toBeTruthy();
550+
});
551+
552+
it('usergroupby should not have ng.directive', () => {
553+
const s = testSourceFile({
554+
project,
555+
file: 'user-group-by.output.ts',
556+
property: 'id',
557+
});
558+
expect(s.propertyDecorators).toHaveLength(1);
559+
expect(s.propertyDecorators?.find(d => d.name === 'Directive')).toBeFalsy();
560+
expect(s.namedImports).not.toContainEqual({
561+
name: 'Directive',
562+
specifier: '@nestjs/graphql',
563+
});
564+
});
565+
});

0 commit comments

Comments
 (0)