Skip to content

Commit 7372a91

Browse files
committed
feat(api): add capability to create empty route handlers
1 parent a71a72b commit 7372a91

File tree

9 files changed

+631
-199
lines changed

9 files changed

+631
-199
lines changed

package.json

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,38 @@
3131
"*.{js,jsx,ts,tsx}": "yarn eslint src --cache --fix"
3232
},
3333
"devDependencies": {
34-
"@commitlint/cli": "^17.6.3",
35-
"@commitlint/config-conventional": "^17.6.3",
36-
"@openapi-typescript-infra/coconfig": "^3.0.1",
34+
"@commitlint/cli": "^17.6.6",
35+
"@commitlint/config-conventional": "^17.6.6",
36+
"@openapi-typescript-infra/coconfig": "^3.2.0",
3737
"@types/configstore": "^6.0.0",
3838
"@types/handlebars-helpers": "^0.5.3",
3939
"@types/minimist": "^1.2.2",
40-
"@types/node": "^20.2.4",
40+
"@types/node": "^20.4.2",
4141
"@types/parse-git-config": "^3.0.1",
42-
"@typescript-eslint/eslint-plugin": "^5.59.7",
43-
"@typescript-eslint/parser": "^5.59.7",
44-
"coconfig": "^0.12.0",
45-
"eslint": "^8.41.0",
42+
"@types/pascalcase": "^1.0.1",
43+
"@typescript-eslint/eslint-plugin": "^6.1.0",
44+
"@typescript-eslint/parser": "^6.1.0",
45+
"coconfig": "^0.12.2",
46+
"eslint": "^8.45.0",
4647
"eslint-config-prettier": "^8.8.0",
4748
"eslint-import-resolver-typescript": "^3.5.5",
4849
"eslint-plugin-import": "^2.27.5",
49-
"eslint-plugin-jest": "^27.2.1",
50+
"eslint-plugin-jest": "^27.2.3",
5051
"husky": "^8.0.3",
51-
"lint-staged": "^13.2.2",
52+
"lint-staged": "^13.2.3",
5253
"pinst": "^3.0.0",
53-
"prettier": "^2.8.8",
54-
"typescript": "^5.0.4"
54+
"prettier": "^3.0.0",
55+
"typescript": "^5.1.6"
5556
},
5657
"packageManager": "[email protected]",
5758
"dependencies": {
58-
"boxen": "^7.1.0",
59+
"@readme/openapi-parser": "^2.5.0",
60+
"boxen": "^7.1.1",
5961
"configstore": "^6.0.0",
6062
"handlebars-helpers": "^0.10.0",
6163
"minimist": "^1.2.8",
64+
"mkdirp": "^3.0.1",
65+
"openapi-types": "^12.1.3",
6266
"parse-git-config": "^3.0.0",
6367
"plop": "^3.1.2",
6468
"plop-pack-git-init": "^0.3.1"
File renamed without changes.

src/dependencies.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ function sortByKey(deps: Record<string, string>) {
99

1010
export function dependencies({ features }: { features: string[] }) {
1111
const deps = {
12-
'@openapi-typescript-infra/coconfig': '^3.0.2',
13-
'@openapi-typescript-infra/service': '^1.1.0',
12+
'@openapi-typescript-infra/coconfig': '^3.2.0',
13+
'@openapi-typescript-infra/service': '^1.1.3',
1414
} as Record<string, string>;
1515
if (features.includes('db')) {
1616
Object.assign(deps, {
@@ -28,23 +28,23 @@ export function dependencies({ features }: { features: string[] }) {
2828

2929
export function devDependencies({ features }: { features: string[] }) {
3030
const deps = {
31-
coconfig: '^0.10.1',
31+
coconfig: '^0.12.2',
3232
'@openapi-typescript-infra/service-tester': '^1.0.5',
33-
'@typescript-eslint/eslint-plugin': '^5.59.7',
34-
'@typescript-eslint/parser': '^5.59.7',
35-
'@types/jest': '^29.5.1',
36-
eslint: '^8.41.0',
33+
'@typescript-eslint/eslint-plugin': '^6.1.0',
34+
'@typescript-eslint/parser': '^6.1.0',
35+
'@types/jest': '^29.5.3',
36+
eslint: '^8.45.0',
3737
'eslint-config-prettier': '^8.8.0',
3838
'eslint-plugin-cypress': '^2.13.3',
3939
'eslint-plugin-import': '^2.27.5',
40-
'eslint-plugin-jest': '^27.2.1',
40+
'eslint-plugin-jest': '^27.2.3',
4141
husky: '^8.0.3',
42-
jest: '^29.5.0',
42+
jest: '^29.6.1',
4343
'jest-openapi': '^0.14.2',
44-
'lint-staged': '^13.2.2',
45-
'prettier': '^2.8.8',
46-
'ts-jest': '^29.1.0',
47-
typescript: '^4.9.5',
44+
'lint-staged': '^13.2.3',
45+
'prettier': '^3.0.0',
46+
'ts-jest': '^29.1.1',
47+
typescript: '^5.1.6',
4848
};
4949
if (features.includes('db')) {
5050
Object.assign(deps, {

src/handlers.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import pascalcase from 'pascalcase';
5+
import Helpers from 'handlebars-helpers';
6+
import { mkdirpSync } from 'mkdirp';
7+
import OpenAPIParser from '@readme/openapi-parser';
8+
import type { HelperDelegate } from 'handlebars';
9+
import type { NodePlopAPI } from 'plop';
10+
11+
import { Prompts } from './prompts.js';
12+
import { useCliAction } from './actions.js';
13+
14+
async function writeMissing(
15+
api: string,
16+
methods: Record<string, Record<string, { operationId: string; }>>,
17+
apiName: string) {
18+
const handlerPath = path.join('src', 'handlers', `${api}.ts`);
19+
const exists = fs.existsSync(handlerPath);
20+
if (!exists) {
21+
console.log('Creating', handlerPath);
22+
mkdirpSync(path.dirname(handlerPath));
23+
fs.writeFileSync(
24+
handlerPath,
25+
`import type { ${apiName} } from '@/types';
26+
27+
${Object.keys(methods)
28+
.filter((f) => !['parameters'].includes(f))
29+
.map(
30+
(method) =>
31+
`export const ${method}: ${apiName}['${methods[method].operationId}'] = async (req, res) => {
32+
// TODO - implement ${method} handler
33+
res.sendStatus(501);
34+
};
35+
`,
36+
)}`,
37+
);
38+
}
39+
}
40+
41+
// eslint-disable-next-line import/no-default-export
42+
export default function (plop: NodePlopAPI) {
43+
const helpers = Helpers();
44+
Object.entries(helpers).forEach(([name, helper]) => {
45+
if (!['raw'].includes(name)) {
46+
plop.setHelper(name, helper);
47+
}
48+
});
49+
plop.setHelper('ternary', ((test, yes, no) => (test ? yes : no)) as HelperDelegate);
50+
useCliAction(plop);
51+
52+
// controller generator
53+
plop.setGenerator('handlers', {
54+
description: 'Generate missing API handlers',
55+
prompts: [Prompts.handlerWarning],
56+
actions() {
57+
return [
58+
async () => {
59+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
60+
const svcName = pkg.name.split('/').pop();
61+
const apiSpec = `api/${svcName}.yaml`;
62+
const { paths } = await OpenAPIParser.validate(apiSpec);
63+
const apiName = `${pascalcase(svcName)}Api`;
64+
if (paths) {
65+
Object.entries(paths).forEach(([api, methods]) => writeMissing(api, methods, apiName));
66+
}
67+
return '';
68+
},
69+
`
70+
We have added any missing handler FILES to your project. If new methods
71+
needed to be added to existing handler files, we did not add those.
72+
73+
Happy hacking!`,
74+
].filter((a) => typeof a === 'object' || typeof a === 'string' || typeof a === 'function');
75+
},
76+
});
77+
}

src/index.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env node
2+
import fs from 'node:fs';
23
import path from 'node:path';
34
import * as url from 'url';
45

@@ -12,22 +13,41 @@ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
1213
async function runPlop() {
1314
const { Plop, run } = await import('plop');
1415

15-
Plop.prepare(
16-
{
17-
cwd: argv.cwd,
18-
configPath: path.join(__dirname, 'plopfile.js'),
19-
preload: argv.preload || [],
20-
completion: argv.completion,
21-
},
22-
(env) =>
23-
Plop.execute(env, (env) => {
24-
const options = {
25-
...env,
26-
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
27-
};
28-
return run(options, undefined, true);
29-
}),
30-
);
16+
if (fs.existsSync('package.json')) {
17+
Plop.prepare(
18+
{
19+
cwd: argv.cwd,
20+
configPath: path.join(__dirname, 'handlers.js'),
21+
preload: argv.preload || [],
22+
completion: argv.completion,
23+
},
24+
(env) =>
25+
Plop.execute(env, (env) => {
26+
const options = {
27+
...env,
28+
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
29+
};
30+
return run(options, undefined, true);
31+
}),
32+
);
33+
} else {
34+
Plop.prepare(
35+
{
36+
cwd: argv.cwd,
37+
configPath: path.join(__dirname, 'apiProject.js'),
38+
preload: argv.preload || [],
39+
completion: argv.completion,
40+
},
41+
(env) =>
42+
Plop.execute(env, (env) => {
43+
const options = {
44+
...env,
45+
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
46+
};
47+
return run(options, undefined, true);
48+
}),
49+
);
50+
}
3151
}
3252

3353
runPlop();

src/prompts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export const Prompts: Record<string, PromptQuestion> = {
3636
name: 'dirWarning',
3737
message: `This will create files in the current directory (${process.cwd()}). Continue?`
3838
},
39+
handlerWarning: {
40+
type: 'confirm',
41+
name: 'handlerWarning',
42+
message: `This will create missing handlers for the project in (${process.cwd()}). Continue?`
43+
},
3944
email: {
4045
type: 'input',
4146
name: 'email',

templates/all/.gitignore.hbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Custom
2+
src/generated
23
.transpiled
34
.nyc_output
45
.eslintcache

templates/all/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
.PHONY: all build clean
55

6-
all: service {{#inArray features 'db'}}dbi {{/inArray}}$(word 1, $(build_files))
6+
all: service {{#inArray features 'db'}}dbi {{/inArray}}ts
77

88
{{#inArray features 'db'}}export DB_NAME ?= {{dbName}}
99
{{/inArray}}export SERVICE_NAME ?= {{dashCase name}}

0 commit comments

Comments
 (0)