Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

feat: create code snippets for api (#342) #396

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-simple-import-sort": "7.0.0",
"eslint-plugin-testing-library": "4.12.2",
"inquirer-select-directory": "1.2.0",
"jest": "27.2.0",
"jest-transform-stub": "2.0.0",
"plop": "2.7.4",
"prettier": "2.4.1",
"rimraf": "3.0.2",
"serve": "12.0.1",
Expand Down
30 changes: 30 additions & 0 deletions plopfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/no-default-export */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import inquirerSelectDirectory from 'inquirer-select-directory';
import { NodePlopAPI } from 'plop';

import {
commandGenerator,
commandHandlerGenerator,
domainFunctionGenerator,
eventGenerator,
moduleGenerator,
restControllerGenerator,
} from './templates';
import { moduleNameFromPath } from './templates/helpers/moduleNameFromPath';

export default function (plop: NodePlopAPI) {
// Plugins
plop.setPrompt('directory', inquirerSelectDirectory);
// Helpers
plop.setHelper('moduleName', moduleNameFromPath);
// Generators
plop.setGenerator('command', commandGenerator);
plop.setGenerator('commandHandler', commandHandlerGenerator);
plop.setGenerator('moduleGenerator', moduleGenerator);
plop.setGenerator('event', eventGenerator);
plop.setGenerator('domain-function', domainFunctionGenerator);
plop.setGenerator('restController', restControllerGenerator);
}
5 changes: 5 additions & 0 deletions templates/command/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const createCommandAction = {
type: 'add',
path: 'packages/api/src/module/shared/commands/{{dashCase command}}.ts',
templateFile: './templates/command/command.hbs',
};
13 changes: 13 additions & 0 deletions templates/command/command.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AbstractApplicationCommand } from '@/module/application-command-events';

export type {{properCase command}} = {
type: '{{properCase command}}';
data: {};
};

export const {{camelCase command}} = (data: {{properCase command}}['data']): {{properCase command}} => ({
type: '{{properCase command}}',
data,
});
kacper-cyra marked this conversation as resolved.
Show resolved Hide resolved

export class {{properCase command}}ApplicationCommand extends AbstractApplicationCommand<{{properCase command}}> {}
8 changes: 8 additions & 0 deletions templates/command/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createCommandAction } from './actions';
import { commandNamePrompt } from './prompts';

export const commandGenerator = {
description: 'Create a new command',
prompts: [commandNamePrompt],
actions: [createCommandAction],
};
5 changes: 5 additions & 0 deletions templates/command/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const commandNamePrompt = {
type: 'input',
name: 'command',
message: 'command name',
};
5 changes: 5 additions & 0 deletions templates/commandHandler/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const createCommandHandlerAction = {
type: 'add',
path: '{{directory}}/application/{{dashCase command}}.command-handler.ts',
templateFile: './templates/commandHandler/commandHandler.hbs',
};
30 changes: 30 additions & 0 deletions templates/commandHandler/commandHandler.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

import { {{properCase command}}ApplicationCommand } from '@/commands/{{dashCase command}}';
import { {{properCase event}} } from '@/module/events/some-event';
import { APPLICATION_SERVICE, ApplicationService } from '@/write/shared/application/application-service';
import { EventStreamName } from '@/write/shared/application/event-stream-name.value-object';

import { {{camelCase domainFunction}} } from '../domain/{{dashCase domainFunction}}';

@CommandHandler({{properCase command}}ApplicationCommand)
export class {{properCase command}}ApplicationCommandHandler implements ICommandHandler<{{properCase command}}ApplicationCommand> {
constructor(
@Inject(APPLICATION_SERVICE)
private readonly applicationService: ApplicationService,
) {}

async execute(command: {{properCase command}}ApplicationCommand): Promise<void> {
const eventStream = EventStreamName.from('{{properCase streamCategory}}', ``);

await this.applicationService.execute<{{properCase event}}>(
eventStream,
{
causationId: command.id,
correlationId: command.metadata.correlationId,
},
(pastEvents) => {{camelCase domainFunction}}(pastEvents, command),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you change snippet like I suggested below, then you can change that snippet to this:

Suggested change
(pastEvents) => {{camelCase domainFunction}}(pastEvents, command),
);
{{camelCase domainFunction}}(command),
);

Copy link
Contributor

@hkawalek hkawalek Oct 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, we can go even further and change applicationService.execute interface to sth like this.

this.applicationService.execute<{{properCase event}}>(
      eventStream,
      command,
      {{camelCase domainFunction}},
    );

Then inside execute we can map command to metadata and we don't need extra currying of domainFunction as proposed by @KrystianKjjk .
What do you think @nowakprojects ?

}
}
18 changes: 18 additions & 0 deletions templates/commandHandler/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { commandNamePrompt } from '../command/prompts';
import { domainFunctionNamePrompt } from '../domain-function/prompts';
import { eventNamePrompt } from '../event/prompts';
import { apiDirectoryPrompt } from '../utils/directory.prompt';
import { createCommandHandlerAction } from './actions';
import { streamCategoryPrompt } from './prompts';

