servcraft 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servcraft",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A modular, production-ready Node.js backend framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,146 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable no-useless-escape */
3
+ import { Command } from 'commander';
4
+
5
+ const bashScript = `
6
+ # servcraft bash completion script
7
+ _servcraft_completions() {
8
+ local cur prev words cword
9
+ _init_completion || return
10
+
11
+ # Main commands
12
+ local commands="init add generate list remove doctor update completion docs --version --help"
13
+
14
+ # Generate subcommands
15
+ local generate_subcommands="module controller service repository types schema routes m c s r t"
16
+
17
+ case "\${words[1]}" in
18
+ generate|g)
19
+ if [[ \${cword} -eq 2 ]]; then
20
+ COMPREPLY=( \$(compgen -W "\${generate_subcommands}" -- "\${cur}") )
21
+ fi
22
+ ;;
23
+ add|remove|rm|update)
24
+ if [[ \${cword} -eq 2 ]]; then
25
+ # Get available modules
26
+ local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
27
+ COMPREPLY=( \$(compgen -W "\${modules}" -- "\${cur}") )
28
+ fi
29
+ ;;
30
+ completion)
31
+ if [[ \${cword} -eq 2 ]]; then
32
+ COMPREPLY=( \$(compgen -W "bash zsh" -- "\${cur}") )
33
+ fi
34
+ ;;
35
+ *)
36
+ if [[ \${cword} -eq 1 ]]; then
37
+ COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
38
+ fi
39
+ ;;
40
+ esac
41
+ }
42
+
43
+ complete -F _servcraft_completions servcraft
44
+ `;
45
+
46
+ const zshScript = `
47
+ #compdef servcraft
48
+
49
+ _servcraft() {
50
+ local context state state_descr line
51
+ typeset -A opt_args
52
+
53
+ _arguments -C \\
54
+ '1: :_servcraft_commands' \\
55
+ '*::arg:->args'
56
+
57
+ case $state in
58
+ args)
59
+ case $line[1] in
60
+ generate|g)
61
+ _servcraft_generate
62
+ ;;
63
+ add|remove|rm|update)
64
+ _servcraft_modules
65
+ ;;
66
+ completion)
67
+ _arguments '1: :(bash zsh)'
68
+ ;;
69
+ esac
70
+ ;;
71
+ esac
72
+ }
73
+
74
+ _servcraft_commands() {
75
+ local commands
76
+ commands=(
77
+ 'init:Initialize a new ServCraft project'
78
+ 'add:Add a pre-built module to your project'
79
+ 'generate:Generate code files (controller, service, etc.)'
80
+ 'list:List available and installed modules'
81
+ 'remove:Remove an installed module'
82
+ 'doctor:Diagnose project configuration'
83
+ 'update:Update installed modules'
84
+ 'completion:Generate shell completion scripts'
85
+ 'docs:Open documentation'
86
+ '--version:Show version'
87
+ '--help:Show help'
88
+ )
89
+ _describe 'command' commands
90
+ }
91
+
92
+ _servcraft_generate() {
93
+ local subcommands
94
+ subcommands=(
95
+ 'module:Generate a complete module (controller + service + routes)'
96
+ 'controller:Generate a controller'
97
+ 'service:Generate a service'
98
+ 'repository:Generate a repository'
99
+ 'types:Generate TypeScript types'
100
+ 'schema:Generate validation schema'
101
+ 'routes:Generate routes file'
102
+ 'm:Alias for module'
103
+ 'c:Alias for controller'
104
+ 's:Alias for service'
105
+ 'r:Alias for repository'
106
+ 't:Alias for types'
107
+ )
108
+ _describe 'subcommand' subcommands
109
+ }
110
+
111
+ _servcraft_modules() {
112
+ local modules
113
+ modules=(
114
+ 'auth:Authentication & Authorization'
115
+ 'cache:Redis caching'
116
+ 'rate-limit:Rate limiting'
117
+ 'notification:Email/SMS notifications'
118
+ 'payment:Payment integration'
119
+ 'oauth:OAuth providers'
120
+ 'mfa:Multi-factor authentication'
121
+ 'queue:Background jobs'
122
+ 'websocket:WebSocket support'
123
+ 'upload:File upload handling'
124
+ )
125
+ _describe 'module' modules
126
+ }
127
+
128
+ _servcraft "$@"
129
+ `;
130
+
131
+ export const completionCommand = new Command('completion')
132
+ .description('Generate shell completion scripts')
133
+ .argument('<shell>', 'Shell type (bash or zsh)')
134
+ .action((shell: string) => {
135
+ const shellLower = shell.toLowerCase();
136
+
137
+ if (shellLower === 'bash') {
138
+ console.log(bashScript);
139
+ } else if (shellLower === 'zsh') {
140
+ console.log(zshScript);
141
+ } else {
142
+ console.error(`Unsupported shell: ${shell}`);
143
+ console.error('Supported shells: bash, zsh');
144
+ process.exit(1);
145
+ }
146
+ });
@@ -1,8 +1,123 @@
1
+ /* eslint-disable no-console */
1
2
  import { Command } from 'commander';
