servcraft 0.2.0 ā 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +9 -4
- package/README.md +70 -2
- package/ROADMAP.md +124 -47
- package/dist/cli/index.cjs +1331 -407
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1298 -389
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +116 -1
- package/src/cli/commands/generate.ts +52 -7
- package/src/cli/commands/list.ts +1 -1
- package/src/cli/commands/scaffold.ts +211 -0
- package/src/cli/commands/templates.ts +147 -0
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/src/cli/utils/template-loader.ts +80 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { getProjectRoot } from '../utils/helpers.js';
|
|
7
|
+
import { validateProject, displayError } from '../utils/error-handler.js';
|
|
8
|
+
|
|
9
|
+
// Template types available for customization
|
|
10
|
+
const TEMPLATE_TYPES = [
|
|
11
|
+
'controller',
|
|
12
|
+
'service',
|
|
13
|
+
'repository',
|
|
14
|
+
'types',
|
|
15
|
+
'schemas',
|
|
16
|
+
'routes',
|
|
17
|
+
'module-index',
|
|
18
|
+
'controller-test',
|
|
19
|
+
'service-test',
|
|
20
|
+
'integration-test',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function initTemplates(): Promise<void> {
|
|
24
|
+
const projectError = validateProject();
|
|
25
|
+
if (projectError) {
|
|
26
|
+
displayError(projectError);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectRoot = getProjectRoot();
|
|
31
|
+
const templatesDir = path.join(projectRoot, '.servcraft', 'templates');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Create .servcraft/templates directory
|
|
35
|
+
await fs.mkdir(templatesDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
console.log(chalk.cyan('\nš Creating custom template directory...\n'));
|
|
38
|
+
|
|
39
|
+
// Create example template files
|
|
40
|
+
const exampleController = `// Custom controller template
|
|
41
|
+
// Available variables: name, pascalName, camelName, pluralName
|
|
42
|
+
export function controllerTemplate(name: string, pascalName: string, camelName: string): string {
|
|
43
|
+
return \`import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
44
|
+
import type { \${pascalName}Service } from './\${name}.service.js';
|
|
45
|
+
|
|
46
|
+
export class \${pascalName}Controller {
|
|
47
|
+
constructor(private \${camelName}Service: \${pascalName}Service) {}
|
|
48
|
+
|
|
49
|
+
// Add your custom controller methods here
|
|
50
|
+
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
|
51
|
+
const data = await this.\${camelName}Service.getAll();
|
|
52
|
+
return reply.send({ data });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
\`;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(
|
|
60
|
+
path.join(templatesDir, 'controller.example.ts'),
|
|
61
|
+
exampleController,
|
|
62
|
+
'utf-8'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
console.log(chalk.green('ā Created template directory: .servcraft/templates/'));
|
|
66
|
+
console.log(chalk.green('ā Created example template: controller.example.ts\n'));
|
|
67
|
+
|
|
68
|
+
console.log(chalk.bold('š Available template types:\n'));
|
|
69
|
+
TEMPLATE_TYPES.forEach((type) => {
|
|
70
|
+
console.log(chalk.gray(` ⢠${type}.ts`));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(chalk.yellow('\nš” To customize a template:'));
|
|
74
|
+
console.log(chalk.gray(' 1. Copy the example template'));
|
|
75
|
+
console.log(chalk.gray(' 2. Rename it (remove .example)'));
|
|
76
|
+
console.log(chalk.gray(' 3. Modify the template code'));
|
|
77
|
+
console.log(chalk.gray(' 4. Use --template flag when generating\n'));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
console.error(chalk.red(`\nā Failed to initialize templates: ${error.message}\n`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function listTemplates(): Promise<void> {
|
|
86
|
+
const projectError = validateProject();
|
|
87
|
+
if (projectError) {
|
|
88
|
+
displayError(projectError);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const projectRoot = getProjectRoot();
|
|
93
|
+
const projectTemplatesDir = path.join(projectRoot, '.servcraft', 'templates');
|
|
94
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
95
|
+
const userTemplatesDir = path.join(homeDir, '.servcraft', 'templates');
|
|
96
|
+
|
|
97
|
+
console.log(chalk.bold.cyan('\nš Available Templates\n'));
|
|
98
|
+
|
|
99
|
+
// Check project templates
|
|
100
|
+
console.log(chalk.bold('Project templates (.servcraft/templates/):'));
|
|
101
|
+
try {
|
|
102
|
+
const files = await fs.readdir(projectTemplatesDir);
|
|
103
|
+
const templates = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.example.ts'));
|
|
104
|
+
|
|
105
|
+
if (templates.length > 0) {
|
|
106
|
+
templates.forEach((t) => {
|
|
107
|
+
console.log(chalk.green(` ā ${t}`));
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.gray(' (none)'));
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
console.log(chalk.gray(' (directory not found)'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check user templates
|
|
117
|
+
console.log(chalk.bold('\nUser templates (~/.servcraft/templates/):'));
|
|
118
|
+
try {
|
|
119
|
+
const files = await fs.readdir(userTemplatesDir);
|
|
120
|
+
const templates = files.filter((f) => f.endsWith('.ts') && !f.endsWith('.example.ts'));
|
|
121
|
+
|
|
122
|
+
if (templates.length > 0) {
|
|
123
|
+
templates.forEach((t) => {
|
|
124
|
+
console.log(chalk.green(` ā ${t}`));
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
console.log(chalk.gray(' (none)'));
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
console.log(chalk.gray(' (directory not found)'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Show built-in templates
|
|
134
|
+
console.log(chalk.bold('\nBuilt-in templates:'));
|
|
135
|
+
TEMPLATE_TYPES.forEach((t) => {
|
|
136
|
+
console.log(chalk.cyan(` ⢠${t}.ts`));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log(chalk.gray('\nš” Run "servcraft templates init" to create custom templates\n'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const templatesCommand = new Command('templates')
|
|
143
|
+
.description('Manage code generation templates')
|
|
144
|
+
.addCommand(
|
|
145
|
+
new Command('init').description('Initialize custom templates directory').action(initTemplates)
|
|
146
|
+
)
|
|
147
|
+
.addCommand(new Command('list').description('List available templates').action(listTemplates));
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { getProjectRoot, getModulesDir } from '../utils/helpers.js';
|
|
9
|
+
import { validateProject, displayError } from '../utils/error-handler.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Get list of available modules (same as list.ts)
|
|
15
|
+
const AVAILABLE_MODULES = [
|
|
16
|
+
'auth',
|
|
17
|
+
'users',
|
|
18
|
+
'email',
|
|
19
|
+
'mfa',
|
|
20
|
+
'oauth',
|
|
21
|
+
'rate-limit',
|
|
22
|
+
'cache',
|
|
23
|
+
'upload',
|
|
24
|
+
'search',
|
|
25
|
+
'notification',
|
|
26
|
+
'webhook',
|
|
27
|
+
'websocket',
|
|
28
|
+
'queue',
|
|
29
|
+
'payment',
|
|
30
|
+
'i18n',
|
|
31
|
+
'feature-flag',
|
|
32
|
+
'analytics',
|
|
33
|
+
'media-processing',
|
|
34
|
+
'api-versioning',
|
|
35
|
+
'audit',
|
|
36
|
+
'swagger',
|
|
37
|
+
'validation',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
async function getInstalledModules(): Promise<string[]> {
|
|
41
|
+
try {
|
|
42
|
+
const modulesDir = getModulesDir();
|
|
43
|
+
|
|
44
|
+
const entries = await fs.readdir(modulesDir, { withFileTypes: true });
|
|
45
|
+
const installedModules = entries
|
|
46
|
+
.filter((entry) => entry.isDirectory())
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.filter((name) => AVAILABLE_MODULES.includes(name));
|
|
49
|
+
|
|
50
|
+
return installedModules;
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function copyModuleFiles(moduleName: string, _projectRoot: string): Promise<void> {
|
|
57
|
+
const cliRoot = path.resolve(__dirname, '../../../');
|
|
58
|
+
const sourceModulePath = path.join(cliRoot, 'src', 'modules', moduleName);
|
|
59
|
+
const targetModulesDir = getModulesDir();
|
|
60
|
+
const targetModulePath = path.join(targetModulesDir, moduleName);
|
|
61
|
+
|
|
62
|
+
// Check if source module exists
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(sourceModulePath);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Copy module files
|
|
70
|
+
await fs.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function updateModule(moduleName: string, options: { check?: boolean }): Promise<void> {
|
|
74
|
+
const projectError = validateProject();
|
|
75
|
+
if (projectError) {
|
|
76
|
+
displayError(projectError);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const projectRoot = getProjectRoot();
|
|
81
|
+
const installedModules = await getInstalledModules();
|
|
82
|
+
|
|
83
|
+
if (!installedModules.includes(moduleName)) {
|
|
84
|
+
console.log(chalk.yellow(`\nā Module "${moduleName}" is not installed\n`));
|
|
85
|
+
console.log(
|
|
86
|
+
chalk.gray(`Run ${chalk.cyan(`servcraft add ${moduleName}`)} to install it first.\n`)
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.check) {
|
|
92
|
+
console.log(chalk.cyan(`\nš¦ Checking updates for "${moduleName}"...\n`));
|
|
93
|
+
console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
|
|
94
|
+
console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Confirm update
|
|
99
|
+
const { confirmed } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'confirmed',
|
|
103
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
console.log(chalk.yellow('\nā Update cancelled\n'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.cyan(`\nš Updating "${moduleName}" module...\n`));
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
117
|
+
console.log(chalk.green(`ā Module "${moduleName}" updated successfully!\n`));
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
console.error(chalk.red(`\nā Failed to update module: ${error.message}\n`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function updateAllModules(options: { check?: boolean }): Promise<void> {
|
|
129
|
+
const projectError = validateProject();
|
|
130
|
+
if (projectError) {
|
|
131
|
+
displayError(projectError);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const installedModules = await getInstalledModules();
|
|
136
|
+
|
|
137
|
+
if (installedModules.length === 0) {
|
|
138
|
+
console.log(chalk.yellow('\nā No modules installed\n'));
|
|
139
|
+
console.log(chalk.gray(`Run ${chalk.cyan('servcraft list')} to see available modules.\n`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.check) {
|
|
144
|
+
console.log(chalk.cyan('\nš¦ Checking updates for all modules...\n'));
|
|
145
|
+
console.log(chalk.bold('Installed modules:'));
|
|
146
|
+
installedModules.forEach((mod) => {
|
|
147
|
+
console.log(` ⢠${chalk.cyan(mod)}`);
|
|
148
|
+
});
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
|
|
151
|
+
console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(chalk.cyan(`\nš¦ Found ${installedModules.length} installed module(s):\n`));
|
|
156
|
+
installedModules.forEach((mod) => {
|
|
157
|
+
console.log(` ⢠${chalk.cyan(mod)}`);
|
|
158
|
+
});
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
// Confirm update all
|
|
162
|
+
const { confirmed } = await inquirer.prompt([
|
|
163
|
+
{
|
|
164
|
+
type: 'confirm',
|
|
165
|
+
name: 'confirmed',
|
|
166
|
+
message: 'Update all modules? This will overwrite existing files.',
|
|
167
|
+
default: false,
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
if (!confirmed) {
|
|
172
|
+
console.log(chalk.yellow('\nā Update cancelled\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(chalk.cyan('\nš Updating all modules...\n'));
|
|
177
|
+
|
|
178
|
+
const projectRoot = getProjectRoot();
|
|
179
|
+
let successCount = 0;
|
|
180
|
+
let failCount = 0;
|
|
181
|
+
|
|
182
|
+
for (const moduleName of installedModules) {
|
|
183
|
+
try {
|
|
184
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
185
|
+
console.log(chalk.green(`ā Updated: ${moduleName}`));
|
|
186
|
+
successCount++;
|
|
187
|
+
} catch {
|
|
188
|
+
console.error(chalk.red(`ā Failed: ${moduleName}`));
|
|
189
|
+
failCount++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(
|
|
195
|
+
chalk.bold(
|
|
196
|
+
`\nā Update complete: ${chalk.green(successCount)} succeeded, ${chalk.red(failCount)} failed\n`
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (successCount > 0) {
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const updateCommand = new Command('update')
|
|
208
|
+
.description('Update installed modules to latest version')
|
|
209
|
+
.argument('[module]', 'Specific module to update')
|
|
210
|
+
.option('--check', 'Check for updates without applying')
|
|
211
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
212
|
+
.action(async (moduleName?: string, options?: { check?: boolean; yes?: boolean }) => {
|
|
213
|
+
// If --yes flag is provided, we'll handle it by auto-confirming in the inquirer prompts
|
|
214
|
+
// For now, we'll just pass through to the update functions
|
|
215
|
+
|
|
216
|
+
if (moduleName) {
|
|
217
|
+
await updateModule(moduleName, { check: options?.check });
|
|
218
|
+
} else {
|
|
219
|
+
await updateAllModules({ check: options?.check });
|
|
220
|
+
}
|
|
221
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { docsCommand } from './commands/docs.js';
|
|
|
9
9
|
import { listCommand } from './commands/list.js';
|
|
10
10
|
import { removeCommand } from './commands/remove.js';
|
|
11
11
|
import { doctorCommand } from './commands/doctor.js';
|
|
12
|
+
import { updateCommand } from './commands/update.js';
|
|
13
|
+
import { completionCommand } from './commands/completion.js';
|
|
14
|
+
import { scaffoldCommand } from './commands/scaffold.js';
|
|
15
|
+
import { templatesCommand } from './commands/templates.js';
|
|
12
16
|
|
|
13
17
|
const program = new Command();
|
|
14
18
|
|
|
@@ -41,4 +45,16 @@ program.addCommand(removeCommand);
|
|
|
41
45
|
// Diagnose project
|
|
42
46
|
program.addCommand(doctorCommand);
|
|
43
47
|
|
|
48
|
+
// Update modules
|
|
49
|
+
program.addCommand(updateCommand);
|
|
50
|
+
|
|
51
|
+
// Shell completion
|
|
52
|
+
program.addCommand(completionCommand);
|
|
53
|
+
|
|
54
|
+
// Scaffold resource
|
|
55
|
+
program.addCommand(scaffoldCommand);
|
|
56
|
+
|
|
57
|
+
// Template management
|
|
58
|
+
program.addCommand(templatesCommand);
|
|
59
|
+
|
|
44
60
|
program.parse();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function controllerTestTemplate(
|
|
2
|
+
name: string,
|
|
3
|
+
pascalName: string,
|
|
4
|
+
camelName: string
|
|
5
|
+
): string {
|
|
6
|
+
return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
7
|
+
import { build } from '../../app.js';
|
|
8
|
+
import { FastifyInstance } from 'fastify';
|
|
9
|
+
|
|
10
|
+
describe('${pascalName}Controller', () => {
|
|
11
|
+
let app: FastifyInstance;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
app = await build();
|
|
15
|
+
await app.ready();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await app.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('GET /${name}', () => {
|
|
23
|
+
it('should return list of ${name}', async () => {
|
|
24
|
+
const response = await app.inject({
|
|
25
|
+
method: 'GET',
|
|
26
|
+
url: '/${name}',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(response.statusCode).toBe(200);
|
|
30
|
+
expect(response.json()).toHaveProperty('data');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('GET /${name}/:id', () => {
|
|
35
|
+
it('should return a single ${camelName}', async () => {
|
|
36
|
+
// TODO: Create test ${camelName} first
|
|
37
|
+
const response = await app.inject({
|
|
38
|
+
method: 'GET',
|
|
39
|
+
url: '/${name}/1',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(response.statusCode).toBe(200);
|
|
43
|
+
expect(response.json()).toHaveProperty('data');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return 404 for non-existent ${camelName}', async () => {
|
|
47
|
+
const response = await app.inject({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
url: '/${name}/999999',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(response.statusCode).toBe(404);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('POST /${name}', () => {
|
|
57
|
+
it('should create a new ${camelName}', async () => {
|
|
58
|
+
const response = await app.inject({
|
|
59
|
+
method: 'POST',
|
|
60
|
+
url: '/${name}',
|
|
61
|
+
payload: {
|
|
62
|
+
// TODO: Add required fields
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(response.statusCode).toBe(201);
|
|
67
|
+
expect(response.json()).toHaveProperty('data');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return 400 for invalid data', async () => {
|
|
71
|
+
const response = await app.inject({
|
|
72
|
+
method: 'POST',
|
|
73
|
+
url: '/${name}',
|
|
74
|
+
payload: {},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(response.statusCode).toBe(400);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('PUT /${name}/:id', () => {
|
|
82
|
+
it('should update a ${camelName}', async () => {
|
|
83
|
+
// TODO: Create test ${camelName} first
|
|
84
|
+
const response = await app.inject({
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
url: '/${name}/1',
|
|
87
|
+
payload: {
|
|
88
|
+
// TODO: Add fields to update
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(response.statusCode).toBe(200);
|
|
93
|
+
expect(response.json()).toHaveProperty('data');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('DELETE /${name}/:id', () => {
|
|
98
|
+
it('should delete a ${camelName}', async () => {
|
|
99
|
+
// TODO: Create test ${camelName} first
|
|
100
|
+
const response = await app.inject({
|
|
101
|
+
method: 'DELETE',
|
|
102
|
+
url: '/${name}/1',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(response.statusCode).toBe(204);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export function integrationTestTemplate(
|
|
2
|
+
name: string,
|
|
3
|
+
pascalName: string,
|
|
4
|
+
camelName: string
|
|
5
|
+
): string {
|
|
6
|
+
return `import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
7
|
+
import { build } from '../../app.js';
|
|
8
|
+
import { FastifyInstance } from 'fastify';
|
|
9
|
+
import { prisma } from '../../lib/prisma.js';
|
|
10
|
+
|
|
11
|
+
describe('${pascalName} Integration Tests', () => {
|
|
12
|
+
let app: FastifyInstance;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
app = await build();
|
|
16
|
+
await app.ready();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await app.close();
|
|
21
|
+
await prisma.$disconnect();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
// Clean up test data
|
|
26
|
+
// await prisma.${camelName}.deleteMany();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Full CRUD workflow', () => {
|
|
30
|
+
it('should create, read, update, and delete a ${camelName}', async () => {
|
|
31
|
+
// Create
|
|
32
|
+
const createResponse = await app.inject({
|
|
33
|
+
method: 'POST',
|
|
34
|
+
url: '/${name}',
|
|
35
|
+
payload: {
|
|
36
|
+
// TODO: Add required fields
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(createResponse.statusCode).toBe(201);
|
|
41
|
+
const created = createResponse.json().data;
|
|
42
|
+
expect(created.id).toBeDefined();
|
|
43
|
+
|
|
44
|
+
// Read
|
|
45
|
+
const readResponse = await app.inject({
|
|
46
|
+
method: 'GET',
|
|
47
|
+
url: \`/${name}/\${created.id}\`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(readResponse.statusCode).toBe(200);
|
|
51
|
+
const read = readResponse.json().data;
|
|
52
|
+
expect(read.id).toBe(created.id);
|
|
53
|
+
|
|
54
|
+
// Update
|
|
55
|
+
const updateResponse = await app.inject({
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
url: \`/${name}/\${created.id}\`,
|
|
58
|
+
payload: {
|
|
59
|
+
// TODO: Add fields to update
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(updateResponse.statusCode).toBe(200);
|
|
64
|
+
const updated = updateResponse.json().data;
|
|
65
|
+
expect(updated.id).toBe(created.id);
|
|
66
|
+
|
|
67
|
+
// Delete
|
|
68
|
+
const deleteResponse = await app.inject({
|
|
69
|
+
method: 'DELETE',
|
|
70
|
+
url: \`/${name}/\${created.id}\`,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(deleteResponse.statusCode).toBe(204);
|
|
74
|
+
|
|
75
|
+
// Verify deletion
|
|
76
|
+
const verifyResponse = await app.inject({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
url: \`/${name}/\${created.id}\`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(verifyResponse.statusCode).toBe(404);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('List and pagination', () => {
|
|
86
|
+
it('should list ${name} with pagination', async () => {
|
|
87
|
+
// Create multiple ${name}
|
|
88
|
+
const count = 5;
|
|
89
|
+
for (let i = 0; i < count; i++) {
|
|
90
|
+
await app.inject({
|
|
91
|
+
method: 'POST',
|
|
92
|
+
url: '/${name}',
|
|
93
|
+
payload: {
|
|
94
|
+
// TODO: Add required fields
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Test pagination
|
|
100
|
+
const response = await app.inject({
|
|
101
|
+
method: 'GET',
|
|
102
|
+
url: '/${name}?page=1&limit=3',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(response.statusCode).toBe(200);
|
|
106
|
+
const result = response.json();
|
|
107
|
+
expect(result.data).toBeDefined();
|
|
108
|
+
expect(result.data.length).toBeLessThanOrEqual(3);
|
|
109
|
+
expect(result.total).toBeGreaterThanOrEqual(count);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Validation', () => {
|
|
114
|
+
it('should validate required fields on create', async () => {
|
|
115
|
+
const response = await app.inject({
|
|
116
|
+
method: 'POST',
|
|
117
|
+
url: '/${name}',
|
|
118
|
+
payload: {},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(response.statusCode).toBe(400);
|
|
122
|
+
expect(response.json()).toHaveProperty('error');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should validate data types', async () => {
|
|
126
|
+
const response = await app.inject({
|
|
127
|
+
method: 'POST',
|
|
128
|
+
url: '/${name}',
|
|
129
|
+
payload: {
|
|
130
|
+
// TODO: Add invalid field types
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(response.statusCode).toBe(400);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
`;
|
|
139
|
+
}
|