export const commandHandlerGenerator = {
description: 'Create a new command handler',
prompts: [
{ ...apiDirectoryPrompt, message: 'Path to module' },
commandNamePrompt,
{ ...eventNamePrompt, message: 'Past event name' },
domainFunctionNamePrompt,
streamCategoryPrompt,
],
actions: [createCommandHandlerAction],
};
1 change: 1 addition & 0 deletions templates/commandHandler/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const streamCategoryPrompt = { type: 'input', name: 'streamCategory', message: 'stream category name' };
11 changes: 11 additions & 0 deletions templates/domain-function/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const createDomainFunctionAction = {
type: 'add',
path: '{{directory}}/domain/{{dashCase domainFunction}}.ts',
templateFile: './templates/domain-function/domain-function.hbs',
};

export const createDomainFunctionTestAction = {
type: 'add',
path: '{{directory}}/domain/{{dashCase domainFunction}}.spec.ts',
templateFile: './templates/domain-function/domain-function.spec.hbs',
};
33 changes: 33 additions & 0 deletions templates/domain-function/domain-function.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { {{properCase event}} } from '@/events/{{dashCase event}}.domain-event';
import { {{properCase command}} } from '@/module/commands/{{dashCase command}}';

export function {{camelCase domainFunction}}(
pastEvents: {{properCase event}}[],
{ data }: {{properCase command}},
): {{properCase event}}[] {
const state = pastEvents
.reduce<{ completed: boolean }>(
kacper-cyra marked this conversation as resolved.
Show resolved Hide resolved
(acc, event) => {
switch (event.type) {
case '{{properCase event}}': {
return { completed: true };
}
default: {
return acc;
}
}
},
{ completed: false },
);

if (state.completed) {
throw new Error('X already completed');
}

const newEvent: {{properCase event}} = {
type: '{{properCase event}}',
data: {},
};
kacper-cyra marked this conversation as resolved.
Show resolved Hide resolved

return [newEvent];
}
43 changes: 43 additions & 0 deletions templates/domain-function/domain-function.spec.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { {{properCase command}} } from '@/module/commands/{{dashCase command}}';
import { {{properCase event}} } from '@/module/events/{{dashCase event}}.domain-event';

import { {{camelCase domainFunction}} } from './{{dashCase domainFunction}}';

describe('{{camelCase domainFunction}}', () => {
const command: {{properCase command}} = {
type: '{{properCase command}}',
data: {},
};

it('should ', () => {
// Given
const pastEvents: {{properCase event}}[] = [];

// When
const events = {{camelCase domainFunction}}(pastEvents, command);

// Then
expect(events).toStrictEqual([
{
type: '{{properCase event}}',
data: {},
},
]);
});

it('should throw exception if ', () => {
// Given
const pastEvents: {{properCase event}}[] = [
{
type: {{properCase event}},
data: {},
},
];

// When
const events = {{camelCase domainFunction}}(pastEvents, command);

// Then
expect(events).toThrowError('');
});
});
15 changes: 15 additions & 0 deletions templates/domain-function/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { commandNamePrompt } from '../command/prompts';
import { eventNamePrompt } from '../event/prompts';
import { createDomainFunctionAction, createDomainFunctionTestAction } from './actions';
import { domainFunctionDirectoryPrompt, domainFunctionNamePrompt } from './prompts';

export const domainFunctionGenerator = {
description: 'Create a new domain function',
prompts: [
domainFunctionDirectoryPrompt,
domainFunctionNamePrompt,
commandNamePrompt,
{ ...eventNamePrompt, message: 'past event name' },
],
actions: [createDomainFunctionAction, createDomainFunctionTestAction],
};
8 changes: 8 additions & 0 deletions templates/domain-function/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { apiDirectoryPrompt } from '../utils/directory.prompt';

export const domainFunctionDirectoryPrompt = {
...apiDirectoryPrompt,
message: 'choose directory in which you want to create domain function',
};

export const domainFunctionNamePrompt = { type: 'input', name: 'domainFunction', message: 'domain function name' };
5 changes: 5 additions & 0 deletions templates/event/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const createEventAction = {
type: 'add',
path: 'packages/api/src/module/shared/events/{{dashCase event}}.domain-event.ts',
templateFile: './templates/event/event.hbs',
};
9 changes: 9 additions & 0 deletions templates/event/event.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type {{properCase event}} = {
type: '{{properCase event}}';
data: {};
};

export const {{camelCase event}}Event = (data: {{properCase event}}['data']): {{properCase event}} => ({
type: '{{properCase event}}',
data,
});
8 changes: 8 additions & 0 deletions templates/event/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createEventAction } from './actions';
import { eventNamePrompt } from './prompts';

