From 52f090a55154bca3b0c9030cf7dff6f1599f6f94 Mon Sep 17 00:00:00 2001 From: Roman_Vasilev Date: Wed, 13 Oct 2021 01:30:40 +0400 Subject: [PATCH] feat(custom decorators): Allow apply custom decorator on models close: #63 --- .eslintrc.js | 1 + README.md | 10 +- src/handlers/model-output-type.ts | 43 ++- src/helpers/create-config.ts | 1 + src/helpers/object-settings.ts | 9 +- src/test/custom-decorators.spec.ts | 406 ++++++++++++++++------------- src/test/generate.spec.ts | 95 ++++--- src/test/helpers.ts | 62 ++++- 8 files changed, 386 insertions(+), 241 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 5f6ee9fd..b7b4dce3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -108,6 +108,7 @@ module.exports = { 'consistent-return': 0, 'max-lines': 0, '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-unsafe-member-access': 0, '@typescript-eslint/no-floating-promises': 0, '@typescript-eslint/no-non-null-assertion': 0, '@typescript-eslint/camelcase': 0, diff --git a/README.md b/README.md index 27b38d66..b9ca68b7 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ generator nestgraphql { fields_{namespace}_from = "module specifier" fields_{namespace}_input = true | false fields_{namespace}_output = true | false + fields_{namespace}_model = true | false fields_{namespace}_defaultImport = "default import name" | true fields_{namespace}_namespaceImport = "namespace import name" fields_{namespace}_namedImport = true | false @@ -367,7 +368,14 @@ Default: `false` ##### `fields_{namespace}_output` -Means that it will be applied on output types (classes decorated by `ObjectType`) +Means that it will be applied on output types (classes decorated by `ObjectType`), +including models +Type: `boolean` +Default: `false` + +##### `fields_{namespace}_model` + +Means that it will be applied only on model types (classes decorated by `ObjectType`) Type: `boolean` Default: `false` diff --git a/src/handlers/model-output-type.ts b/src/handlers/model-output-type.ts index 34c7df50..9389587a 100644 --- a/src/handlers/model-output-type.ts +++ b/src/handlers/model-output-type.ts @@ -14,7 +14,11 @@ import { getGraphqlImport } from '../helpers/get-graphql-import'; import { getOutputTypeName } from '../helpers/get-output-type-name'; import { getPropertyType } from '../helpers/get-property-type'; import { ImportDeclarationMap } from '../helpers/import-declaration-map'; -import { createObjectSettings } from '../helpers/object-settings'; +import { + createObjectSettings, + ObjectSetting, + ObjectSettings, +} from '../helpers/object-settings'; import { propertyStructure } from '../helpers/property-structure'; import { EventArguments, OutputType } from '../types'; @@ -49,9 +53,11 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { properties: [], }; (sourceFileStructure.statements as StatementStructures[]).push(classStructure); - const decorator = classStructure.decorators?.find(d => d.name === 'ObjectType'); + ok(classStructure.decorators, 'classStructure.decorators is undefined'); + const decorator = classStructure.decorators.find(d => d.name === 'ObjectType'); ok(decorator, 'ObjectType decorator not found'); + let modelSettings: ObjectSettings | undefined; // Get model settings from documentation if (model.documentation) { const objectTypeOptions: PlainObject = {}; @@ -66,6 +72,7 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { objectTypeOptions.description = documentation; } decorator.arguments = settings.getObjectTypeArguments(objectTypeOptions); + modelSettings = settings; } importDeclarations.add('Field', nestjsGraphql); @@ -170,19 +177,19 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { ], }); - for (const options of settings || []) { - if (!options.output || options.kind !== 'Decorator') { + for (const setting of settings || []) { + if (!shouldBeDecorated(setting)) { continue; } property.decorators.push({ - name: options.name, - arguments: options.arguments as string[], + name: setting.name, + arguments: setting.arguments as string[], }); ok( - options.from, + setting.from, "Missed 'from' part in configuration or field setting", ); - importDeclarations.create(options); + importDeclarations.create(setting); } for (const decorate of config.decorate) { @@ -208,6 +215,18 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { }); } + // Generate class decorators from model settings + for (const setting of modelSettings || []) { + if (!shouldBeDecorated(setting)) { + continue; + } + classStructure.decorators.push({ + name: setting.name, + arguments: setting.arguments as string[], + }); + importDeclarations.create(setting); + } + if (exportDeclaration) { sourceFile.set({ statements: [exportDeclaration, '\n', classStructure], @@ -226,6 +245,14 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { } } +function shouldBeDecorated(setting: ObjectSetting) { + return ( + setting.kind === 'Decorator' && + (setting.output || setting.model) && + !(setting.output && setting.model) + ); +} + function getExportDeclaration(name: string, statements: StatementStructures[]) { return statements.find(structure => { return ( diff --git a/src/helpers/create-config.ts b/src/helpers/create-config.ts index 9bbcd7ae..a9b3ada9 100644 --- a/src/helpers/create-config.ts +++ b/src/helpers/create-config.ts @@ -62,6 +62,7 @@ export function createConfig(data: Record) { arguments: [], output: toBoolean(value.output), input: toBoolean(value.input), + model: toBoolean(value.model), from: value.from, defaultImport: toBoolean(value.defaultImport) ? true diff --git a/src/helpers/object-settings.ts b/src/helpers/object-settings.ts index f99367b6..8d365520 100644 --- a/src/helpers/object-settings.ts +++ b/src/helpers/object-settings.ts @@ -14,6 +14,7 @@ export type ObjectSetting = { arguments?: string[] | Record; input: boolean; output: boolean; + model: boolean; match?: (test: string) => boolean; from: string; namespace?: string; @@ -43,7 +44,6 @@ export class ObjectSettings extends Array { ); } - /* eslint-disable consistent-return */ getFieldType({ name, input, @@ -70,9 +70,7 @@ export class ObjectSettings extends Array { return fieldType; } - /* eslint-enable consistent-return */ - /* eslint-disable consistent-return */ getPropertyType({ name }: ObjectSettingsFilterArgs): ObjectSetting | undefined { const propertyType = this.find(s => s.kind === 'PropertyType'); @@ -87,7 +85,6 @@ export class ObjectSettings extends Array { return propertyType; } - /* eslint-enable consistent-return */ getObjectTypeArguments(options: Record): string[] { const objectTypeOptions = merge({}, options); @@ -125,6 +122,7 @@ export function createObjectSettings(args: { arguments: [], input: false, output: false, + model: false, from: '', }; if (name === 'TypeGraphQL.omit' || name === 'HideField') { @@ -137,9 +135,6 @@ export function createObjectSettings(args: { options, { kind: name }, ); - } else if (name === 'IsAbstract') { - element.kind = 'ObjectType'; - element.arguments = { isAbstract: true }; } else if (name === 'ObjectType' && match.groups?.args) { element.kind = 'ObjectType'; const options = customType(match.groups.args) as Record; diff --git a/src/test/custom-decorators.spec.ts b/src/test/custom-decorators.spec.ts index 4eb8c26b..3e62effa 100644 --- a/src/test/custom-decorators.spec.ts +++ b/src/test/custom-decorators.spec.ts @@ -1,42 +1,12 @@ import expect from 'expect'; -import { - ClassDeclaration, - ImportDeclarationStructure, - ImportSpecifierStructure, - Project, - PropertyDeclarationStructure, - SourceFile, -} from 'ts-morph'; - -import { getPropertyStructure } from './helpers'; +import { Project, SourceFile } from 'ts-morph'; + +import { testSourceFile } from './helpers'; import { testGenerate } from './test-generate'; let sourceFile: SourceFile; -let sourceText: string; let project: Project; -let propertyStructure: PropertyDeclarationStructure; -let classFile: ClassDeclaration; let sourceFiles: SourceFile[]; -let importDeclarations: ImportDeclarationStructure[] = []; -let imports: { name: string; specifier: string }[]; - -const p = (name: string) => getPropertyStructure(sourceFile, name); -const d = (name: string) => getPropertyStructure(sourceFile, name)?.decorators?.[0]; -const t = (name: string) => - getPropertyStructure(sourceFile, name)?.decorators?.find(d => d.name === 'Field') - ?.arguments?.[0]; -const setSourceFile = (name: string) => { - sourceFile = project.getSourceFile(s => s.getFilePath().endsWith(name))!; - classFile = sourceFile.getClass(() => true)!; - sourceText = sourceFile.getText(); - importDeclarations = sourceFile.getImportDeclarations().map(d => d.getStructure()); - imports = importDeclarations.flatMap(d => - (d.namedImports as ImportSpecifierStructure[]).map(x => ({ - name: x.name, - specifier: d.moduleSpecifier, - })), - ); -}; describe('custom decorators namespace both input and output', () => { before(async () => { @@ -67,32 +37,48 @@ describe('custom decorators namespace both input and output', () => { describe('aggregates should not have validators', () => { it('user-count-aggregate.input', () => { - setSourceFile('user-count-aggregate.input.ts'); - expect( - p('email')?.decorators?.find(d => d.name.includes('IsEmail')), - ).toBeUndefined(); - expect(t('email')).toEqual('() => Boolean'); + const s = testSourceFile({ + project, + file: 'user-count-aggregate.input.ts', + property: 'email', + }); + expect(s.propertyDecorators).toHaveLength(1); + expect(s.propertyDecorators).not.toContainEqual( + expect.objectContaining({ + name: 'IsEmail', + }), + ); + expect(s.fieldDecoratorType).toEqual('() => Boolean'); }); it('user-count-order-by-aggregate.input name is type of sort order', () => { - setSourceFile('user-count-order-by-aggregate.input.ts'); + const s = testSourceFile({ + project, + file: 'user-count-order-by-aggregate.input.ts', + property: 'email', + }); expect( - p('email')?.decorators?.find(d => d.name.includes('IsEmail')), + s.propertyDecorators?.find(d => d.name.includes('IsEmail')), ).toBeUndefined(); - expect(t('email')).toEqual('() => SortOrder'); + expect(s.fieldDecoratorType).toEqual('() => SortOrder'); }); }); describe('custom decorators in user create input', () => { + let sourceFile: SourceFile; + let importDeclarations: any[]; + let classFile: any; + before(() => { - setSourceFile('user-create.input.ts'); + ({ sourceFile, classFile } = testSourceFile({ + project, + file: 'user-create.input.ts', + })); importDeclarations = sourceFile .getImportDeclarations() .map(d => d.getStructure()); }); - // it('^', () => console.log(sourceFile.getText())); - it('decorator validator maxlength should exists', () => { const d = classFile .getProperty('name') @@ -111,8 +97,12 @@ describe('custom decorators namespace both input and output', () => { }); it('several decorators length', () => { - const decorators = p('age')?.decorators; - expect(decorators).toHaveLength(3); + const s = testSourceFile({ + project, + file: 'user-create.input.ts', + property: 'age', + }); + expect(s.propertyDecorators).toHaveLength(3); }); it('validator should be imported once', () => { @@ -122,29 +112,42 @@ describe('custom decorators namespace both input and output', () => { }); }); - describe('user model output should not have validator decorator', () => { - before(() => setSourceFile('user.model.ts')); - - describe('should not have metadata in description', () => { - it('age', () => { - expect(d('age')?.arguments?.[1]).not.toContain('description'); + describe('should not have metadata in description', () => { + it('age', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'age', }); + expect(s.fieldDecoratorOptions).not.toContain('description'); + }); - it('name', () => { - expect(d('name')?.arguments?.[1]).not.toContain('description'); + it('name', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'name', }); + expect(s.fieldDecoratorOptions).not.toContain('description'); + }); - it('email', () => { - expect(d('email')?.arguments?.[1]).not.toContain('description'); + it('email', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'email', }); + expect(s.fieldDecoratorOptions).not.toContain('description'); }); + }); - it('output model has no maxlength decorator', () => { - const decorator = p('name')?.decorators?.find(d => d.name === 'MaxLength'); - expect(decorator).toBeFalsy(); + it('output model has no maxlength decorator', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'name', }); - - // it('^', () => console.log(sourceFile.getText())); + expect(s.propertyDecorators?.find(d => d.name === 'MaxLength')).toBeFalsy(); }); }); @@ -168,8 +171,12 @@ describe('fieldtype disable output', () => { }); it('upload image output', () => { - setSourceFile('user.model.ts'); - expect(t('image')).toEqual('() => String'); + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'image', + }); + expect(s.fieldDecoratorType).toEqual('() => String'); }); }); @@ -193,22 +200,26 @@ describe('custom decorators and description', () => { })); }); - describe('user model output', () => { - before(() => setSourceFile('user.model.ts')); - - it('has description', () => { - const data = d('name')?.arguments?.[1]; - expect(data).toContain("description:'User name really'"); + it('has description', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'name', }); + expect(s.fieldDecoratorOptions).toContain("description:'User name really'"); + }); - it('has decorator length', () => { - const decorators = p('name')?.decorators; - expect(decorators).toHaveLength(2); - expect(decorators).toContainEqual( - expect.objectContaining({ name: 'Length' }), - ); - expect(sourceText).toContain('@Validator.Length(5, 15, "check length")'); + it('has decorator length', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'name', }); + expect(s.propertyDecorators).toHaveLength(2); + expect(s.propertyDecorators).toContainEqual( + expect.objectContaining({ name: 'Length' }), + ); + expect(s.sourceText).toContain('@Validator.Length(5, 15, "check length")'); }); }); @@ -230,63 +241,63 @@ describe('custom decorators default import', () => { })); }); - describe('in user create input', () => { - before(() => { - setSourceFile('user-create.input.ts'); + it('importDeclarations should import default', () => { + const s = testSourceFile({ + project, + file: 'user-create.input.ts', }); - it('importDeclarations should import default', () => { - importDeclarations = sourceFile - .getImportDeclarations() - .map(d => d.getStructure()) - .filter(d => d.moduleSpecifier === 'is-valid-name'); + const importDeclarations = s.sourceFile + .getImportDeclarations() + .map(d => d.getStructure()) + .filter(d => d.moduleSpecifier === 'is-valid-name'); - expect(importDeclarations).toHaveLength(1); - expect(importDeclarations[0]).toEqual( - expect.objectContaining({ - defaultImport: 'IsValidName', - namedImports: [], - namespaceImport: undefined, - }), - ); - }); - - // it('^', () => console.log(sourceFile.getText())); + expect(importDeclarations).toHaveLength(1); + expect(importDeclarations[0]).toEqual( + expect.objectContaining({ + defaultImport: 'IsValidName', + namedImports: [], + namespaceImport: undefined, + }), + ); }); +}); - describe('default import alternative syntax', () => { - before(async () => { - ({ project, sourceFiles } = await testGenerate({ - schema: ` +describe('default import alternative syntax', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + schema: ` model User { id Int @id /// @IsEmail() name String }`, - options: [ - `outputFilePattern = "{name}.{type}.ts"`, - `fields_IsEmail_from = "isvalidemail"`, - `fields_IsEmail_input = true`, - `fields_IsEmail_defaultImport = true`, - ], - })); - setSourceFile('user-create.input.ts'); - }); + options: [ + `outputFilePattern = "{name}.{type}.ts"`, + `fields_IsEmail_from = "isvalidemail"`, + `fields_IsEmail_input = true`, + `fields_IsEmail_defaultImport = true`, + ], + })); + }); - it('test', () => { - importDeclarations = sourceFile - .getImportDeclarations() - .map(d => d.getStructure()) - .filter(d => d.moduleSpecifier === 'isvalidemail'); - expect(importDeclarations).toHaveLength(1); - expect(importDeclarations[0]).toEqual( - expect.objectContaining({ - defaultImport: 'IsEmail', - namedImports: [], - namespaceImport: undefined, - }), - ); + it('test', () => { + const s = testSourceFile({ + project, + file: 'user-create.input.ts', }); + const importDeclarations = s.sourceFile + .getImportDeclarations() + .map(d => d.getStructure()) + .filter(d => d.moduleSpecifier === 'isvalidemail'); + expect(importDeclarations).toHaveLength(1); + expect(importDeclarations[0]).toEqual( + expect.objectContaining({ + defaultImport: 'IsEmail', + namedImports: [], + namespaceImport: undefined, + }), + ); }); }); @@ -312,60 +323,46 @@ describe('custom decorators field custom type namespace', () => { }); describe('user create input', () => { - before(() => { - setSourceFile('user-create.input.ts'); - }); - it('email field type', () => { - const decorator = p('email')?.decorators?.find(d => d.name === 'Field'); - const typeArgument = decorator?.arguments?.[0]; - expect(typeArgument).toEqual('() => Scalars.EmailAddress'); - }); - - it('should not apply to field as decorator', () => { - const decorators = p('email')?.decorators; - expect(decorators).toHaveLength(1); + const s = testSourceFile({ + project, + file: 'user-create.input.ts', + property: 'email', + }); + expect(s.fieldDecoratorType).toEqual('() => Scalars.EmailAddress'); + expect(s.propertyDecorators).toHaveLength(1); }); it('field type secondemail', () => { - const decorator = p('secondEmail')?.decorators?.find( - d => d.name === 'Field', - ); - const typeArgument = decorator?.arguments?.[0]; - expect(typeArgument).toEqual('() => Scalars.EmailAddress'); + const s = testSourceFile({ + project, + file: 'user-create.input.ts', + property: 'secondEmail', + }); + expect(s.fieldDecoratorType).toEqual('() => Scalars.EmailAddress'); }); it('importdeclarations should import namespace', () => { - importDeclarations = sourceFile - .getImportDeclarations() - .map(d => d.getStructure()) - .filter(d => d.moduleSpecifier === 'graphql-scalars'); - - expect(importDeclarations).toHaveLength(1); - expect(importDeclarations[0]).toEqual( - expect.objectContaining({ - defaultImport: undefined, - namedImports: [], - namespaceImport: 'Scalars', - }), - ); + const s = testSourceFile({ + project, + file: 'user-create.input.ts', + }); + expect(s.namespaceImports).toContainEqual({ + name: 'Scalars', + specifier: 'graphql-scalars', + }); }); - - // it('^', () => console.log(sourceFile.getText())); }); describe('custom type user model', () => { - before(() => { - setSourceFile('user.model.ts'); - }); - it('custom type user model email field type', () => { - const decorator = p('email')?.decorators?.find(d => d.name === 'Field'); - const typeArgument = decorator?.arguments?.[0]; - expect(typeArgument).toEqual('() => Scalars.EmailAddress'); + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'email', + }); + expect(s.fieldDecoratorType).toEqual('() => Scalars.EmailAddress'); }); - - // it('^', () => console.log(sourceFile.getText())); }); }); @@ -400,58 +397,117 @@ describe('decorate option', () => { }); it('validatenested create one user args', () => { - setSourceFile('create-one-user.args.ts'); - const data = p('data'); - expect(data?.decorators).toContainEqual( + const s = testSourceFile({ + project, + file: 'create-one-user.args.ts', + property: 'data', + }); + expect(s.propertyDecorators).toContainEqual( expect.objectContaining({ name: 'ValidateNested', arguments: [], typeArguments: [], }), ); - expect(data?.decorators).toContainEqual( + expect(s.propertyDecorators).toContainEqual( expect.objectContaining({ name: 'Type', arguments: ['() => UserCreateInput'], typeArguments: [], }), ); - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'Type', specifier: 'class-transformer', }); - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'ValidateNested', specifier: 'class-validator', }); }); it('validatenested create many user args', () => { - setSourceFile('create-many-user.args.ts'); - const data = p('data'); - expect(data?.decorators).toContainEqual( + const s = testSourceFile({ + project, + file: 'create-many-user.args.ts', + property: 'data', + }); + + expect(s.propertyDecorators).toContainEqual( expect.objectContaining({ name: 'ValidateNested', arguments: [], typeArguments: [], }), ); - expect(data?.decorators).toContainEqual( + expect(s.propertyDecorators).toContainEqual( expect.objectContaining({ name: 'Type', arguments: ['() => UserCreateManyInput'], typeArguments: [], }), ); - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'Type', specifier: 'class-transformer', }); - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'ValidateNested', specifier: 'class-validator', }); }); }); -// it('^', () => console.log(sourceFile.getText())); +describe('model decorate', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + schema: ` + /// @NG.Directive('@extends') + /// @NG.Directive('@key(fields: "id")') + model User { + /// @NG.Directive('@external') + id String @id + }`, + options: [ + `outputFilePattern = "{name}.{type}.ts"`, + `fields_NG_from = "@nestjs/graphql"`, + `fields_NG_output = false`, + `fields_NG_model = true`, + ], + })); + }); + + it('user model id property', () => { + const { propertyDecorators } = testSourceFile({ + project, + file: 'user.model.ts', + property: 'id', + }); + expect(propertyDecorators?.find(d => d.name === 'Directive')).toBeTruthy(); + expect( + propertyDecorators?.find(d => d.name === 'Directive')?.arguments?.[0], + ).toBe("'@external'"); + }); + + it('user model class', () => { + const s = testSourceFile({ + project, + file: 'user.model.ts', + }); + expect(s.namespaceImports).toContainEqual({ + name: 'NG', + specifier: '@nestjs/graphql', + }); + expect(s.classFile.getDecorator('Directive')).toBeTruthy(); + }); + + it('usergroupby should not have ng.directive', () => { + const s = testSourceFile({ + project, + file: 'user-group-by.output.ts', + property: 'id', + }); + expect(s.propertyDecorators).toHaveLength(1); + expect(s.propertyDecorators?.find(d => d.name === 'Directive')).toBeFalsy(); + }); +}); diff --git a/src/test/generate.spec.ts b/src/test/generate.spec.ts index 45bb5d58..51110fe4 100644 --- a/src/test/generate.spec.ts +++ b/src/test/generate.spec.ts @@ -14,7 +14,7 @@ import { } from 'ts-morph'; import { EventArguments } from '../types'; -import { getFieldOptions, getPropertyStructure } from './helpers'; +import { getFieldOptions, getPropertyStructure, testSourceFile } from './helpers'; import { testGenerate } from './test-generate'; let sourceFile: SourceFile; @@ -64,7 +64,10 @@ describe('model with one id int', () => { }); describe('model', () => { - before(() => setSourceFile('user.model.ts')); + before(() => { + setSourceFile('user.model.ts'); + ({ classFile } = testSourceFile({ project, file: 'user.model.ts' })); + }); it('class should be exported', () => { const [classFile] = sourceFile.getClasses(); @@ -72,7 +75,12 @@ describe('model with one id int', () => { }); it('argument decorated id', () => { - expect(t('id')).toEqual('() => ID'); + const { fieldDecoratorType } = testSourceFile({ + project, + file: 'user.model.ts', + property: 'id', + }); + expect(fieldDecoratorType).toEqual('() => ID'); }); it('should have import graphql type id', () => { @@ -176,8 +184,6 @@ describe('model with one id int', () => { ?.getStructure()!; }); - // it('', () => console.log(sourceFile.getText())); - it('id property should be Int/number', () => { expect(propertyStructure.type).toEqual('number'); expect(d('id')?.arguments?.[0]).toEqual('() => Int'); @@ -196,38 +202,49 @@ describe('model with one id int', () => { }); describe('where input', () => { - before(() => { - sourceFile = project.getSourceFile(s => - s.getFilePath().endsWith('user-where.input.ts'), - )!; - }); - - // it('', () => console.log(sourceFile.getText())); - it('should have id property', () => { - id = getPropertyStructure(sourceFile, 'id')!; - expect(id).toEqual(expect.objectContaining({ name: 'id' })); + const { property } = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'id', + }); + expect(property?.name).toEqual('id'); }); it('should have type IntFilter', () => { - id = getPropertyStructure(sourceFile, 'id')!; - expect(id.type).toEqual('IntFilter'); + const { property } = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'id', + }); + expect(property?.type).toEqual('IntFilter'); }); it('field decorator returns IntFilter', () => { - const argument = t('id'); - expect(argument).toEqual('() => IntFilter'); + const { fieldDecoratorType } = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'id', + }); + expect(fieldDecoratorType).toEqual('() => IntFilter'); }); it('field decorator IntFilter nullable', () => { - const argument = getFieldOptions(sourceFile, 'id'); - expect(argument).toMatch(/nullable:\s*true/); + const { fieldDecoratorOptions } = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'id', + }); + expect(fieldDecoratorOptions).toMatch(/nullable:\s*true/); }); it('property AND has one type', () => { - expect(getPropertyStructure(sourceFile, 'AND')?.type).toEqual( - 'Array', - ); + const { property } = testSourceFile({ + project, + file: 'user-where.input.ts', + property: 'AND', + }); + expect(property?.type).toEqual('Array'); }); }); @@ -237,11 +254,13 @@ describe('model with one id int', () => { classFile = sourceFile.getClass(() => true)!; }); - // it('', () => console.log(sourceFile.getText())); - it('decorator name args', () => { + const { classFile } = testSourceFile({ + project, + file: 'user-aggregate.args.ts', + }); const decorator = classFile.getDecorator('ArgsType'); - expect(decorator).toBeTruthy(); + expect(decorator?.getText()).toEqual('@ArgsType()'); }); it('no duplicated properties', () => { @@ -695,8 +714,6 @@ describe('nullish compatibility', () => { expect(p('posts')?.hasQuestionToken).toBe(true); }); }); - - // it('', () => console.log(sourceFile.getText())); }); describe('one model with enum', () => { @@ -779,8 +796,6 @@ describe('one model with enum', () => { setSourceFile('user.model.ts'); }); - // it('', () => console.log('sourceText', sourceText)); - it('should import Role as enum', () => { expect(imports).toContainEqual({ name: 'Role', @@ -812,8 +827,6 @@ describe('one model with self reference', () => { setSourceFile('user.model.ts'); }); - // it('', () => console.log('sourceText', sourceText)); - it('should not contain import to self file', () => { expect(imports).not.toContainEqual( expect.objectContaining({ name: 'User' }), @@ -2003,22 +2016,6 @@ describe('noTypeId config', () => { }); describe('object model options', () => { - it('user model should have abstract true', async () => { - ({ project, sourceFiles } = await testGenerate({ - schema: ` - /// @IsAbstract() - model User { - id Int @id - }`, - options: [`outputFilePattern = "{name}.{type}.ts"`], - })); - - setSourceFile('user.model.ts'); - const argument = objectTypeArguments()?.[0]; - const json = JSON5.parse(argument); - expect(json).toEqual({ isAbstract: true }); - }); - it('abstract true by objecttype', async () => { ({ project, sourceFiles } = await testGenerate({ schema: ` diff --git a/src/test/helpers.ts b/src/test/helpers.ts index d37577cb..c16b90ea 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1,4 +1,10 @@ -import { PropertyDeclaration, SourceFile } from 'ts-morph'; +import { + ImportSpecifierStructure, + Project, + PropertyDeclaration, + PropertyDeclarationStructure, + SourceFile, +} from 'ts-morph'; export function getFieldOptions( sourceFile: SourceFile, @@ -19,3 +25,57 @@ export function getPropertyStructure(sourceFile: SourceFile, name: string) { ?.getProperty(p => p.getName() === name) ?.getStructure(); } + +export function testSourceFile(args: { + project: Project; + file: string; + property?: string; +}) { + const { project, file, property } = args; + const sourceFile = project.getSourceFileOrThrow(s => + s.getFilePath().endsWith(file), + ); + const importDeclarations = sourceFile + .getImportDeclarations() + .map(d => d.getStructure()); + const classFile = sourceFile.getClass(() => true)!; + const propertyStructure = + property && + classFile.getProperty(p => p.getName() === property)?.getStructure(); + const propertyDecorators = ( + propertyStructure as PropertyDeclarationStructure | undefined + )?.decorators; + const fieldDecorator = propertyDecorators?.find(d => d.name === 'Field'); + + type ImportElement = { name: string; specifier: string }; + const namedImports: ImportElement[] = []; + const namespaceImports: ImportElement[] = []; + + for (const d of importDeclarations) { + if (d.namespaceImport) { + namespaceImports.push({ + name: d.namespaceImport, + specifier: d.moduleSpecifier, + }); + } + for (const s of (d.namedImports || []) as ImportSpecifierStructure[]) { + namedImports.push({ + name: s.name, + specifier: d.moduleSpecifier, + }); + } + } + + return { + sourceFile, + classFile, + sourceText: sourceFile.getText(), + namedImports, + namespaceImports, + property: propertyStructure as PropertyDeclarationStructure | undefined, + propertyDecorators, + fieldDecorator, + fieldDecoratorType: fieldDecorator?.arguments?.[0], + fieldDecoratorOptions: fieldDecorator?.arguments?.[1], + }; +}