2
3
  import chalk from 'chalk';
4
+ import fs from 'fs/promises';
5
+
6
+ interface Check {
7
+ name: string;
8
+ status: 'pass' | 'warn' | 'fail';
9
+ message: string;
10
+ suggestion?: string;
11
+ }
12
+
13
+ async function checkNodeVersion(): Promise<Check> {
14
+ const version = process.version;
15
+ const major = parseInt(version.slice(1).split('.')[0] || '0', 10);
16
+
17
+ if (major >= 18) {
18
+ return { name: 'Node.js', status: 'pass', message: `${version} ✓` };
19
+ }
20
+ return {
21
+ name: 'Node.js',
22
+ status: 'fail',
23
+ message: `${version} (< 18)`,
24
+ suggestion: 'Upgrade to Node.js 18+',
25
+ };
26
+ }
27
+
28
+ async function checkPackageJson(): Promise<Check[]> {
29
+ const checks: Check[] = [];
30
+ try {
31
+ const content = await fs.readFile('package.json', 'utf-8');
32
+ const pkg = JSON.parse(content);
33
+
34
+ checks.push({ name: 'package.json', status: 'pass', message: 'Found' });
35
+
36
+ if (pkg.dependencies?.fastify) {
37
+ checks.push({ name: 'Fastify', status: 'pass', message: 'Installed' });
38
+ } else {
39
+ checks.push({
40
+ name: 'Fastify',
41
+ status: 'fail',
42
+ message: 'Missing',
43
+ suggestion: 'npm install fastify',
44
+ });
45
+ }
46
+ } catch {
47
+ checks.push({
48
+ name: 'package.json',
49
+ status: 'fail',
50
+ message: 'Not found',
51
+ suggestion: 'Run servcraft init',
52
+ });
53
+ }
54
+ return checks;
55
+ }
56
+
57
+ async function checkDirectories(): Promise<Check[]> {
58
+ const checks: Check[] = [];
59
+ const dirs = ['src', 'node_modules', '.git', '.env'];
60
+
61
+ for (const dir of dirs) {
62
+ try {
63
+ await fs.access(dir);
64
+ checks.push({ name: dir, status: 'pass', message: 'Exists' });
65
+ } catch {
66
+ const isCritical = dir === 'src' || dir === 'node_modules';
67
+ checks.push({
68
+ name: dir,
69
+ status: isCritical ? 'fail' : 'warn',
70
+ message: 'Not found',
71
+ suggestion:
72
+ dir === 'node_modules' ? 'npm install' : dir === '.env' ? 'Create .env file' : undefined,
73
+ });
74
+ }
75
+ }
76
+ return checks;
77
+ }
3
78
 
4
79
  export const doctorCommand = new Command('doctor')
5
80
  .description('Diagnose project configuration and dependencies')