export const eventGenerator = {
description: 'Create a new event',
prompts: [eventNamePrompt],
actions: [createEventAction],
};
5 changes: 5 additions & 0 deletions templates/event/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const eventNamePrompt = {
type: 'input',
name: 'event',
message: 'event name',
};
5 changes: 5 additions & 0 deletions templates/helpers/moduleNameFromPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const moduleNameFromPath = (path: string) => {
const pathParts = path.split('\\');

return pathParts[pathParts.length - 1];
};
6 changes: 6 additions & 0 deletions templates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { commandGenerator } from './command/generator';
export { commandHandlerGenerator } from './commandHandler/generator';
export { moduleGenerator } from './module/generator';
export { eventGenerator } from './event/generator';
export { domainFunctionGenerator } from './domain-function/generator';
export { restControllerGenerator } from './rest-controller/generator';
17 changes: 17 additions & 0 deletions templates/module/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const createModuleAction = {
type: 'add',
path: '{{directory}}/{{dashCase module}}/{{dashCase module}}.module.ts',
templateFile: './templates/module/module.hbs',
};

export const createTestModuleAction = {
type: 'add',
path: '{{directory}}/{{dashCase module}}/{{dashCase module}}.test-module.ts',
templateFile: './templates/module/module.test.hbs',
};

export const createTestFileAction = {
type: 'add',
path: '{{directory}}/{{dashCase module}}/{{dashCase module}}.module.spec.ts',
templateFile: './templates/module/module.spec.hbs',
};
56 changes: 56 additions & 0 deletions templates/module/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createCommandAction } from '../command/actions';
import { commandNamePrompt } from '../command/prompts';
import { createCommandHandlerAction } from '../commandHandler/actions';
import { streamCategoryPrompt } from '../commandHandler/prompts';
import { createDomainFunctionAction, createDomainFunctionTestAction } from '../domain-function/actions';
import { domainFunctionNamePrompt } from '../domain-function/prompts';
import { createEventAction } from '../event/actions';
import { eventNamePrompt } from '../event/prompts';
import { createRestControllerAction, createTypesFileWithBodyTypeAction } from '../rest-controller/actions';
import { methodNamePrompt, restControllerNamePrompt } from '../rest-controller/prompts';
import type { Answers } from '../types';
import { runActionIfAnswersWereGiven } from '../utils/runActionConditionally';
import { createModuleAction, createTestFileAction, createTestModuleAction } from './actions';
import { moduleDirectoryPrompt, moduleNamePrompt } from './prompts';

export const moduleGenerator = {
description: 'Create a new module',
prompts: [
moduleNamePrompt,
moduleDirectoryPrompt,
commandNamePrompt,
eventNamePrompt,
{ ...domainFunctionNamePrompt, when: (answers: Answers) => answers[eventNamePrompt.name] !== '' },
{ ...streamCategoryPrompt, when: (answers: Answers) => answers[domainFunctionNamePrompt.name] !== '' },
{ ...restControllerNamePrompt },
methodNamePrompt,
],
actions: [
{ ...createCommandAction, skipIfExist: true },
{ ...createEventAction, skipIfExist: true },
createModuleAction,
createTestModuleAction,
runActionIfAnswersWereGiven([commandNamePrompt.name, eventNamePrompt.name], createTestFileAction),
runActionIfAnswersWereGiven([domainFunctionNamePrompt.name, streamCategoryPrompt.name], {
...createDomainFunctionAction,
path: `{{${moduleDirectoryPrompt.name}}}/{{dashCase ${moduleNamePrompt.name}}}/domain/{{dashCase ${domainFunctionNamePrompt.name}}}.ts`,
}),
runActionIfAnswersWereGiven([domainFunctionNamePrompt.name, streamCategoryPrompt.name], {
...createDomainFunctionTestAction,
path: `{{${moduleDirectoryPrompt.name}}}/{{dashCase ${moduleNamePrompt.name}}}/domain/{{dashCase ${domainFunctionNamePrompt.name}}}.spec.ts`,
}),
{
...createCommandHandlerAction,
path: `{{${moduleDirectoryPrompt.name}}}/{{dashCase ${moduleNamePrompt.name}}}/application/{{dashCase ${commandNamePrompt.name}}}.command-handler.ts`,
},
{
...createRestControllerAction,
path: `{{${moduleDirectoryPrompt.name}}}/{{dashCase ${moduleNamePrompt.name}}}/presentation/rest/{{dashCase ${restControllerNamePrompt.name}}}.rest-controller.ts`,
},
{
...createTypesFileWithBodyTypeAction,
path: `./packages/shared/src/models/{{dashCase ${moduleNamePrompt.name}}}/{{camelCase ${commandNamePrompt.name}}}RequestBody.ts`,
kacper-cyra marked this conversation as resolved.
Show resolved Hide resolved
skipIfExist: true,
},
],
};
Loading