Skip to content

Commit

Permalink
fix(cli): Various fixes to CLI add commands
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 8, 2024
1 parent f869a17 commit 4ea7711
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 72 deletions.
5 changes: 4 additions & 1 deletion packages/cli/src/commands/add/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cancel, intro, isCancel, log, outro, select, spinner } from '@clack/pro
import { Command } from 'commander';
import pc from 'picocolors';

import { Messages } from '../../constants';
import { CliCommand } from '../../shared/cli-command';
import { pauseForPromptDisplay } from '../../utilities/utils';

Expand Down Expand Up @@ -63,9 +64,11 @@ export function registerAddCommand(program: Command) {
outro('✅ Done!');
} catch (e: any) {
log.error(e.message as string);
if (e.stack) {
const isCliMessage = Object.values(Messages).includes(e.message);
if (!isCliMessage && e.stack) {
log.error(e.stack);
}
outro('❌ Error');
}
process.exit(0);
});
Expand Down
254 changes: 202 additions & 52 deletions packages/cli/src/commands/add/api-extension/add-api-extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { spinner } from '@clack/prompts';
import { cancel, isCancel, log, spinner, text } from '@clack/prompts';
import { paramCase } from 'change-case';
import path from 'path';
import {
ClassDeclaration,
CodeBlockWriter,
Expression,
Node,
Project,
SourceFile,
SyntaxKind,
Type,
VariableDeclaration,
VariableDeclarationKind,
Expand Down Expand Up @@ -42,22 +45,73 @@ async function addApiExtension(
const providedVendurePlugin = options?.plugin;
const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
const plugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
const serviceRef = await selectServiceRef(project, plugin);

const serviceRef = await selectServiceRef(project, plugin, false);
const serviceEntityRef = serviceRef.crudEntityRef;
const modifiedSourceFiles: SourceFile[] = [];
let resolver: ClassDeclaration | undefined;
let apiExtensions: VariableDeclaration | undefined;

const scaffoldSpinner = spinner();

let queryName = '';
let mutationName = '';
if (!serviceEntityRef) {
const queryNameResult = await text({
message: 'Enter a name for the new query',
initialValue: 'myNewQuery',
});
if (!isCancel(queryNameResult)) {
queryName = queryNameResult;
}
const mutationNameResult = await text({
message: 'Enter a name for the new mutation',
initialValue: 'myNewMutation',
});
if (!isCancel(mutationNameResult)) {
mutationName = mutationNameResult;
}
}

scaffoldSpinner.start('Generating resolver file...');
await pauseForPromptDisplay();
if (serviceEntityRef) {
resolver = createCrudResolver(project, plugin, serviceRef, serviceEntityRef);
modifiedSourceFiles.push(resolver.getSourceFile());
} else {
resolver = createSimpleResolver(project, plugin, serviceRef);
if (isCancel(queryName)) {
cancel(cancelledMessage);
process.exit(0);
}
resolver = createSimpleResolver(project, plugin, serviceRef, queryName, mutationName);
if (queryName) {
serviceRef.classDeclaration.addMethod({
name: queryName,
parameters: [
{ name: 'ctx', type: 'RequestContext' },
{ name: 'id', type: 'ID' },
],
isAsync: true,
returnType: 'Promise<boolean>',
statements: `return true;`,
});
}
if (mutationName) {
serviceRef.classDeclaration.addMethod({
name: mutationName,
parameters: [
{ name: 'ctx', type: 'RequestContext' },
{ name: 'id', type: 'ID' },
],
isAsync: true,
returnType: 'Promise<boolean>',
statements: `return true;`,
});
}

addImportsToFile(serviceRef.classDeclaration.getSourceFile(), {
namedImports: ['RequestContext', 'ID'],
moduleSpecifier: '@vendure/core',
});
modifiedSourceFiles.push(resolver.getSourceFile());
}

Expand All @@ -67,7 +121,7 @@ async function addApiExtension(
if (serviceEntityRef) {
apiExtensions = createCrudApiExtension(project, plugin, serviceRef);
} else {
apiExtensions = createSimpleApiExtension(project, plugin, serviceRef);
apiExtensions = createSimpleApiExtension(project, plugin, serviceRef, queryName, mutationName);
}
if (apiExtensions) {
modifiedSourceFiles.push(apiExtensions.getSourceFile());
Expand Down Expand Up @@ -102,22 +156,70 @@ async function addApiExtension(
};
}

function createSimpleResolver(project: Project, plugin: VendurePluginRef, serviceRef: ServiceRef) {
function getResolverFileName(
project: Project,
serviceRef: ServiceRef,
): { resolverFileName: string; suffix: number | undefined } {
let suffix: number | undefined;
let resolverFileName = '';
let sourceFileExists = false;
do {
resolverFileName =
paramCase(serviceRef.name).replace('-service', '') +
`-admin.resolver${typeof suffix === 'number' ? `-${suffix?.toString()}` : ''}.ts`;
sourceFileExists = !!project.getSourceFile(resolverFileName);
if (sourceFileExists) {
suffix = (suffix ?? 1) + 1;
}
} while (sourceFileExists);
return { resolverFileName, suffix };
}

function createSimpleResolver(
project: Project,
plugin: VendurePluginRef,
serviceRef: ServiceRef,
queryName: string,
mutationName: string,
) {
const { resolverFileName, suffix } = getResolverFileName(project, serviceRef);
const resolverSourceFile = createFile(
project,
path.join(__dirname, 'templates/simple-resolver.template.ts'),
);
resolverSourceFile.move(
path.join(
plugin.getPluginDir().getPath(),
'api',
paramCase(serviceRef.name).replace('-service', '') + '-admin.resolver.ts',
),
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
resolverSourceFile.move(path.join(plugin.getPluginDir().getPath(), 'api', resolverFileName));

const resolverClassDeclaration = resolverSourceFile
.getClass('SimpleAdminResolver')!
.rename(serviceRef.name.replace(/Service$/, '') + 'AdminResolver');
.getClasses()
.find(cl => cl.getDecorator('Resolver') != null);

if (!resolverClassDeclaration) {
throw new Error('Could not find resolver class declaration');
}
if (resolverClassDeclaration.getName() === 'SimpleAdminResolver') {
resolverClassDeclaration.rename(
serviceRef.name.replace(/Service$/, '') + 'AdminResolver' + (suffix ? suffix.toString() : ''),
);
}

if (queryName) {
resolverSourceFile.getClass('TemplateService')?.getMethod('exampleQueryHandler')?.rename(queryName);
resolverClassDeclaration.getMethod('exampleQuery')?.rename(queryName);
} else {
resolverSourceFile.getClass('TemplateService')?.getMethod('exampleQueryHandler')?.remove();
resolverClassDeclaration.getMethod('exampleQuery')?.remove();
}

if (mutationName) {
resolverSourceFile
.getClass('TemplateService')
?.getMethod('exampleMutationHandler')
?.rename(mutationName);
resolverClassDeclaration.getMethod('exampleMutation')?.rename(mutationName);
} else {
resolverSourceFile.getClass('TemplateService')?.getMethod('exampleMutationHandler')?.remove();
resolverClassDeclaration.getMethod('exampleMutation')?.remove();
}

resolverClassDeclaration
.getConstructors()[0]
Expand Down Expand Up @@ -236,34 +338,70 @@ function createCrudResolver(
return resolverClassDeclaration;
}

function createSimpleApiExtension(project: Project, plugin: VendurePluginRef, serviceRef: ServiceRef) {
function createSimpleApiExtension(
project: Project,
plugin: VendurePluginRef,
serviceRef: ServiceRef,
queryName: string,
mutationName: string,
) {
const apiExtensionsFile = getOrCreateApiExtensionsFile(project, plugin);
const adminApiExtensionDocuments = apiExtensionsFile.getVariableDeclaration('adminApiExtensionDocuments');
const insertAtIndex = adminApiExtensionDocuments?.getParent().getParent().getChildIndex() ?? 2;
const adminApiExtensions = apiExtensionsFile.getVariableDeclaration('adminApiExtensions');
const insertAtIndex = adminApiExtensions?.getParent().getParent().getChildIndex() ?? 2;
const schemaVariableName = `${serviceRef.nameCamelCase.replace(/Service$/, '')}AdminApiExtensions`;
apiExtensionsFile.insertVariableStatement(insertAtIndex, {
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: schemaVariableName,
initializer: writer => {
writer.writeLine(`gql\``);
writer.indent(() => {
const existingSchemaVariable = apiExtensionsFile.getVariableStatement(schemaVariableName);
if (!existingSchemaVariable) {
apiExtensionsFile.insertVariableStatement(insertAtIndex, {
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: schemaVariableName,
initializer: writer => {
writer.writeLine(`gql\``);
writer.indent(() => {
if (queryName) {
writer.writeLine(` extend type Query {`);
writer.writeLine(` ${queryName}(id: ID!): Boolean!`);
writer.writeLine(` }`);
}
writer.newLine();
if (mutationName) {
writer.writeLine(` extend type Mutation {`);
writer.writeLine(` ${mutationName}(id: ID!): Boolean!`);
writer.writeLine(` }`);
}
});
writer.write(`\``);
},
},
],
});
} else {
const taggedTemplateLiteral = existingSchemaVariable
.getDeclarations()[0]
?.getFirstChildByKind(SyntaxKind.TaggedTemplateExpression)
?.getChildren()[1];
if (!taggedTemplateLiteral) {
log.error('Could not update schema automatically');
} else {
appendToGqlTemplateLiteral(existingSchemaVariable.getDeclarations()[0], writer => {
writer.indent(() => {
if (queryName) {
writer.writeLine(` extend type Query {`);
writer.writeLine(` exampleQuery(id: ID!): Boolean!`);
writer.writeLine(` ${queryName}(id: ID!): Boolean!`);
writer.writeLine(` }`);
writer.newLine();
}
writer.newLine();
if (mutationName) {
writer.writeLine(` extend type Mutation {`);
writer.writeLine(` exampleMutation(id: ID!): Boolean!`);
writer.writeLine(` ${mutationName}(id: ID!): Boolean!`);
writer.writeLine(` }`);
});
writer.write(`\``);
},
},
],
});
}
});
});
}
}

const adminApiExtensions = apiExtensionsFile.getVariableDeclaration('adminApiExtensions');
addSchemaToApiExtensionsTemplateLiteral(adminApiExtensions, schemaVariableName);

return adminApiExtensions;
Expand Down Expand Up @@ -388,22 +526,34 @@ function addSchemaToApiExtensionsTemplateLiteral(
schemaVariableName: string,
) {
if (adminApiExtensions) {
const apiExtensionsInitializer = adminApiExtensions.getInitializer();
if (Node.isTaggedTemplateExpression(apiExtensionsInitializer)) {
adminApiExtensions
.setInitializer(writer => {
writer.writeLine(`gql\``);
const template = apiExtensionsInitializer.getTemplate();
if (Node.isNoSubstitutionTemplateLiteral(template)) {
writer.write(`${template.getLiteralValue()}`);
} else {
writer.write(template.getText().replace(/^`/, '').replace(/`$/, ''));
}
writer.writeLine(` \${${schemaVariableName}}`);
writer.write(`\``);
})
.formatText();
if (adminApiExtensions.getText().includes(` \${${schemaVariableName}}`)) {
return;
}
appendToGqlTemplateLiteral(adminApiExtensions, writer => {
writer.writeLine(` \${${schemaVariableName}}`);
});
}
}

function appendToGqlTemplateLiteral(
variableDeclaration: VariableDeclaration,
append: (writer: CodeBlockWriter) => void,
) {
const initializer = variableDeclaration.getInitializer();
if (Node.isTaggedTemplateExpression(initializer)) {
variableDeclaration
.setInitializer(writer => {
writer.write(`gql\``);
const template = initializer.getTemplate();
if (Node.isNoSubstitutionTemplateLiteral(template)) {
writer.write(`${template.getLiteralValue()}`);
} else {
writer.write(template.getText().replace(/^`/, '').replace(/`$/, ''));
}
append(writer);
writer.write(`\``);
})
.formatText();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Permission } from '@vendure/common/lib/generated-types';
import { Allow, Ctx, RequestContext } from '@vendure/core';
import { ID } from '@vendure/common/lib/shared-types';
import { Allow, Ctx, RequestContext, Transaction } from '@vendure/core';

class TemplateService {}
class TemplateService {
async exampleQueryHandler(ctx: RequestContext, id: ID) {
return true;
}
async exampleMutationHandler(ctx: RequestContext, id: ID) {
return true;
}
}

@Resolver()
export class SimpleAdminResolver {
constructor(private templateService: TemplateService) {}

@Query()
@Allow(Permission.SuperAdmin)
async exampleQuery(@Ctx() ctx: RequestContext, @Args() args: { id: string }): Promise<boolean> {
return true;
async exampleQuery(@Ctx() ctx: RequestContext, @Args() args: { id: ID }): Promise<boolean> {
return this.templateService.exampleQueryHandler(ctx, args.id);
}

@Mutation()
@Transaction()
@Allow(Permission.SuperAdmin)
async exampleMutation(@Ctx() ctx: RequestContext, @Args() args: { id: string }): Promise<boolean> {
return true;
async exampleMutation(@Ctx() ctx: RequestContext, @Args() args: { id: ID }): Promise<boolean> {
return this.templateService.exampleMutationHandler(ctx, args.id);
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/commands/add/plugin/create-new-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
if (!options.name) {
const name = await text({
message: 'What is the name of the plugin?',
initialValue: '',
initialValue: 'my-new-feature',
validate: input => {
if (!/^[a-z][a-z-0-9]+$/.test(input)) {
return 'The plugin name must be lowercase and contain only letters, numbers and dashes';
Expand Down
Loading

0 comments on commit 4ea7711

Please sign in to comment.