diff --git a/package-lock.json b/package-lock.json index e5c59e4..2d8b4b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lenne.tech/cli", - "version": "0.0.120", + "version": "0.0.121", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lenne.tech/cli", - "version": "0.0.120", + "version": "0.0.121", "license": "MIT", "dependencies": { "@lenne.tech/cli-plugin-helper": "0.0.12", diff --git a/package.json b/package.json index 9a7691b..462fbd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lenne.tech/cli", - "version": "0.0.120", + "version": "0.0.121", "description": "lenne.Tech CLI: lt", "keywords": [ "lenne.Tech", diff --git a/src/commands/server/add-property.ts b/src/commands/server/add-property.ts index 8dabac2..886d8e4 100644 --- a/src/commands/server/add-property.ts +++ b/src/commands/server/add-property.ts @@ -1,7 +1,7 @@ import { GluegunCommand } from 'gluegun'; import { join } from 'path'; import { - ClassPropertyTypes, + ClassPropertyTypes, IndentationText, OptionalKind, Project, PropertyDeclarationStructure, @@ -138,26 +138,64 @@ const NewCommand: GluegunCommand = { } const type = ['any', 'bigint', 'boolean', 'never', 'null', 'number', 'string', 'symbol', 'undefined', 'unknown', 'void'].includes(propObj.type) ? propObj.type : pascalCase(propObj.type); + + const description = `'${pascalCase(propObj.name)} of ${pascalCase(elementToEdit)}'`; + + const typeString = () => { + switch (true) { + case type === 'Json': + return 'JSON'; + + case !!propObj.enumRef: + return propObj.enumRef; + + case !!propObj.schema: + return propObj.schema; + + case propObj.type === 'ObjectId': + return propObj.reference; + + default: + return pascalCase(type); + } + }; + + // Build @UnifiedField options; types vary and can't go in standardDeclaration + function constructUnifiedFieldOptions(type: 'create' | 'input' | 'model'): string { + switch (type) { + case 'create': + return `{ + description: ${description},${propObj.nullable ? '\nisOptional: true,' : ''} + roles: RoleEnum.ADMIN,${propObj.enumRef ? '' : `\ntype: () => ${typeString()}${propObj.type === 'ObjectId' || propObj.schema ? 'CreateInput' : ''}`} + }`; + case 'input': + return `{ + description: ${description}, + isOptional: true, + roles: RoleEnum.ADMIN, + ${propObj.enumRef ? `enum: { enum: ${propObj.enumRef} }` : `type: () => ${typeString()}${propObj.type === 'ObjectId' || propObj.schema ? 'Input' : ''}`} + }`; + case 'model': + return `{ + description: ${description},${propObj.nullable ? '\nisOptional: true,' : ''} + roles: RoleEnum.ADMIN,${propObj.enumRef ? '' : `\ntype: () => ${typeString()}`} + }`; + } + } + const standardDeclaration: OptionalKind = { - decorators: [ - { arguments: [`() => ${propObj.isArray ? `[${propObj.type - === 'ObjectId' ? propObj.reference : pascalCase(propObj.type)}]` : propObj.type - === 'ObjectId' ? propObj.reference : pascalCase(propObj.type)}`, - `{ description: '${pascalCase(propObj.name)} of ${pascalCase(elementToEdit)}', nullable: ${propObj.nullable} }`], - name: 'Field', - }, - { arguments: ['RoleEnum.ADMIN'], name: 'Restricted' }, - ], + decorators: [], hasQuestionToken: propObj.nullable, initializer: declare ? undefined : 'undefined', name: propObj.name, - type: `${propObj.type === 'ObjectId' ? propObj.reference : type}${propObj.isArray ? '[]' : ''}`, }; // Patch model const lastModelProperty = modelProperties[modelProperties.length - 1]; const newModelProperty: OptionalKind = structuredClone(standardDeclaration); - newModelProperty.decorators.push({ arguments: [`${propObj.type === 'ObjectId' ? `{ ref: ${propObj.reference}, type: Schema.Types.ObjectId }` : ''}`], name: 'Prop' }); + newModelProperty.decorators.push({ arguments: [`${propObj.type === 'ObjectId' || propObj.schema ? `{ ref: () => ${propObj.reference}, type: Schema.Types.ObjectId }` : ''}`], name: 'Prop' }); + newModelProperty.decorators.push({ arguments: [constructUnifiedFieldOptions('model')], name: 'UnifiedField' }); + newModelProperty.type = `${typeString()}${propObj.isArray ? '[]' : ''}`; const insertedModelProp = modelDeclaration.insertProperty(lastModelProperty.getChildIndex() + 1, newModelProperty); insertedModelProp.prependWhitespace('\n'); insertedModelProp.appendWhitespace('\n'); @@ -165,26 +203,38 @@ const NewCommand: GluegunCommand = { // Patch input const lastInputProperty = inputProperties[inputProperties.length - 1]; const newInputProperty: OptionalKind = structuredClone(standardDeclaration); - if (propObj.nullable) { - newInputProperty.decorators.push({ arguments: [], name: 'IsOptional' }); - } + newInputProperty.decorators.push({ arguments: [constructUnifiedFieldOptions('input')], name: 'UnifiedField' }); + const inputSuffix = propObj.type === 'ObjectId' || propObj.schema ? 'Input' : ''; + newInputProperty.type = `${typeString()}${inputSuffix}${propObj.isArray ? '[]' : ''}`; const insertedInputProp = inputDeclaration.insertProperty(lastInputProperty.getChildIndex() + 1, newInputProperty); insertedInputProp.prependWhitespace('\n'); insertedInputProp.appendWhitespace('\n'); // Patch create input const lastCreateInputProperty = createInputProperties[createInputProperties.length - 1]; - const newCreateInputProperty: OptionalKind = structuredClone(newInputProperty); + const newCreateInputProperty: OptionalKind = structuredClone(standardDeclaration); if (declare) { newCreateInputProperty.hasDeclareKeyword = true; } else { newCreateInputProperty.hasOverrideKeyword = true; } + newCreateInputProperty.decorators.push({ arguments: [constructUnifiedFieldOptions('create')], name: 'UnifiedField' }); + const createSuffix = propObj.type === 'ObjectId' || propObj.schema ? 'CreateInput' : ''; + newCreateInputProperty.type = `${typeString()}${createSuffix}${propObj.isArray ? '[]' : ''}`; const insertedCreateInputProp = createInputDeclaration.insertProperty(lastCreateInputProperty.getChildIndex() + 1, newCreateInputProperty); insertedCreateInputProp.prependWhitespace('\n'); insertedCreateInputProp.appendWhitespace('\n'); } + project.manipulationSettings.set({ + indentationText: IndentationText.TwoSpaces, + }); + + // Format files + moduleFile.formatText(); + inputFile.formatText(); + createInputFile.formatText(); + // Save files await moduleFile.save(); await inputFile.save(); diff --git a/src/commands/server/module.ts b/src/commands/server/module.ts index 4616865..cd7eba3 100644 --- a/src/commands/server/module.ts +++ b/src/commands/server/module.ts @@ -116,14 +116,14 @@ const NewCommand: ExtendedGluegunCommand = { // nest-server-module/inputs/xxx-create.input.ts await template.generate({ - props: { imports: createTemplate.imports, nameCamel, nameKebab, namePascal, props: createTemplate.props }, + props: { imports: createTemplate.imports, isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal, props: createTemplate.props }, target: join(directory, 'inputs', `${nameKebab}-create.input.ts`), template: 'nest-server-module/inputs/template-create.input.ts.ejs', }); // nest-server-module/output/find-and-count-xxxs-result.output.ts await template.generate({ - props: { nameCamel, nameKebab, namePascal }, + props: { isGql: controller === 'GraphQL' || controller === 'Both', nameCamel, nameKebab, namePascal }, target: join(directory, 'outputs', `find-and-count-${nameKebab}s-result.output.ts`), template: 'nest-server-module/outputs/template-fac-result.output.ts.ejs', }); @@ -132,6 +132,7 @@ const NewCommand: ExtendedGluegunCommand = { await template.generate({ props: { imports: modelTemplate.imports, + isGql: controller === 'GraphQL' || controller === 'Both', mappings: modelTemplate.mappings, nameCamel, nameKebab, diff --git a/src/extensions/server.ts b/src/extensions/server.ts index c56729b..8889d94 100644 --- a/src/extensions/server.ts +++ b/src/extensions/server.ts @@ -304,9 +304,10 @@ export class Server { /** * User who has tested the ${this.pascalCase(modelName)} */ - @Field(() => User, { + @UnifiedField({ description: 'User who has tested the ${this.pascalCase(modelName)}', - nullable: true, + isOptional: true, + type: () => User, }) @Prop({ type: Schema.Types.ObjectId, ref: 'User' }) testedBy: User${undefinedString} @@ -344,11 +345,6 @@ export class Server { /** * ${this.pascalCase(propName) + (modelName ? ` of ${this.pascalCase(modelName)}` : '')} */ - @Restricted(RoleEnum.S_EVERYONE) - @Field(() => ${(isArray ? '[' : '') + (reference ? reference : modelFieldType) + (isArray ? ']' : '')}, { - description: '${this.pascalCase(propName) + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}', - nullable: ${item.nullable}, - }) @Prop(${ reference ? `${isArray ? '[' : ''}{ ref: '${reference}', type: Schema.Types.ObjectId }${isArray ? ']' : ''}` @@ -360,10 +356,13 @@ export class Server { ? `${isArray ? '[' : ''}{ type: Object }${isArray ? ']' : ''}` : '' }) - ${propName}: ${ - (reference ? reference : enumRef || modelClassType) + (isArray ? '[]' : '') - // (reference ? ' | ' + reference + (isArray ? '[]' : '') : '') - }${undefinedString} + @UnifiedField({ + description: '${this.pascalCase(propName) + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}', + isOptional: ${item.nullable}, + roles: RoleEnum.S_EVERYONE, + type: () => ${reference ? reference : modelFieldType}, + }) + ${propName}: ${(reference ? reference : enumRef || modelClassType) + (isArray ? '[]' : '')}${undefinedString} `; } @@ -422,11 +421,11 @@ export class Server { properties: string[]${undefinedString} /** - * User how has tested the ${this.pascalCase(modelName)} + * User who has tested the ${this.pascalCase(modelName)} */ - @Field(() => User, { + @UnifiedField({ description: 'User who has tested the ${this.pascalCase(modelName)}', - nullable: ${config.nullable}, + isOptional: ${config.nullable}, }) testedBy: User${undefinedString} `, @@ -464,14 +463,13 @@ export class Server { /** * ${this.pascalCase(name) + propertySuffix + (modelName ? ` of ${this.pascalCase(modelName)}` : '')} */ - @Restricted(RoleEnum.S_EVERYONE) - @Field(() => ${(item.isArray ? '[' : '') + inputFieldType + (item.isArray ? ']' : '')}, { + @UnifiedField({ description: '${this.pascalCase(name) + propertySuffix + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}', - nullable: ${nullable || item.nullable}, - })${nullable || item.nullable ? '\n @IsOptional()' : ''} - ${overrideFlag + this.camelCase(name)}${nullable || item.nullable ? '?' : ''}: ${ - inputClassType + (item.isArray ? '[]' : '') - }${undefinedString} + isOptional: ${nullable}, + roles: RoleEnum.S_EVERYONE, + type: () => ${inputFieldType} + }) + ${overrideFlag + this.camelCase(name)}${nullable || item.nullable ? '?' : ''}: ${inputClassType}${item.isArray ? '[]' : ''}${undefinedString} `; } diff --git a/src/templates/nest-server-module/inputs/template-create.input.ts.ejs b/src/templates/nest-server-module/inputs/template-create.input.ts.ejs index 9f2119b..e35e36f 100644 --- a/src/templates/nest-server-module/inputs/template-create.input.ts.ejs +++ b/src/templates/nest-server-module/inputs/template-create.input.ts.ejs @@ -1,6 +1,5 @@ -import { Restricted, RoleEnum } from '@lenne.tech/nest-server'; -import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional } from 'class-validator';<%- props.imports %> +import { Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; +import { InputType } from '@nestjs/graphql'; import { <%= props.namePascal %>Input } from './<%= props.nameKebab %>.input'; diff --git a/src/templates/nest-server-module/inputs/template.input.ts.ejs b/src/templates/nest-server-module/inputs/template.input.ts.ejs index 9b80c8b..b132f4f 100644 --- a/src/templates/nest-server-module/inputs/template.input.ts.ejs +++ b/src/templates/nest-server-module/inputs/template.input.ts.ejs @@ -1,6 +1,5 @@ -import { CoreInput, Restricted, RoleEnum } from '@lenne.tech/nest-server'; -import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional } from 'class-validator';<%- props.imports %> +import { CoreInput, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; +import { InputType } from '@nestjs/graphql'; /** * <%= props.namePascal %> input diff --git a/src/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs b/src/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs index 801f6b5..83b4038 100644 --- a/src/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +++ b/src/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs @@ -1,12 +1,22 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { UnifiedField } from '@lenne.tech/nest-server'; +<% if (props.isGql) { %> + import { ObjectType } from '@nestjs/graphql'; +<% } %> import { <%= props.namePascal %> } from '../<%= props.nameKebab %>.model'; - +<% if (props.isGql) { %> @ObjectType({ description: 'Result of find and count <%= props.namePascal %>s' }) +<% } %> export class FindAndCount<%= props.namePascal %>sResult { - @Field(() => [<%= props.namePascal %>], { description: 'Found <%= props.namePascal %>s' }) + + @UnifiedField({ + type: () => <%= props.namePascal %>, + description: 'Found <%= props.namePascal %>s', + }) items: <%= props.namePascal %>[]; - @Field({ description: 'Total count (skip/offset and limit/take are ignored in the count)' }) + @UnifiedField({ + description: 'Total count (skip/offset and limit/take are ignored in the count)', + }) totalCount: number; } diff --git a/src/templates/nest-server-module/template.controller.ts.ejs b/src/templates/nest-server-module/template.controller.ts.ejs index c116657..06a6509 100644 --- a/src/templates/nest-server-module/template.controller.ts.ejs +++ b/src/templates/nest-server-module/template.controller.ts.ejs @@ -1,9 +1,11 @@ import { ApiCommonErrorResponses, FilterArgs, RoleEnum, Roles } from '@lenne.tech/nest-server'; import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { ApiOkResponse } from '@nestjs/swagger'; import { <%= props.namePascal %>Service } from './<%= props.nameKebab %>.service'; import { <%= props.namePascal %>Input } from './inputs/<%= props.nameKebab %>.input'; import { <%= props.namePascal %>CreateInput } from './inputs/<%= props.nameKebab %>-create.input'; +import { <%= props.namePascal %> } from './<%= props.nameKebab %>.model'; @ApiCommonErrorResponses() @Controller('<%= props.lowercase %>') @@ -14,31 +16,36 @@ constructor(protected readonly <%= props.nameCamel %>Service: <%= props.namePasc @Post() @Roles(RoleEnum.ADMIN) - async create(@Body() input: <%= props.namePascal %>CreateInput): Promise { + @ApiOkResponse({ type: <%= props.namePascal %> }) + async create(@Body() input: <%= props.namePascal %>CreateInput): Promise<<%= props.namePascal %>> { return await this.<%= props.nameCamel %>Service.create(input); } @Get() @Roles(RoleEnum.ADMIN) - async get(@Body() filterArgs: FilterArgs): Promise { + @ApiOkResponse({ isArray: true, type: <%= props.namePascal %> }) + async get(@Body() filterArgs: FilterArgs): Promise<<%= props.namePascal %>[]> { return await this.<%= props.nameCamel %>Service.find(filterArgs); } @Get(':id') @Roles(RoleEnum.ADMIN) - async getById(@Param('id') id: string): Promise { + @ApiOkResponse({ type: <%= props.namePascal %> }) + async getById(@Param('id') id: string): Promise<<%= props.namePascal %>> { return await this.<%= props.nameCamel %>Service.findOne({filterQuery: { _id: id }}) } @Put(':id') @Roles(RoleEnum.ADMIN) - async update(@Param('id') id: string, @Body() input: <%= props.namePascal %>Input): Promise { + @ApiOkResponse({ type: <%= props.namePascal %> }) + async update(@Param('id') id: string, @Body() input: <%= props.namePascal %>Input): Promise<<%= props.namePascal %>> { return await this.<%= props.nameCamel %>Service.update(id, input); } @Delete(':id') @Roles(RoleEnum.ADMIN) - async delete(@Param('id') id: string): Promise { + @ApiOkResponse({ type: <%= props.namePascal %> }) + async delete(@Param('id') id: string): Promise<<%= props.namePascal %>> { return await this.<%= props.nameCamel %>Service.delete(id); } diff --git a/src/templates/nest-server-module/template.model.ts.ejs b/src/templates/nest-server-module/template.model.ts.ejs index aa4cad3..8ecc7db 100644 --- a/src/templates/nest-server-module/template.model.ts.ejs +++ b/src/templates/nest-server-module/template.model.ts.ejs @@ -1,5 +1,7 @@ -import { Restricted, RoleEnum, equalIds, mapClasses } from '@lenne.tech/nest-server'; -import { Field, ObjectType } from '@nestjs/graphql'; +import { Restricted, RoleEnum, equalIds, mapClasses , UnifiedField} from '@lenne.tech/nest-server'; +<% if (props.isGql) { %> +import { ObjectType } from '@nestjs/graphql'; +<% } %> import { Schema as MongooseSchema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document, Schema } from 'mongoose';<%- props.imports %> @@ -12,7 +14,7 @@ export type <%= props.namePascal %>Document = <%= props.namePascal %> & Document * <%= props.namePascal %> model */ @Restricted(RoleEnum.ADMIN) -@ObjectType({ description: '<%= props.namePascal %>' }) +<% if (props.isGql) { %> @ObjectType({ description: '<%= props.namePascal %>' }) <% } %> @MongooseSchema({ timestamps: true }) export class <%= props.namePascal %> extends PersistenceModel { diff --git a/src/templates/nest-server-module/template.module.ts.ejs b/src/templates/nest-server-module/template.module.ts.ejs index 57d4582..e68d8a8 100644 --- a/src/templates/nest-server-module/template.module.ts.ejs +++ b/src/templates/nest-server-module/template.module.ts.ejs @@ -10,7 +10,7 @@ import { <%= props.namePascal %>Resolver } from './<%= props.nameKebab %>.resolv <% } -%> import { <%= props.namePascal %>Service } from './<%= props.nameKebab %>.service'; <% if ((props.controller === 'Rest') || (props.controller === 'Both')) { -%> - import { <%= props.namePascal %>Controller } from './<%= props.nameKebab %>.controller'; +import { <%= props.namePascal %>Controller } from './<%= props.nameKebab %>.controller'; <% } -%> /** diff --git a/src/templates/nest-server-object/template-create.input.ts.ejs b/src/templates/nest-server-object/template-create.input.ts.ejs index bb7522a..a477b4b 100644 --- a/src/templates/nest-server-object/template-create.input.ts.ejs +++ b/src/templates/nest-server-object/template-create.input.ts.ejs @@ -1,9 +1,7 @@ -import { Restricted, RoleEnum } from '@lenne.tech/nest-server'; -import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional } from 'class-validator'; +import { Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; +import { InputType } from '@nestjs/graphql'; import { <%= props.namePascal %>Input } from './<%= props.nameKebab %>.input';<%- props.imports %> - /** * <%= props.namePascal %> create input */ diff --git a/src/templates/nest-server-object/template.input.ts.ejs b/src/templates/nest-server-object/template.input.ts.ejs index 9b80c8b..1db91e4 100644 --- a/src/templates/nest-server-object/template.input.ts.ejs +++ b/src/templates/nest-server-object/template.input.ts.ejs @@ -1,6 +1,5 @@ -import { CoreInput, Restricted, RoleEnum } from '@lenne.tech/nest-server'; -import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional } from 'class-validator';<%- props.imports %> +import { CoreInput, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; +import { InputType } from '@nestjs/graphql';<%- props.imports %> /** * <%= props.namePascal %> input diff --git a/src/templates/nest-server-object/template.object.ts.ejs b/src/templates/nest-server-object/template.object.ts.ejs index 75f3f98..380d3fe 100644 --- a/src/templates/nest-server-object/template.object.ts.ejs +++ b/src/templates/nest-server-object/template.object.ts.ejs @@ -1,7 +1,7 @@ -import { CoreModel, mapClasses, Restricted, RoleEnum } from '@lenne.tech/nest-server'; -import { Field, ObjectType } from '@nestjs/graphql'; +import { CoreModel, Restricted, RoleEnum, UnifiedField } from '@lenne.tech/nest-server'; +import { ObjectType } from '@nestjs/graphql'; import { Schema as MongooseSchema, Prop, SchemaFactory } from '@nestjs/mongoose'; -import { Document, Schema } from 'mongoose';<%- props.imports %> +import { Document } from 'mongoose';<%- props.imports %> import { User } from '../../../modules/user/user.model'; @@ -40,6 +40,7 @@ export class <%= props.namePascal %> extends CoreModel { */ override map(input) { super.map(input); + // return mapClasses(input, { propertyName: PropertyModel }, this); return <%- props.mappings %> }