From 432e8f1f5b61f214df9e823cf63b2c455ac59e65 Mon Sep 17 00:00:00 2001 From: Roman_Vasilev Date: Fri, 6 May 2022 18:18:47 +0400 Subject: [PATCH] feat: Allow add deprecation reason close: #104 --- .eslintrc.js | 2 +- README.md | 198 +++++++++++++++++------------- src/handlers/input-type.ts | 1 + src/handlers/model-output-type.ts | 9 +- src/handlers/output-type.ts | 1 + src/helpers/create-comment.ts | 20 +++ src/helpers/object-settings.ts | 192 ++++++++++++++++++++--------- src/test/generate.spec.ts | 147 +++++++++++++++++----- src/test/hide-field.spec.ts | 21 ++-- src/test/mongodb.spec.ts | 23 ++++ 10 files changed, 425 insertions(+), 189 deletions(-) create mode 100644 src/helpers/create-comment.ts diff --git a/.eslintrc.js b/.eslintrc.js index d06ea51f..4b10eae2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,7 +35,7 @@ module.exports = { ], rules: { // core - 'consistent-return': [1, { treatUndefinedAsUnspecified: true }], + 'consistent-return': [0, { treatUndefinedAsUnspecified: false }], quotes: [1, 'single', { allowTemplateLiterals: true, avoidEscape: true }], semi: [1, 'always'], 'max-lines': [1, { max: 300 }], diff --git a/README.md b/README.md index 0214e790..75d33692 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Generate object types, inputs, args, etc. from prisma schema file for usage with ## Features -- Generates only necessary imports -- Combines zoo of nested/nullable filters -- Does not generate resolvers, since it's application specific +- Generates only necessary imports +- Combines zoo of nested/nullable filters +- Does not generate resolvers, since it's application specific ## Install @@ -40,8 +40,8 @@ npm install graphql-type-json prisma-graphql-type-decimal ``` -- [graphql-type-json](https://github.com/taion/graphql-type-json) -- [prisma-graphql-type-decimal](https://github.com/unlight/prisma-graphql-type-decimal) +- [graphql-type-json](https://github.com/taion/graphql-type-json) +- [prisma-graphql-type-decimal](https://github.com/unlight/prisma-graphql-type-decimal) Or write you own graphql scalar types, [read more on docs.nestjs.com](https://docs.nestjs.com/graphql/scalars). @@ -59,10 +59,10 @@ Type: `string` Default: `{model}/{name}.{type}.ts` Possible tokens: -- `{model}` Model name in dashed case or 'prisma' if unknown -- `{name}` Dashed-case name of model/input/arg without suffix -- `{type}` Short type name (model, input, args, output) -- `{plural.type}` Plural short type name (models, inputs, enums) +- `{model}` Model name in dashed case or 'prisma' if unknown +- `{name}` Dashed-case name of model/input/arg without suffix +- `{type}` Short type name (model, input, args, output) +- `{plural.type}` Plural short type name (models, inputs, enums) #### `tsConfigFilePath` @@ -149,30 +149,30 @@ generator nestgraphql { Where: -- `typeName` Full name or partial name of the class where need to choose input type. - Example: `UserCreateInput` full name, `WhereInput` partial name, matches `UserWhereInput`, `PostWhereInput`, etc. -- `property` Property of the class for which need to choose type. Special case name `ALL` means any / all properties. -- `pattern` Part of name (or full) of type which should be chosen, you can use - wild card or negate symbols, in this case pattern should starts with `match:`, - e.g. `match:*UncheckedCreateInput` see [outmatch](https://github.com/axtgr/outmatch#usage) for details. +- `typeName` Full name or partial name of the class where need to choose input type. + Example: `UserCreateInput` full name, `WhereInput` partial name, matches `UserWhereInput`, `PostWhereInput`, etc. +- `property` Property of the class for which need to choose type. Special case name `ALL` means any / all properties. +- `pattern` Part of name (or full) of type which should be chosen, you can use + wild card or negate symbols, in this case pattern should starts with `match:`, + e.g. `match:*UncheckedCreateInput` see [outmatch](https://github.com/axtgr/outmatch#usage) for details. Example: ```ts export type PostWhereInput = { - author?: XOR; + author?: XOR; }; export type UserRelationFilter = { - is?: UserWhereInput; - isNot?: UserWhereInput; + is?: UserWhereInput; + isNot?: UserWhereInput; }; export type UserWhereInput = { - AND?: Enumerable; - OR?: Enumerable; - NOT?: Enumerable; - id?: StringFilter | string; - name?: StringFilter | string; + AND?: Enumerable; + OR?: Enumerable; + NOT?: Enumerable; + id?: StringFilter | string; + name?: StringFilter | string; }; ``` @@ -190,8 +190,8 @@ generator nestgraphql { ```ts @InputType() export class PostWhereInput { - @Field(() => UserWhereInput, { nullable: true }) - author?: UserWhereInput; + @Field(() => UserWhereInput, { nullable: true }) + author?: UserWhereInput; } ``` @@ -214,24 +214,24 @@ generator nestgraphql { Where `{key}` any identifier to group values (written in [flatten](https://github.com/hughsk/flat) style) -- `decorate_{key}_type` - outmatch pattern to match class name -- `decorate_{key}_field` - outmatch pattern to match field name -- `decorate_{key}_from` - module specifier to import from (e.g `class-validator`) -- `decorate_{key}_name` - import name or name with namespace -- `decorate_{key}_defaultImport` - import as default -- `decorate_{key}_namespaceImport` - use this name as import namespace -- `decorate_{key}_namedImport` - named import (without namespace) -- `decorate_{key}_arguments` - arguments for decorator (if decorator need to be called as function) - Special tokens can be used: - - `{propertyType.0}` - field's type (TypeScript type annotation) +- `decorate_{key}_type` - outmatch pattern to match class name +- `decorate_{key}_field` - outmatch pattern to match field name +- `decorate_{key}_from` - module specifier to import from (e.g `class-validator`) +- `decorate_{key}_name` - import name or name with namespace +- `decorate_{key}_defaultImport` - import as default +- `decorate_{key}_namespaceImport` - use this name as import namespace +- `decorate_{key}_namedImport` - named import (without namespace) +- `decorate_{key}_arguments` - arguments for decorator (if decorator need to be called as function) + Special tokens can be used: + - `{propertyType.0}` - field's type (TypeScript type annotation) Example of generated class: ```ts @ArgsType() export class CreateOneUserArgs { - @Field(() => UserCreateInput, { nullable: false }) - data!: UserCreateInput; + @Field(() => UserCreateInput, { nullable: false }) + data!: UserCreateInput; } ``` @@ -259,10 +259,10 @@ import { Type } from 'class-transformer'; @ArgsType() export class CreateOneUserArgs { - @Field(() => UserCreateInput, { nullable: false }) - @ValidateNested() - @Type(() => UserCreateInput) - data!: UserCreateInput; + @Field(() => UserCreateInput, { nullable: false }) + @ValidateNested() + @Type(() => UserCreateInput) + data!: UserCreateInput; } ``` @@ -329,13 +329,45 @@ May generate: import { GraphQLBigInt } from 'graphql-scalars'; export class BigIntFilter { - @Field(() => GraphQLBigInt, { nullable: true }) - equals?: bigint | number; + @Field(() => GraphQLBigInt, { nullable: true }) + equals?: bigint | number; } ``` It will affect all inputs and outputs types (including models). +## Documentation and field options + +Comments with double slash will projected to typescript code comments +and some `@Field()` decorator options + +For example: + +```prisma +model Product { + // Old description + // @deprecated Use new name instead + oldName +} +``` + +May produce: + +```ts +@ObjectType() +export class Product { + /** + * Old description + * @deprecated Use new name instead + */ + @Field(() => String, { + description: 'Old description', + deprecationReason: 'Use new name instead', + }) + oldName: string; +} +``` + ## Field Settings Special directives in triple slash comments for more precise code generation. @@ -352,9 +384,9 @@ see [outmatch](https://github.com/axtgr/outmatch#usage) for details. Examples: -- `@HideField()` same as `@HideField({ output: true })` -- `@HideField({ input: true, output: true })` -- `@HideField({ match: 'UserCreate*Input' })` +- `@HideField()` same as `@HideField({ output: true })` +- `@HideField({ input: true, output: true })` +- `@HideField({ match: 'UserCreate*Input' })` ```prisma model User { @@ -373,24 +405,24 @@ May generate classes: ```ts @ObjectType() export class User { - @HideField() - password: string; - @HideField() - secret: string; - @Field(() => Date, { nullable: false }) - createdAt: Date; + @HideField() + password: string; + @HideField() + secret: string; + @Field(() => Date, { nullable: false }) + createdAt: Date; } ``` ```ts @InputType() export class UserCreateInput { - @Field() - password: string; - @HideField() - secret: string; - @HideField() - createdAt: Date; + @Field() + password: string; + @HideField() + secret: string; + @HideField() + createdAt: Date; } ``` @@ -480,9 +512,9 @@ import * as Validator from 'class-validator'; @InputType() export class UserCreateInput { - @Field(() => String, { nullable: false }) - @Validator.MinLength(3) - name!: string; + @Field(() => String, { nullable: false }) + @Validator.MinLength(3) + name!: string; } ``` @@ -539,8 +571,8 @@ import * as Scalars from 'graphql-scalars'; @InputType() export class UserCreateInput { - @Field(() => Scalars.GraphQLEmailAddress, { nullable: false }) - email!: string; + @Field(() => Scalars.GraphQLEmailAddress, { nullable: false }) + email!: string; } ``` @@ -602,8 +634,8 @@ import * as TF from 'type-fest'; @ObjectType() export class User { - @Field(() => GraphQLJSON) - data!: TF.JsonObject; + @Field(() => GraphQLJSON) + data!: TF.JsonObject; } ``` @@ -629,9 +661,9 @@ May generate: @Directive('@extends') @Directive('@key(fields: "id")') export class User { - @Field(() => ID, { nullable: false }) - @Directive('@external') - id!: string; + @Field(() => ID, { nullable: false }) + @Directive('@external') + id!: string; } ``` @@ -671,21 +703,21 @@ export class User {} ## Similar Projects -- https://github.com/kimjbstar/prisma-class-generator -- https://github.com/odroe/nest-gql-mix -- https://github.com/rfermann/nestjs-prisma-graphql-generator -- https://github.com/madscience/graphql-codegen-nestjs -- https://github.com/wSedlacek/prisma-generators/tree/master/libs/nestjs -- https://github.com/EndyKaufman/typegraphql-prisma-nestjs -- https://github.com/MichalLytek/typegraphql-prisma +- https://github.com/kimjbstar/prisma-class-generator +- https://github.com/odroe/nest-gql-mix +- https://github.com/rfermann/nestjs-prisma-graphql-generator +- https://github.com/madscience/graphql-codegen-nestjs +- https://github.com/wSedlacek/prisma-generators/tree/master/libs/nestjs +- https://github.com/EndyKaufman/typegraphql-prisma-nestjs +- https://github.com/MichalLytek/typegraphql-prisma ## Resources -- Todo - https://github.com/unlight/prisma-nestjs-graphql/issues/2 -- https://github.com/prisma/prisma/blob/main/packages/client/src/generation/TSClient/TSClient.ts -- https://ts-ast-viewer.com/ -- https://github.com/unlight/nestjs-graphql-prisma-realworld-example-app -- https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model -- JSON type for the code first approach - https://github.com/nestjs/graphql/issues/111#issuecomment-631452899 -- https://github.com/paljs/prisma-tools/tree/master/packages/plugins -- https://github.com/wasp-lang/wasp +- Todo - https://github.com/unlight/prisma-nestjs-graphql/issues/2 +- https://github.com/prisma/prisma/blob/main/packages/client/src/generation/TSClient/TSClient.ts +- https://ts-ast-viewer.com/ +- https://github.com/unlight/nestjs-graphql-prisma-realworld-example-app +- https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/data-model +- JSON type for the code first approach - https://github.com/nestjs/graphql/issues/111#issuecomment-631452899 +- https://github.com/paljs/prisma-tools/tree/master/packages/plugins +- https://github.com/wasp-lang/wasp diff --git a/src/handlers/input-type.ts b/src/handlers/input-type.ts index 007b14b3..1a3cc200 100644 --- a/src/handlers/input-type.ts +++ b/src/handlers/input-type.ts @@ -183,6 +183,7 @@ export function inputType( arguments: [ isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`, JSON5.stringify({ + ...settings?.fieldArguments(), nullable: !isRequired, }), ], diff --git a/src/handlers/model-output-type.ts b/src/handlers/model-output-type.ts index 10dae407..7c12f84e 100644 --- a/src/handlers/model-output-type.ts +++ b/src/handlers/model-output-type.ts @@ -9,6 +9,7 @@ import { StatementStructures, StructureKind, } from 'ts-morph'; +import { createComment } from '../helpers/create-comment'; import { getGraphqlImport } from '../helpers/get-graphql-import'; import { getOutputTypeName } from '../helpers/get-output-type-name'; @@ -67,7 +68,7 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { }); if (documentation) { if (!classStructure.leadingTrivia) { - classStructure.leadingTrivia = `/** ${documentation} */\n`; + classStructure.leadingTrivia = createComment(documentation); } objectTypeOptions.description = documentation; } @@ -147,14 +148,14 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { }); if (typeof property.leadingTrivia === 'string' && modelField?.documentation) { - property.leadingTrivia += `/** ${modelField.documentation} */\n`; + property.leadingTrivia += createComment(modelField.documentation, settings); } classStructure.properties?.push(property); if (propertySettings) { importDeclarations.create({ ...propertySettings }); - } else if (!propertySettings && propertyType.includes('Decimal')) { + } else if (propertyType.includes('Decimal')) { importDeclarations.add('Decimal', '@prisma/client/runtime'); } @@ -164,11 +165,13 @@ export function modelOutputType(outputType: OutputType, args: EventArguments) { importDeclarations.add('HideField', nestjsGraphql); property.decorators.push({ name: 'HideField', arguments: [] }); } else { + // Generate `@Field()` decorator property.decorators.push({ name: 'Field', arguments: [ isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`, JSON5.stringify({ + ...settings?.fieldArguments(), nullable: Boolean(field.isNullable), defaultValue: ['number', 'string', 'boolean'].includes( typeof modelField?.default, diff --git a/src/handlers/output-type.ts b/src/handlers/output-type.ts index 704160ef..f472b691 100644 --- a/src/handlers/output-type.ts +++ b/src/handlers/output-type.ts @@ -158,6 +158,7 @@ export function outputType(outputType: OutputType, args: EventArguments) { arguments: [ isList ? `() => [${graphqlType}]` : `() => ${graphqlType}`, JSON5.stringify({ + ...settings?.fieldArguments(), nullable: Boolean(field.isNullable), }), ], diff --git a/src/helpers/create-comment.ts b/src/helpers/create-comment.ts new file mode 100644 index 00000000..b86a9c3b --- /dev/null +++ b/src/helpers/create-comment.ts @@ -0,0 +1,20 @@ +import { ObjectSettings } from '../types'; + +export function createComment(documentation: string, settings?: ObjectSettings) { + const documentationLines = documentation.split('\n'); + const commentLines = ['/**']; + + for (const line of documentationLines) { + commentLines.push(` * ${line}`); + } + + const deprecationReason = settings?.fieldArguments()?.deprecationReason as string; + + if (deprecationReason) { + commentLines.push(` * @deprecated ${deprecationReason}`); + } + + commentLines.push(' */\n'); + + return commentLines.join('\n'); +} diff --git a/src/helpers/object-settings.ts b/src/helpers/object-settings.ts index ad925e00..b1b51ea6 100644 --- a/src/helpers/object-settings.ts +++ b/src/helpers/object-settings.ts @@ -10,7 +10,7 @@ export type ObjectSetting = { * Act as named import or namespaceImport or defaultImport */ name: string; - kind: 'Decorator' | 'FieldType' | 'PropertyType' | 'ObjectType'; + kind: 'Decorator' | 'Field' | 'FieldType' | 'PropertyType' | 'ObjectType'; arguments?: string[] | Record; input: boolean; output: boolean; @@ -111,6 +111,13 @@ export class ObjectSettings extends Array { } return resultArguments.map(x => JSON5.stringify(x)); } + + fieldArguments(): Record | undefined { + const item = this.find(item => item.kind === 'Field'); + if (item) { + return item.arguments as Record; + } + } } export function createObjectSettings(args: { @@ -121,76 +128,141 @@ export function createObjectSettings(args: { const result = new ObjectSettings(); const textLines = text.split('\n'); const documentationLines: string[] = []; + + let fieldElement = result.find(item => item.kind === 'Field'); + if (!fieldElement) { + fieldElement = { + name: '', + kind: 'Field', + arguments: {}, + } as ObjectSetting; + } + for (const line of textLines) { const match = /^@(?\w+(\.(\w+))?)\((?.*)\)/.exec(line); - const name = match?.groups?.name; - if (!match || !name) { - documentationLines.push(line); - continue; + const { element, documentLine } = createSettingElement({ + line, + config, + fieldElement, + match, + }); + + if (element) { + result.push(element); } - const element: ObjectSetting = { - kind: 'Decorator', - name: '', - arguments: [], - input: false, - output: false, - model: false, - from: '', - }; - if (name === 'TypeGraphQL.omit' || name === 'HideField') { - Object.assign(element, hideFieldDecorator(match)); - } else if (['FieldType', 'PropertyType'].includes(name) && match.groups?.args) { - const options = customType(match.groups.args); - merge( - element, - options.namespace && config.fields[options.namespace], - options, - { kind: name }, - ); - } else if (name === 'ObjectType' && match.groups?.args) { - element.kind = 'ObjectType'; - const options = customType(match.groups.args) as Record; - if (typeof options[0] === 'string' && options[0]) { - options.name = options[0]; - } - if (isObject(options[1])) { - merge(options, options[1]); - } - element.arguments = { - name: options.name, - isAbstract: options.isAbstract, - }; - } else if (name === 'Directive' && match.groups?.args) { - const options = customType(match.groups.args); - merge(element, { model: true, from: '@nestjs/graphql' }, options, { - name, - namespace: false, - kind: 'Decorator', - arguments: Array.isArray(options.arguments) - ? options.arguments.map(s => JSON5.stringify(s)) - : options.arguments, - }); - } else { - const namespace = getNamespace(name); - element.namespaceImport = namespace; - const options = { - name, - arguments: (match.groups?.args || '') - .split(',') - .map(s => trim(s)) - .filter(Boolean), - }; - merge(element, namespace && config.fields[namespace], options); + + if (documentLine) { + documentationLines.push(line); } - result.push(element); } return { settings: result, - documentation: documentationLines.filter(Boolean).join('\\n') || undefined, + documentation: documentationLines.filter(Boolean).join('\n') || undefined, }; } +function createSettingElement({ + line, + config, + fieldElement, + match, +}: { + line: string; + config: GeneratorConfiguration; + fieldElement: ObjectSetting; + match: RegExpExecArray | null; +}) { + const result = { + documentLine: '', + element: undefined as ObjectSetting | undefined, + }; + if (line.startsWith('@deprecated')) { + fieldElement.arguments!['deprecationReason'] = trim(line.slice(11)); + + result.element = fieldElement; + + return result; + } + + const name = match?.groups?.name; + + if (!(match && name)) { + result.documentLine = line; + return result; + } + + const element: ObjectSetting = { + kind: 'Decorator', + name: '', + arguments: [], + input: false, + output: false, + model: false, + from: '', + }; + + result.element = element; + + if (name === 'TypeGraphQL.omit' || name === 'HideField') { + Object.assign(element, hideFieldDecorator(match)); + + return result; + } + + if (['FieldType', 'PropertyType'].includes(name) && match.groups?.args) { + const options = customType(match.groups.args); + merge(element, options.namespace && config.fields[options.namespace], options, { + kind: name, + }); + return result; + } + + if (name === 'ObjectType' && match.groups?.args) { + element.kind = 'ObjectType'; + const options = customType(match.groups.args) as Record; + if (typeof options[0] === 'string' && options[0]) { + options.name = options[0]; + } + if (isObject(options[1])) { + merge(options, options[1]); + } + element.arguments = { + name: options.name, + isAbstract: options.isAbstract, + }; + + return result; + } + + if (name === 'Directive' && match.groups?.args) { + const options = customType(match.groups.args); + merge(element, { model: true, from: '@nestjs/graphql' }, options, { + name, + namespace: false, + kind: 'Decorator', + arguments: Array.isArray(options.arguments) + ? options.arguments.map(s => JSON5.stringify(s)) + : options.arguments, + }); + + return result; + } + + const namespace = getNamespace(name); + element.namespaceImport = namespace; + const options = { + name, + arguments: (match.groups?.args || '') + .split(',') + .map(s => trim(s)) + .filter(Boolean), + }; + merge(element, namespace && config.fields[namespace], options); + + return result; +} + function customType(args: string) { const result: Partial = {}; let options = parseArgs(args); diff --git a/src/test/generate.spec.ts b/src/test/generate.spec.ts index aa4fb850..c9b76cac 100644 --- a/src/test/generate.spec.ts +++ b/src/test/generate.spec.ts @@ -60,14 +60,13 @@ describe('model with one id int', () => { }); describe('model', () => { + let s: ReturnType; before(() => { - setSourceFile('user.model.ts'); - ({ classFile } = testSourceFile({ project, file: 'user.model.ts' })); + s = testSourceFile({ project, file: 'user.model.ts' }); }); it('class should be exported', () => { - const [classFile] = sourceFile.getClasses(); - expect(classFile.isExported()).toBe(true); + expect(s.classFile.isExported()).toBe(true); }); it('argument decorated id', () => { @@ -80,14 +79,14 @@ describe('model with one id int', () => { }); it('should have import graphql type id', () => { - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'ID', specifier: '@nestjs/graphql', }); }); it('should have import field decorator', () => { - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'Field', specifier: '@nestjs/graphql', }); @@ -103,38 +102,50 @@ describe('model with one id int', () => { }); it('default value', () => { - const argument = getFieldOptions(sourceFile, 'id'); - expect(argument).toMatch(/nullable:\s*false/); - expect(argument).toMatch(/defaultValue:\s*1/); - expect(argument).not.toMatch(/description:\s*undefined/); + const s = testSourceFile({ + project, + class: 'User', + property: 'id', + }); + expect(s.fieldDecoratorOptions).toMatch(/nullable:\s*false/); + expect(s.fieldDecoratorOptions).toMatch(/defaultValue:\s*1/); + expect(s.fieldDecoratorOptions).not.toMatch(/description:\s*undefined/); }); it('property description', () => { - const argument = getFieldOptions(sourceFile, 'id'); - expect(argument).toMatch(/nullable:\s*false/); - expect(argument).toMatch(/description:\s*["']user id really["']/); + const s = testSourceFile({ + project, + file: 'user.model.ts', + property: 'id', + }); + expect(s.fieldDecoratorOptions).toMatch(/nullable:\s*false/); + expect(s.fieldDecoratorOptions).toMatch( + /description:\s*["']user id really["']/, + ); }); it('property description in jsdoc', () => { - const description = classFile + const description = s.classFile .getProperty('id') ?.getJsDocs()[0] .getDescription(); - expect(description).toEqual('user id really'); + expect(description).toEqual('\nuser id really'); }); it('object type description', () => { - const decoratorArgument = classFile.getDecorators()[0].getStructure() + const decoratorArgument = s.classFile.getDecorators()[0].getStructure() .arguments?.[0] as string | undefined; expect(decoratorArgument).toMatch(/description:\s*["']User really["']/); }); it('has js comment', () => { - expect(classFile.getJsDocs()[0].getDescription()).toEqual('User really'); + expect(s.classFile.getJsDocs()[0].getDescription()).toEqual( + '\nUser really', + ); }); it('has import objecttype', () => { - expect(imports).toContainEqual({ + expect(s.namedImports).toContainEqual({ name: 'ObjectType', specifier: '@nestjs/graphql', }); @@ -2081,9 +2092,11 @@ describe('object model options', () => { options: [`outputFilePattern = "{name}.{type}.ts"`], })); - setSourceFile('user.model.ts'); - const argument = objectTypeArguments()?.[0]; + const s = testSourceFile({ project, class: 'User' }); + const argument = s.classFile.getDecorator('ObjectType')?.getStructure() + .arguments?.[0]; const json = JSON5.parse(argument); + expect(json).toEqual({ isAbstract: true }); }); @@ -2569,25 +2582,95 @@ describe('configuration custom scalars', () => { }); }); -describe('single model and field mongodb', () => { +describe('multiple description', () => { before(async () => { ({ project, sourceFiles } = await testGenerate({ - provider: 'mongodb', schema: ` - model Product { - id String @id @default(auto()) @map("_id") @db.ObjectId - } - `, + /// Model description + /// Model 2 + model User { + id Int @id + /// Name description + /// Second line + name String + }`, options: [`outputFilePattern = "{name}.{type}.ts"`], })); }); - it('example input update type', () => { - const s = testSourceFile({ - project, - file: 'update-one-product.args.ts', + it('field', () => { + const s = testSourceFile({ project, class: 'User' }); + expect(s.sourceText).toContain(` + /** + * Name description + * Second line + */ + `); + }); + + it('model', () => { + const s = testSourceFile({ project, class: 'User' }); + expect(s.sourceText).toContain(`/** + * Model description + * Model 2 + */`); + }); +}); + +describe('deprecation reason', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + schema: ` + model User { + id Int @id + /// Name description + /// @deprecated Use name2 instead + name String + }`, + options: [`outputFilePattern = "{name}.{type}.ts"`], + })); + }); + + it('deprecation reason default', async () => { + const s = testSourceFile({ project, class: 'User', property: 'name' }); + + expect(JSON5.parse(s.fieldDecoratorOptions)).toEqual( + expect.objectContaining({ + deprecationReason: 'Use name2 instead', + description: 'Name description', + }), + ); + }); + + for (const className of [ + 'UserWhereInput', + 'UserCountAggregateInput', + 'UserCountAggregate', + 'UserMaxAggregateInput', + ]) { + it(className, () => { + const s = testSourceFile({ + project, + class: 'UserCountAggregateInput', + property: 'name', + }); + expect(JSON5.parse(s.fieldDecoratorOptions)).toEqual( + expect.objectContaining({ deprecationReason: 'Use name2 instead' }), + ); }); - // data field is missing because of single id field - expect(s.classFile.getProperties()).toHaveLength(1); + } + + it('deprecated decorator in comment', () => { + const s = testSourceFile({ project, class: 'User', property: 'name' }); + const jsdoc = s.classFile.getProperty('name')?.getJsDocs().at(0)?.getText(); + expect(jsdoc).toContain('* Name description'); + expect(jsdoc).toContain('* @deprecated Use name2 instead'); + + expect(JSON5.parse(s.fieldDecoratorOptions)).toEqual( + expect.objectContaining({ + description: 'Name description', + deprecationReason: 'Use name2 instead', + }), + ); }); }); diff --git a/src/test/hide-field.spec.ts b/src/test/hide-field.spec.ts index 929729db..21487190 100644 --- a/src/test/hide-field.spec.ts +++ b/src/test/hide-field.spec.ts @@ -236,14 +236,14 @@ describe('enums are not imported in classes when decorated', () => { })); }); - it('check files', () => { - for (const file of [ - 'user-group-by.output.ts', - 'user-max-aggregate.output.ts', - 'user-min-aggregate.output.ts', - 'user-create-many.input.ts', - 'user.model.ts', - ]) { + for (const file of [ + 'user-group-by.output.ts', + 'user-max-aggregate.output.ts', + 'user-min-aggregate.output.ts', + 'user-create-many.input.ts', + 'user.model.ts', + ]) { + it(`check files ${file}`, () => { const s = testSourceFile({ project, file, @@ -254,11 +254,12 @@ describe('enums are not imported in classes when decorated', () => { name: 'Role', specifier: './role.enum', }); + expect(s.propertyDecorators).toContainEqual( expect.objectContaining({ name: 'HideField' }), ); - } - }); + }); + } }); describe.skip('hide enum', () => { diff --git a/src/test/mongodb.spec.ts b/src/test/mongodb.spec.ts index f642cc29..cbacc718 100644 --- a/src/test/mongodb.spec.ts +++ b/src/test/mongodb.spec.ts @@ -69,3 +69,26 @@ describe('mongodb json', () => { expect(project).toBeTruthy(); }); }); + +describe('single model and field mongodb', () => { + before(async () => { + ({ project, sourceFiles } = await testGenerate({ + provider: 'mongodb', + schema: ` + model Product { + id String @id @default(auto()) @map("_id") @db.ObjectId + } + `, + options: [`outputFilePattern = "{name}.{type}.ts"`], + })); + }); + + it('example input update type', () => { + const s = testSourceFile({ + project, + file: 'update-one-product.args.ts', + }); + // data field is missing because of single id field + expect(s.classFile.getProperties()).toHaveLength(1); + }); +});