6
81
  .action(async () => {
7
- console.log(chalk.bold.cyan('\nServCraft Doctor - Coming soon!\n'));
82
+ console.log(chalk.bold.cyan('\n🔍 ServCraft Doctor\n'));
83
+
84
+ const allChecks: Check[] = [];
85
+
86
+ allChecks.push(await checkNodeVersion());
87
+ allChecks.push(...(await checkPackageJson()));
88
+ allChecks.push(...(await checkDirectories()));
89
+
90
+ // Display results
91
+ allChecks.forEach((check) => {
92
+ const icon =
93
+ check.status === 'pass'
94
+ ? chalk.green('✓')
95
+ : check.status === 'warn'
96
+ ? chalk.yellow('⚠')
97
+ : chalk.red('✗');
98
+ const color =
99
+ check.status === 'pass' ? chalk.green : check.status === 'warn' ? chalk.yellow : chalk.red;
100
+
101
+ console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
102
+ if (check.suggestion) {
103
+ console.log(chalk.gray(` → ${check.suggestion}`));
104
+ }
105
+ });
106
+
107
+ const pass = allChecks.filter((c) => c.status === 'pass').length;
108
+ const warn = allChecks.filter((c) => c.status === 'warn').length;
109
+ const fail = allChecks.filter((c) => c.status === 'fail').length;
110
+
111
+ console.log(chalk.gray('\n' + '─'.repeat(60)));
112
+ console.log(
113
+ `\n${chalk.green(pass + ' passed')} | ${chalk.yellow(warn + ' warnings')} | ${chalk.red(fail + ' failed')}\n`
114
+ );
115
+
116
+ if (fail === 0 && warn === 0) {
117
+ console.log(chalk.green.bold('✨ Everything looks good!\n'));
118
+ } else if (fail > 0) {
119
+ console.log(chalk.red.bold('✗ Fix critical issues before using ServCraft.\n'));
120
+ } else {
121
+ console.log(chalk.yellow.bold('⚠ Some warnings, but should work.\n'));
122
+ }
8
123
  });
@@ -45,6 +45,9 @@ import { prismaModelTemplate } from '../templates/prisma-model.js';
45
45
  import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
46
46
  import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
47
47
  import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
48
+ import { controllerTestTemplate } from '../templates/controller-test.js';
49
+ import { serviceTestTemplate } from '../templates/service-test.js';
50
+ import { integrationTestTemplate } from '../templates/integration-test.js';
48
51
 
49
52
  export const generateCommand = new Command('generate')
50
53
  .alias('g')
@@ -62,6 +65,7 @@ generateCommand
62
65
  .option('--prisma', 'Generate Prisma model suggestion')
63
66
  .option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
64
67
  .option('-i, --interactive', 'Interactive mode to define fields')
68
+ .option('--with-tests', 'Generate test files (__tests__ directory)')
65
69
  .option('--dry-run', 'Preview changes without writing files')
66
70
  .action(async (name: string, fieldsArgs: string[], options) => {
67
71
  enableDryRunIfNeeded(options);
@@ -139,6 +143,26 @@ generateCommand
139
143
  await writeFile(path.join(moduleDir, file.name), file.content);
140
144
  }
141
145
 
146
+ // Generate test files if --with-tests flag is provided
147
+ if (options.withTests) {
148
+ const testDir = path.join(moduleDir, '__tests__');
149
+
150
+ await writeFile(
151
+ path.join(testDir, `${kebabName}.controller.test.ts`),
152
+ controllerTestTemplate(kebabName, pascalName, camelName)
153
+ );
154
+
155
+ await writeFile(
156
+ path.join(testDir, `${kebabName}.service.test.ts`),
157
+ serviceTestTemplate(kebabName, pascalName, camelName)
158
+ );
159
+
160
+ await writeFile(
161
+ path.join(testDir, `${kebabName}.integration.test.ts`),
162
+ integrationTestTemplate(kebabName, pascalName, camelName)
163
+ );
164
+ }
165
+
142
166
  spinner.succeed(`Module "${pascalName}" generated successfully!`);
143
167
 
144
168
  // Show Prisma model if requested or fields provided
@@ -169,6 +193,12 @@ generateCommand
169
193
  console.log('\n📁 Files created:');
170
194
  files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
171
195
 
196
+ if (options.withTests) {
197
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
198
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
199
+ success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
200
+ }
201
+
172
202
  console.log('\n📌 Next steps:');
173
203
  if (!hasFields) {
174
204
  info(' 1. Update the types in ' + `${kebabName}.types.ts`);
@@ -193,7 +193,7 @@ export const listCommand = new Command('list')
193
193
  byCategory[mod.category] = [];
194
194
  }
195
195
 
196
- byCategory[mod.category].push({
196
+ byCategory[mod.category]?.push({
197
197
  id: key,
198
198
  name: mod.name,
199
199
  description: mod.description,
@@ -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,8 @@ 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';
12
14
 
13
15
  const program = new Command();
14
16
 
@@ -41,4 +43,10 @@ program.addCommand(removeCommand);
41
43
  // Diagnose project
42
44
  program.addCommand(doctorCommand);
43
45
 
46
+ // Update modules
47
+ program.addCommand(updateCommand);
48
+
49
+ // Shell completion
50
+ program.addCommand(completionCommand);
51
+
44
52
  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
+ }