ソースを参照

refactor(cli): Improve generated types & refactors

Michael Bromley 1 年間 前
コミット
9d1f5609ad

+ 2 - 1
packages/cli/src/commands/add/add.ts

@@ -48,7 +48,7 @@ export function registerAddCommand(program: Command) {
                 if (!command) {
                     throw new Error(`Could not find command with id "${featureType as string}"`);
                 }
-                const { modifiedSourceFiles } = await command.run();
+                const { modifiedSourceFiles, project } = await command.run();
 
                 if (modifiedSourceFiles.length) {
                     const importsSpinner = spinner();
@@ -57,6 +57,7 @@ export function registerAddCommand(program: Command) {
                     for (const sourceFile of modifiedSourceFiles) {
                         sourceFile.organizeImports();
                     }
+                    await project.save();
                     importsSpinner.stop('Imports organized');
                 }
                 outro('✅ Done!');

+ 38 - 3
packages/cli/src/commands/add/api-extension/add-api-extension.ts

@@ -16,7 +16,11 @@ import { EntityRef } from '../../../shared/entity-ref';
 import { ServiceRef } from '../../../shared/service-ref';
 import { analyzeProject, selectPlugin, selectServiceRef } from '../../../shared/shared-prompts';
 import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
-import { addImportsToFile, createFile } from '../../../utilities/ast-utils';
+import {
+    addImportsToFile,
+    createFile,
+    customizeCreateUpdateInputInterfaces,
+} from '../../../utilities/ast-utils';
 import { pauseForPromptDisplay } from '../../../utilities/utils';
 
 const cancelledMessage = 'Add API extension cancelled';
@@ -153,13 +157,41 @@ function createCrudResolver(
         .rename(serviceEntityRef.name + 'AdminResolver');
 
     if (serviceRef.features.findOne) {
-        resolverClassDeclaration.getMethod('entity')?.rename(serviceEntityRef.nameCamelCase);
+        const findOneMethod = resolverClassDeclaration
+            .getMethod('entity')
+            ?.rename(serviceEntityRef.nameCamelCase);
+        const serviceFindOneMethod = serviceRef.classDeclaration.getMethod('findOne');
+        if (serviceFindOneMethod) {
+            if (
+                !serviceFindOneMethod
+                    .getParameters()
+                    .find(p => p.getName() === 'relations' && p.getType().getText().includes('RelationPaths'))
+            ) {
+                findOneMethod?.getParameters()[2].remove();
+                findOneMethod?.setBodyText(`return this.${serviceRef.nameCamelCase}.findOne(ctx, args.id);`);
+            }
+        }
     } else {
         resolverClassDeclaration.getMethod('entity')?.remove();
     }
 
     if (serviceRef.features.findAll) {
-        resolverClassDeclaration.getMethod('entities')?.rename(serviceEntityRef.nameCamelCase + 's');
+        const findAllMethod = resolverClassDeclaration
+            .getMethod('entities')
+            ?.rename(serviceEntityRef.nameCamelCase + 's');
+        const serviceFindAllMethod = serviceRef.classDeclaration.getMethod('findAll');
+        if (serviceFindAllMethod) {
+            if (
+                !serviceFindAllMethod
+                    .getParameters()
+                    .find(p => p.getName() === 'relations' && p.getType().getText().includes('RelationPaths'))
+            ) {
+                findAllMethod?.getParameters()[2].remove();
+                findAllMethod?.setBodyText(
+                    `return this.${serviceRef.nameCamelCase}.findAll(ctx, args.options || undefined);`,
+                );
+            }
+        }
     } else {
         resolverClassDeclaration.getMethod('entities')?.remove();
     }
@@ -182,6 +214,8 @@ function createCrudResolver(
         resolverClassDeclaration.getMethod('deleteEntity')?.remove();
     }
 
+    customizeCreateUpdateInputInterfaces(resolverSourceFile, serviceEntityRef);
+
     resolverClassDeclaration
         .getConstructors()[0]
         .getParameter('templateService')
@@ -189,6 +223,7 @@ function createCrudResolver(
         .setType(serviceRef.name);
     resolverSourceFile.getClass('TemplateEntity')?.rename(serviceEntityRef.name).remove();
     resolverSourceFile.getClass('TemplateService')?.remove();
+
     addImportsToFile(resolverSourceFile, {
         namedImports: [serviceRef.name],
         moduleSpecifier: serviceRef.classDeclaration.getSourceFile(),

+ 56 - 12
packages/cli/src/commands/add/api-extension/templates/crud-resolver.template.ts

@@ -1,15 +1,32 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { DeletionResponse, Permission } from '@vendure/common/lib/generated-types';
-import { Allow, Ctx, PaginatedList, RequestContext, Transaction, VendureEntity } from '@vendure/core';
+import { CustomFieldsObject } from '@vendure/common/lib/shared-types';
+import {
+    Allow,
+    Ctx,
+    PaginatedList,
+    RequestContext,
+    Transaction,
+    Relations,
+    VendureEntity,
+    ID,
+    TranslationInput,
+    ListQueryOptions,
+    RelationPaths,
+} from '@vendure/core';
 
-class TemplateEntity extends VendureEntity {}
+class TemplateEntity extends VendureEntity {
+    constructor() {
+        super();
+    }
+}
 
 class TemplateService {
-    findAll(ctx: RequestContext, options?: any): Promise<PaginatedList<TemplateEntity>> {
+    findAll(ctx: RequestContext, options?: any, relations?: any): Promise<PaginatedList<TemplateEntity>> {
         throw new Error('Method not implemented.');
     }
 
-    findOne(ctx: RequestContext, id: string): Promise<TemplateEntity | null> {
+    findOne(ctx: RequestContext, id: ID, relations?: any): Promise<TemplateEntity | null> {
         throw new Error('Method not implemented.');
     }
 
@@ -21,45 +38,72 @@ class TemplateService {
         throw new Error('Method not implemented.');
     }
 
-    delete(ctx: RequestContext, id: string): Promise<DeletionResponse> {
+    delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
         throw new Error('Method not implemented.');
     }
 }
 
+// These can be replaced by generated types if you set up code generation
+interface CreateEntityInput {
+    // Define the input fields here
+    customFields?: CustomFieldsObject;
+    translations: Array<TranslationInput<TemplateEntity>>;
+}
+interface UpdateEntityInput {
+    id: ID;
+    // Define the input fields here
+    customFields?: CustomFieldsObject;
+    translations: Array<TranslationInput<TemplateEntity>>;
+}
+
 @Resolver()
 export class EntityAdminResolver {
     constructor(private templateService: TemplateService) {}
 
     @Query()
     @Allow(Permission.SuperAdmin)
-    async entity(@Ctx() ctx: RequestContext, @Args() args: any): Promise<TemplateEntity | null> {
-        return this.templateService.findOne(ctx, args.id);
+    async entity(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { id: ID },
+        @Relations(TemplateEntity) relations: RelationPaths<TemplateEntity>,
+    ): Promise<TemplateEntity | null> {
+        return this.templateService.findOne(ctx, args.id, relations);
     }
 
     @Query()
     @Allow(Permission.SuperAdmin)
-    async entities(@Ctx() ctx: RequestContext, @Args() args: any): Promise<PaginatedList<TemplateEntity>> {
-        return this.templateService.findAll(ctx, args.options || undefined);
+    async entities(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { options: ListQueryOptions<TemplateEntity> },
+        @Relations(TemplateEntity) relations: RelationPaths<TemplateEntity>,
+    ): Promise<PaginatedList<TemplateEntity>> {
+        return this.templateService.findAll(ctx, args.options || undefined, relations);
     }
 
     @Mutation()
     @Transaction()
     @Allow(Permission.SuperAdmin)
-    async createEntity(@Ctx() ctx: RequestContext, @Args() args: any): Promise<TemplateEntity> {
+    async createEntity(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: CreateEntityInput },
+    ): Promise<TemplateEntity> {
         return this.templateService.create(ctx, args.input);
     }
 
     @Mutation()
     @Transaction()
     @Allow(Permission.SuperAdmin)
-    async updateEntity(@Ctx() ctx: RequestContext, @Args() args: any): Promise<TemplateEntity> {
+    async updateEntity(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: UpdateEntityInput },
+    ): Promise<TemplateEntity> {
         return this.templateService.update(ctx, args.input);
     }
 
     @Mutation()
     @Transaction()
     @Allow(Permission.SuperAdmin)
-    async deleteEntity(@Ctx() ctx: RequestContext, @Args() args: any): Promise<DeletionResponse> {
+    async deleteEntity(@Ctx() ctx: RequestContext, @Args() args: { id: ID }): Promise<DeletionResponse> {
         return this.templateService.delete(ctx, args.id);
     }
 }

+ 7 - 2
packages/cli/src/commands/add/entity/add-entity.ts

@@ -1,7 +1,7 @@
 import { cancel, isCancel, multiselect, spinner, text } from '@clack/prompts';
 import { paramCase, pascalCase } from 'change-case';
 import path from 'path';
-import { ClassDeclaration } from 'ts-morph';
+import { ClassDeclaration, SourceFile } from 'ts-morph';
 
 import { pascalCaseRegex } from '../../../constants';
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
@@ -9,6 +9,7 @@ import { EntityRef } from '../../../shared/entity-ref';
 import { analyzeProject, selectPlugin } from '../../../shared/shared-prompts';
 import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
 import { createFile } from '../../../utilities/ast-utils';
+import { pauseForPromptDisplay } from '../../../utilities/utils';
 
 import { addEntityToPlugin } from './codemods/add-entity-to-plugin/add-entity-to-plugin';
 
@@ -38,6 +39,7 @@ async function addEntity(
     const providedVendurePlugin = options?.plugin;
     const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
+    const modifiedSourceFiles: SourceFile[] = [];
 
     const customEntityName = options?.className ?? (await getCustomEntityName(cancelledMessage));
 
@@ -50,11 +52,14 @@ async function addEntity(
 
     const entitySpinner = spinner();
     entitySpinner.start('Creating entity...');
+    await pauseForPromptDisplay();
 
     const { entityClass, translationClass } = createEntity(vendurePlugin, context);
     addEntityToPlugin(vendurePlugin, entityClass);
+    modifiedSourceFiles.push(entityClass.getSourceFile());
     if (context.features.translatable) {
         addEntityToPlugin(vendurePlugin, translationClass);
+        modifiedSourceFiles.push(translationClass.getSourceFile());
     }
 
     entitySpinner.stop('Entity created');
@@ -63,7 +68,7 @@ async function addEntity(
 
     return {
         project,
-        modifiedSourceFiles: [entityClass.getSourceFile()],
+        modifiedSourceFiles,
         entityRef: new EntityRef(entityClass),
     };
 }

+ 6 - 26
packages/cli/src/commands/add/service/add-service.ts

@@ -9,7 +9,11 @@ import { EntityRef } from '../../../shared/entity-ref';
 import { ServiceRef } from '../../../shared/service-ref';
 import { analyzeProject, selectEntity, selectPlugin } from '../../../shared/shared-prompts';
 import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
-import { addImportsToFile, createFile } from '../../../utilities/ast-utils';
+import {
+    addImportsToFile,
+    createFile,
+    customizeCreateUpdateInputInterfaces,
+} from '../../../utilities/ast-utils';
 import { pauseForPromptDisplay } from '../../../utilities/utils';
 import { addEntityCommand } from '../entity/add-entity';
 
@@ -139,7 +143,7 @@ async function addService(
         } else {
             templateTranslationEntityClass?.remove();
         }
-        customizeInputInterfaces(serviceSourceFile, entityRef);
+        customizeCreateUpdateInputInterfaces(serviceSourceFile, entityRef);
         customizeFindOneMethod(serviceClassDeclaration, entityRef);
         customizeFindAllMethod(serviceClassDeclaration, entityRef);
         customizeCreateMethod(serviceClassDeclaration, entityRef);
@@ -295,30 +299,6 @@ function customizeUpdateMethod(serviceClassDeclaration: ClassDeclaration, entity
     }
 }
 
-function customizeInputInterfaces(serviceSourceFile: SourceFile, entityRef: EntityRef) {
-    const createInputInterface = serviceSourceFile
-        .getInterface('CreateEntityInput')
-        ?.rename(`Create${entityRef.name}Input`);
-    const updateInputInterface = serviceSourceFile
-        .getInterface('UpdateEntityInput')
-        ?.rename(`Update${entityRef.name}Input`);
-    if (!entityRef.hasCustomFields()) {
-        createInputInterface?.getProperty('customFields')?.remove();
-        updateInputInterface?.getProperty('customFields')?.remove();
-    }
-    if (entityRef.isTranslatable()) {
-        createInputInterface
-            ?.getProperty('translations')
-            ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
-        updateInputInterface
-            ?.getProperty('translations')
-            ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
-    } else {
-        createInputInterface?.getProperty('translations')?.remove();
-        updateInputInterface?.getProperty('translations')?.remove();
-    }
-}
-
 function removedUnusedConstructorArgs(serviceClassDeclaration: ClassDeclaration, entityRef: EntityRef) {
     const isTranslatable = entityRef.isTranslatable();
     const hasCustomFields = entityRef.hasCustomFields();

+ 4 - 1
packages/cli/src/shared/shared-prompts.ts

@@ -152,7 +152,10 @@ export async function selectServiceRef(project: Project, plugin: VendurePluginRe
 
 export function getServices(project: Project): ServiceRef[] {
     const servicesSourceFiles = project.getSourceFiles().filter(sf => {
-        return sf.getDirectory().getPath().endsWith('/services');
+        return (
+            sf.getDirectory().getPath().endsWith('/services') ||
+            sf.getDirectory().getPath().endsWith('/service')
+        );
     });
 
     return servicesSourceFiles

+ 46 - 0
packages/cli/src/utilities/ast-utils.ts

@@ -4,6 +4,7 @@ import path from 'node:path';
 import { Directory, Node, Project, ProjectOptions, SourceFile } from 'ts-morph';
 
 import { defaultManipulationSettings } from '../constants';
+import { EntityRef } from '../shared/entity-ref';
 
 export function getTsMorphProject(options: ProjectOptions = {}) {
     const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
@@ -117,3 +118,48 @@ function convertPathToRelativeImport(filePath: string): string {
     const parsedPath = path.parse(normalizedPath);
     return `./${parsedPath.dir}/${parsedPath.name}`.replace(/\/\//g, '/');
 }
+
+export function customizeCreateUpdateInputInterfaces(sourceFile: SourceFile, entityRef: EntityRef) {
+    const createInputInterface = sourceFile
+        .getInterface('CreateEntityInput')
+        ?.rename(`Create${entityRef.name}Input`);
+    const updateInputInterface = sourceFile
+        .getInterface('UpdateEntityInput')
+        ?.rename(`Update${entityRef.name}Input`);
+
+    for (const { name, type, nullable } of entityRef.getProps()) {
+        if (
+            type.isBoolean() ||
+            type.isString() ||
+            type.isNumber() ||
+            (type.isClass() && type.getText() === 'Date')
+        ) {
+            createInputInterface?.addProperty({
+                name,
+                type: writer => writer.write(type.getText()),
+                hasQuestionToken: nullable,
+            });
+            updateInputInterface?.addProperty({
+                name,
+                type: writer => writer.write(type.getText()),
+                hasQuestionToken: true,
+            });
+        }
+    }
+
+    if (!entityRef.hasCustomFields()) {
+        createInputInterface?.getProperty('customFields')?.remove();
+        updateInputInterface?.getProperty('customFields')?.remove();
+    }
+    if (entityRef.isTranslatable()) {
+        createInputInterface
+            ?.getProperty('translations')
+            ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
+        updateInputInterface
+            ?.getProperty('translations')
+            ?.setType(`Array<TranslationInput<${entityRef.name}>>`);
+    } else {
+        createInputInterface?.getProperty('translations')?.remove();
+        updateInputInterface?.getProperty('translations')?.remove();
+    }
+}