archetype-engine 2.0.1 → 2.1.0

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.
@@ -0,0 +1,520 @@
1
+ "use strict";
2
+ /**
3
+ * Test Generator
4
+ *
5
+ * Generates comprehensive Vitest test suites for each entity's tRPC router.
6
+ * Tests are generated based on entity configuration (fields, validations, relations, protection).
7
+ *
8
+ * Generated files:
9
+ * - tests/{entity}.test.ts - Full CRUD test suite with validation, auth, relations, filters
10
+ *
11
+ * Features:
12
+ * - CRUD operation tests (create, list, get, update, remove)
13
+ * - Validation tests (required fields, field constraints, type checking)
14
+ * - Authentication tests (protected operations require auth)
15
+ * - Relation tests (hasMany, belongsToMany associations)
16
+ * - Filter/search/pagination tests
17
+ * - Batch operation tests (createMany, updateMany, removeMany)
18
+ * - Soft delete tests (if enabled)
19
+ * - Computed field tests (if present)
20
+ *
21
+ * @module generators/test
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.testGenerator = void 0;
25
+ const utils_1 = require("../../../core/utils");
26
+ /**
27
+ * Check if entity has any protected operations
28
+ */
29
+ function hasProtection(protection) {
30
+ return Object.values(protection).some(v => v);
31
+ }
32
+ /**
33
+ * Get required fields from entity
34
+ */
35
+ function getRequiredFields(entity) {
36
+ return Object.entries(entity.fields).filter(([_, field]) => field.required && field.type !== 'computed');
37
+ }
38
+ /**
39
+ * Get optional fields from entity
40
+ */
41
+ function getOptionalFields(entity) {
42
+ return Object.entries(entity.fields).filter(([_, field]) => !field.required && field.type !== 'computed');
43
+ }
44
+ /**
45
+ * Get computed fields from entity
46
+ */
47
+ function getComputedFields(entity) {
48
+ return Object.entries(entity.fields).filter(([_, field]) => field.type === 'computed');
49
+ }
50
+ /**
51
+ * Get text fields for search testing
52
+ */
53
+ function getTextFields(entity) {
54
+ return Object.entries(entity.fields).filter(([_, field]) => field.type === 'text' && field.type !== 'computed');
55
+ }
56
+ /**
57
+ * Generate valid mock data for a field
58
+ */
59
+ function generateMockValue(fieldName, field) {
60
+ switch (field.type) {
61
+ case 'text':
62
+ // Check for specific validation types
63
+ if (field.validations.some(v => v.type === 'email')) {
64
+ return `'test-${fieldName}@example.com'`;
65
+ }
66
+ if (field.validations.some(v => v.type === 'url')) {
67
+ return `'https://example.com/${fieldName}'`;
68
+ }
69
+ if (field.enumValues) {
70
+ return `'${field.enumValues[0]}'`;
71
+ }
72
+ const minLength = field.validations.find(v => v.type === 'minLength');
73
+ if (minLength) {
74
+ const len = minLength.value + 5;
75
+ return `'${'x'.repeat(len)}'`;
76
+ }
77
+ return `'Test ${fieldName}'`;
78
+ case 'number':
79
+ const min = field.validations.find(v => v.type === 'min')?.value || 0;
80
+ const max = field.validations.find(v => v.type === 'max')?.value;
81
+ const isPositive = field.validations.some(v => v.type === 'positive');
82
+ const isInteger = field.validations.some(v => v.type === 'integer');
83
+ let value = min > 0 ? min + 10 : isPositive ? 10 : 42;
84
+ if (max && value > max)
85
+ value = max - 1;
86
+ if (isInteger)
87
+ value = Math.floor(value);
88
+ return String(value);
89
+ case 'boolean':
90
+ return 'true';
91
+ case 'date':
92
+ return `new Date().toISOString()`;
93
+ case 'enum':
94
+ return field.enumValues ? `'${field.enumValues[0]}'` : `'default'`;
95
+ default:
96
+ return `'test-value'`;
97
+ }
98
+ }
99
+ /**
100
+ * Generate invalid mock data for validation testing
101
+ */
102
+ function generateInvalidValue(fieldName, field) {
103
+ switch (field.type) {
104
+ case 'text':
105
+ if (field.validations.some(v => v.type === 'email')) {
106
+ return { value: `'invalid-email'`, reason: 'invalid email format' };
107
+ }
108
+ if (field.validations.some(v => v.type === 'url')) {
109
+ return { value: `'not-a-url'`, reason: 'invalid URL format' };
110
+ }
111
+ const minLength = field.validations.find(v => v.type === 'minLength');
112
+ if (minLength) {
113
+ return { value: `'x'`, reason: `below minimum length of ${minLength.value}` };
114
+ }
115
+ const maxLength = field.validations.find(v => v.type === 'maxLength');
116
+ if (maxLength) {
117
+ const len = maxLength.value + 10;
118
+ return { value: `'${'x'.repeat(len)}'`, reason: `exceeds maximum length of ${maxLength.value}` };
119
+ }
120
+ if (field.enumValues) {
121
+ return { value: `'invalid-enum'`, reason: `not in allowed values: ${field.enumValues.join(', ')}` };
122
+ }
123
+ return null;
124
+ case 'number':
125
+ const min = field.validations.find(v => v.type === 'min');
126
+ if (min) {
127
+ return { value: String(min.value - 10), reason: `below minimum of ${min.value}` };
128
+ }
129
+ const max = field.validations.find(v => v.type === 'max');
130
+ if (max) {
131
+ return { value: String(max.value + 10), reason: `exceeds maximum of ${max.value}` };
132
+ }
133
+ if (field.validations.some(v => v.type === 'integer')) {
134
+ return { value: '3.14', reason: 'not an integer' };
135
+ }
136
+ if (field.validations.some(v => v.type === 'positive')) {
137
+ return { value: '-5', reason: 'not positive' };
138
+ }
139
+ return null;
140
+ default:
141
+ return null;
142
+ }
143
+ }
144
+ /**
145
+ * Generate valid entity data for testing
146
+ */
147
+ function generateValidEntityData(entity) {
148
+ const fields = getRequiredFields(entity);
149
+ const assignments = fields.map(([name, field]) => ` ${name}: ${generateMockValue(name, field)},`);
150
+ return `{\n${assignments.join('\n')}\n }`;
151
+ }
152
+ /**
153
+ * Generate test file for an entity
154
+ */
155
+ function generateEntityTest(entity, manifest) {
156
+ const entityName = entity.name;
157
+ const routerName = (0, utils_1.toCamelCase)(entityName);
158
+ const hasAuth = hasProtection(entity.protected);
159
+ const requiredFields = getRequiredFields(entity);
160
+ const optionalFields = getOptionalFields(entity);
161
+ const computedFields = getComputedFields(entity);
162
+ const textFields = getTextFields(entity);
163
+ const hasSoftDelete = entity.behaviors.softDelete;
164
+ const hasTimestamps = entity.behaviors.timestamps;
165
+ const lines = [];
166
+ // Imports
167
+ lines.push(`import { describe, it, expect, beforeEach } from 'vitest'`);
168
+ lines.push(`import { appRouter } from '@/generated/trpc/routers'`);
169
+ lines.push(`import { createCallerFactory } from '@trpc/server'`);
170
+ lines.push(``);
171
+ lines.push(`// Create tRPC caller for testing`);
172
+ lines.push(`const createCaller = createCallerFactory(appRouter)`);
173
+ lines.push(``);
174
+ // Mock contexts
175
+ if (hasAuth) {
176
+ lines.push(`// Mock authenticated context`);
177
+ lines.push(`const mockAuthContext = {`);
178
+ lines.push(` session: {`);
179
+ lines.push(` user: { id: 'test-user-123', email: 'test@example.com', name: 'Test User' }`);
180
+ lines.push(` }`);
181
+ lines.push(`}`);
182
+ lines.push(``);
183
+ }
184
+ lines.push(`// Mock unauthenticated context`);
185
+ lines.push(`const mockPublicContext = {`);
186
+ lines.push(` session: null`);
187
+ lines.push(`}`);
188
+ lines.push(``);
189
+ // Test suite
190
+ lines.push(`describe('${entityName} Router', () => {`);
191
+ lines.push(` const publicCaller = createCaller(mockPublicContext)`);
192
+ if (hasAuth) {
193
+ lines.push(` const authCaller = createCaller(mockAuthContext)`);
194
+ }
195
+ lines.push(``);
196
+ // Valid test data
197
+ lines.push(` const validData = ${generateValidEntityData(entity)}`);
198
+ lines.push(``);
199
+ // CREATE tests
200
+ lines.push(` describe('create', () => {`);
201
+ if (entity.protected.create) {
202
+ lines.push(` it('should require authentication', async () => {`);
203
+ lines.push(` await expect(`);
204
+ lines.push(` publicCaller.${routerName}.create(validData)`);
205
+ lines.push(` ).rejects.toThrow(/UNAUTHORIZED|unauthorized/)`);
206
+ lines.push(` })`);
207
+ lines.push(``);
208
+ lines.push(` it('should create ${entityName} when authenticated', async () => {`);
209
+ lines.push(` const result = await authCaller.${routerName}.create(validData)`);
210
+ }
211
+ else {
212
+ lines.push(` it('should create ${entityName} with valid data', async () => {`);
213
+ lines.push(` const result = await publicCaller.${routerName}.create(validData)`);
214
+ }
215
+ lines.push(``);
216
+ lines.push(` expect(result).toBeDefined()`);
217
+ lines.push(` expect(result.id).toBeDefined()`);
218
+ // Check required fields
219
+ requiredFields.forEach(([name]) => {
220
+ lines.push(` expect(result.${name}).toBe(validData.${name})`);
221
+ });
222
+ // Check timestamps
223
+ if (hasTimestamps) {
224
+ lines.push(` expect(result.createdAt).toBeDefined()`);
225
+ lines.push(` expect(result.updatedAt).toBeDefined()`);
226
+ }
227
+ // Check computed fields
228
+ computedFields.forEach(([name, field]) => {
229
+ lines.push(` expect(result.${name}).toBeDefined() // computed field`);
230
+ });
231
+ lines.push(` })`);
232
+ lines.push(``);
233
+ // Validation tests for required fields
234
+ requiredFields.forEach(([fieldName, field]) => {
235
+ lines.push(` it('should reject missing ${fieldName}', async () => {`);
236
+ lines.push(` const invalidData = { ...validData }`);
237
+ lines.push(` delete invalidData.${fieldName}`);
238
+ lines.push(``);
239
+ const caller = entity.protected.create ? 'authCaller' : 'publicCaller';
240
+ lines.push(` await expect(`);
241
+ lines.push(` ${caller}.${routerName}.create(invalidData as any)`);
242
+ lines.push(` ).rejects.toThrow()`);
243
+ lines.push(` })`);
244
+ lines.push(``);
245
+ // Field-specific validation tests
246
+ const invalidCase = generateInvalidValue(fieldName, field);
247
+ if (invalidCase) {
248
+ const validationCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
249
+ lines.push(` it('should reject invalid ${fieldName} (${invalidCase.reason})', async () => {`);
250
+ lines.push(` const invalidData = { ...validData, ${fieldName}: ${invalidCase.value} }`);
251
+ lines.push(``);
252
+ lines.push(` await expect(`);
253
+ lines.push(` ${validationCaller}.${routerName}.create(invalidData)`);
254
+ lines.push(` ).rejects.toThrow()`);
255
+ lines.push(` })`);
256
+ lines.push(``);
257
+ }
258
+ });
259
+ lines.push(` })`);
260
+ lines.push(``);
261
+ // LIST tests
262
+ lines.push(` describe('list', () => {`);
263
+ if (entity.protected.list) {
264
+ lines.push(` it('should require authentication', async () => {`);
265
+ lines.push(` await expect(`);
266
+ lines.push(` publicCaller.${routerName}.list({})`);
267
+ lines.push(` ).rejects.toThrow(/UNAUTHORIZED|unauthorized/)`);
268
+ lines.push(` })`);
269
+ lines.push(``);
270
+ lines.push(` it('should return paginated results when authenticated', async () => {`);
271
+ lines.push(` const result = await authCaller.${routerName}.list({ page: 1, limit: 10 })`);
272
+ }
273
+ else {
274
+ lines.push(` it('should return paginated results', async () => {`);
275
+ lines.push(` const result = await publicCaller.${routerName}.list({ page: 1, limit: 10 })`);
276
+ }
277
+ lines.push(``);
278
+ lines.push(` expect(result).toBeDefined()`);
279
+ lines.push(` expect(result.items).toBeInstanceOf(Array)`);
280
+ lines.push(` expect(result.total).toBeTypeOf('number')`);
281
+ lines.push(` expect(result.page).toBe(1)`);
282
+ lines.push(` expect(result.limit).toBe(10)`);
283
+ lines.push(` })`);
284
+ lines.push(``);
285
+ // Filter tests
286
+ if (textFields.length > 0) {
287
+ const [firstTextField, _] = textFields[0];
288
+ const caller = entity.protected.list ? 'authCaller' : 'publicCaller';
289
+ lines.push(` it('should filter by ${firstTextField}', async () => {`);
290
+ lines.push(` const result = await ${caller}.${routerName}.list({`);
291
+ lines.push(` where: { ${firstTextField}: { contains: 'test' } }`);
292
+ lines.push(` })`);
293
+ lines.push(``);
294
+ lines.push(` expect(result.items).toBeInstanceOf(Array)`);
295
+ lines.push(` })`);
296
+ lines.push(``);
297
+ lines.push(` it('should search across text fields', async () => {`);
298
+ lines.push(` const result = await ${caller}.${routerName}.list({`);
299
+ lines.push(` search: 'test'`);
300
+ lines.push(` })`);
301
+ lines.push(``);
302
+ lines.push(` expect(result.items).toBeInstanceOf(Array)`);
303
+ lines.push(` })`);
304
+ lines.push(``);
305
+ }
306
+ // Pagination test
307
+ const listCaller = entity.protected.list ? 'authCaller' : 'publicCaller';
308
+ lines.push(` it('should support pagination', async () => {`);
309
+ lines.push(` const page1 = await ${listCaller}.${routerName}.list({ page: 1, limit: 5 })`);
310
+ lines.push(` const page2 = await ${listCaller}.${routerName}.list({ page: 2, limit: 5 })`);
311
+ lines.push(``);
312
+ lines.push(` expect(page1.page).toBe(1)`);
313
+ lines.push(` expect(page2.page).toBe(2)`);
314
+ lines.push(` })`);
315
+ lines.push(``);
316
+ lines.push(` })`);
317
+ lines.push(``);
318
+ // GET tests
319
+ lines.push(` describe('get', () => {`);
320
+ if (entity.protected.get) {
321
+ lines.push(` it('should require authentication', async () => {`);
322
+ lines.push(` await expect(`);
323
+ lines.push(` publicCaller.${routerName}.get({ id: 'test-id' })`);
324
+ lines.push(` ).rejects.toThrow(/UNAUTHORIZED|unauthorized/)`);
325
+ lines.push(` })`);
326
+ lines.push(``);
327
+ lines.push(` it('should return entity by ID when authenticated', async () => {`);
328
+ lines.push(` const created = await authCaller.${routerName}.create(validData)`);
329
+ lines.push(` const result = await authCaller.${routerName}.get({ id: created.id })`);
330
+ }
331
+ else {
332
+ lines.push(` it('should return entity by ID', async () => {`);
333
+ const createCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
334
+ lines.push(` const created = await ${createCaller}.${routerName}.create(validData)`);
335
+ lines.push(` const result = await publicCaller.${routerName}.get({ id: created.id })`);
336
+ }
337
+ lines.push(``);
338
+ lines.push(` expect(result).toBeDefined()`);
339
+ lines.push(` expect(result.id).toBe(created.id)`);
340
+ lines.push(` })`);
341
+ lines.push(``);
342
+ const getCaller = entity.protected.get ? 'authCaller' : 'publicCaller';
343
+ lines.push(` it('should throw error for non-existent ID', async () => {`);
344
+ lines.push(` await expect(`);
345
+ lines.push(` ${getCaller}.${routerName}.get({ id: 'non-existent-id' })`);
346
+ lines.push(` ).rejects.toThrow()`);
347
+ lines.push(` })`);
348
+ lines.push(``);
349
+ lines.push(` })`);
350
+ lines.push(``);
351
+ // UPDATE tests
352
+ lines.push(` describe('update', () => {`);
353
+ if (entity.protected.update) {
354
+ lines.push(` it('should require authentication', async () => {`);
355
+ const createCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
356
+ lines.push(` const created = await ${createCaller}.${routerName}.create(validData)`);
357
+ lines.push(``);
358
+ lines.push(` await expect(`);
359
+ lines.push(` publicCaller.${routerName}.update({ id: created.id, data: validData })`);
360
+ lines.push(` ).rejects.toThrow(/UNAUTHORIZED|unauthorized/)`);
361
+ lines.push(` })`);
362
+ lines.push(``);
363
+ lines.push(` it('should update ${entityName} when authenticated', async () => {`);
364
+ lines.push(` const created = await authCaller.${routerName}.create(validData)`);
365
+ }
366
+ else {
367
+ lines.push(` it('should update ${entityName}', async () => {`);
368
+ const createCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
369
+ lines.push(` const created = await ${createCaller}.${routerName}.create(validData)`);
370
+ }
371
+ // Generate update data (modify first field)
372
+ if (requiredFields.length > 0) {
373
+ const [firstField, firstFieldConfig] = requiredFields[0];
374
+ const newValue = generateMockValue('updated', firstFieldConfig);
375
+ const updateCaller = entity.protected.update ? 'authCaller' : 'publicCaller';
376
+ lines.push(` const updateData = { ${firstField}: ${newValue} }`);
377
+ lines.push(` const result = await ${updateCaller}.${routerName}.update({ id: created.id, data: updateData })`);
378
+ lines.push(``);
379
+ lines.push(` expect(result.${firstField}).toBe(updateData.${firstField})`);
380
+ if (hasTimestamps) {
381
+ lines.push(` expect(new Date(result.updatedAt).getTime()).toBeGreaterThan(new Date(created.updatedAt).getTime())`);
382
+ }
383
+ }
384
+ lines.push(` })`);
385
+ lines.push(``);
386
+ lines.push(` })`);
387
+ lines.push(``);
388
+ // REMOVE tests
389
+ lines.push(` describe('remove', () => {`);
390
+ if (entity.protected.remove) {
391
+ lines.push(` it('should require authentication', async () => {`);
392
+ const createCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
393
+ lines.push(` const created = await ${createCaller}.${routerName}.create(validData)`);
394
+ lines.push(``);
395
+ lines.push(` await expect(`);
396
+ lines.push(` publicCaller.${routerName}.remove({ id: created.id })`);
397
+ lines.push(` ).rejects.toThrow(/UNAUTHORIZED|unauthorized/)`);
398
+ lines.push(` })`);
399
+ lines.push(``);
400
+ lines.push(` it('should remove ${entityName} when authenticated', async () => {`);
401
+ lines.push(` const created = await authCaller.${routerName}.create(validData)`);
402
+ lines.push(` const result = await authCaller.${routerName}.remove({ id: created.id })`);
403
+ }
404
+ else {
405
+ lines.push(` it('should remove ${entityName}', async () => {`);
406
+ const createCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
407
+ lines.push(` const created = await ${createCaller}.${routerName}.create(validData)`);
408
+ lines.push(` const result = await publicCaller.${routerName}.remove({ id: created.id })`);
409
+ }
410
+ lines.push(``);
411
+ lines.push(` expect(result).toBeDefined()`);
412
+ lines.push(` expect(result.id).toBe(created.id)`);
413
+ if (hasSoftDelete) {
414
+ lines.push(` expect(result.deletedAt).toBeDefined() // soft delete`);
415
+ }
416
+ lines.push(` })`);
417
+ lines.push(``);
418
+ lines.push(` })`);
419
+ lines.push(``);
420
+ // BATCH OPERATIONS tests
421
+ lines.push(` describe('batch operations', () => {`);
422
+ const batchCaller = entity.protected.create ? 'authCaller' : 'publicCaller';
423
+ lines.push(` it('should create multiple ${entityName}s', async () => {`);
424
+ lines.push(` const items = [validData, validData, validData]`);
425
+ lines.push(` const result = await ${batchCaller}.${routerName}.createMany({ items })`);
426
+ lines.push(``);
427
+ lines.push(` expect(result.created).toHaveLength(3)`);
428
+ lines.push(` expect(result.count).toBe(3)`);
429
+ lines.push(` })`);
430
+ lines.push(``);
431
+ lines.push(` it('should update multiple ${entityName}s', async () => {`);
432
+ lines.push(` const created = await ${batchCaller}.${routerName}.createMany({ items: [validData, validData] })`);
433
+ if (requiredFields.length > 0) {
434
+ const [firstField, firstFieldConfig] = requiredFields[0];
435
+ const newValue = generateMockValue('batch-updated', firstFieldConfig);
436
+ lines.push(` const updates = created.created.map(item => ({`);
437
+ lines.push(` id: item.id,`);
438
+ lines.push(` data: { ${firstField}: ${newValue} }`);
439
+ lines.push(` }))`);
440
+ }
441
+ else {
442
+ lines.push(` const updates = created.created.map(item => ({ id: item.id, data: {} }))`);
443
+ }
444
+ const updateManyCaller = entity.protected.update ? 'authCaller' : 'publicCaller';
445
+ lines.push(` const result = await ${updateManyCaller}.${routerName}.updateMany({ items: updates })`);
446
+ lines.push(``);
447
+ lines.push(` expect(result.count).toBe(2)`);
448
+ lines.push(` })`);
449
+ lines.push(``);
450
+ lines.push(` it('should remove multiple ${entityName}s', async () => {`);
451
+ lines.push(` const created = await ${batchCaller}.${routerName}.createMany({ items: [validData, validData] })`);
452
+ lines.push(` const ids = created.created.map(item => item.id)`);
453
+ const removeManyCaller = entity.protected.remove ? 'authCaller' : 'publicCaller';
454
+ lines.push(` const result = await ${removeManyCaller}.${routerName}.removeMany({ ids })`);
455
+ lines.push(``);
456
+ lines.push(` expect(result.count).toBe(2)`);
457
+ lines.push(` })`);
458
+ lines.push(``);
459
+ lines.push(` })`);
460
+ // Close test suite
461
+ lines.push(`})`);
462
+ lines.push(``);
463
+ return lines.join('\n');
464
+ }
465
+ /**
466
+ * Test generator - creates Vitest test files for tRPC routers
467
+ */
468
+ exports.testGenerator = {
469
+ name: 'vitest-tests',
470
+ description: 'Generates comprehensive test suites for tRPC routers',
471
+ generate(manifest, ctx) {
472
+ const files = [];
473
+ // Generate test file for each entity
474
+ for (const entity of manifest.entities) {
475
+ files.push({
476
+ path: `tests/${(0, utils_1.toCamelCase)(entity.name)}.test.ts`,
477
+ content: generateEntityTest(entity, manifest),
478
+ });
479
+ }
480
+ // Generate test setup file
481
+ files.push({
482
+ path: 'tests/setup.ts',
483
+ content: generateTestSetup(manifest),
484
+ });
485
+ return files;
486
+ },
487
+ };
488
+ /**
489
+ * Generate test setup/configuration file
490
+ */
491
+ function generateTestSetup(manifest) {
492
+ const lines = [];
493
+ lines.push(`/**`);
494
+ lines.push(` * Test Setup`);
495
+ lines.push(` * `);
496
+ lines.push(` * Global test configuration and utilities.`);
497
+ lines.push(` */`);
498
+ lines.push(``);
499
+ lines.push(`import { beforeAll, afterAll, afterEach } from 'vitest'`);
500
+ lines.push(``);
501
+ lines.push(`// Setup test database connection`);
502
+ lines.push(`beforeAll(async () => {`);
503
+ lines.push(` // TODO: Initialize test database`);
504
+ lines.push(` // For SQLite: create in-memory or temp file`);
505
+ lines.push(` // For PostgreSQL: create test database`);
506
+ lines.push(`})`);
507
+ lines.push(``);
508
+ lines.push(`// Clean up after each test`);
509
+ lines.push(`afterEach(async () => {`);
510
+ lines.push(` // TODO: Clear test data`);
511
+ lines.push(` // Truncate tables or reset database`);
512
+ lines.push(`})`);
513
+ lines.push(``);
514
+ lines.push(`// Cleanup after all tests`);
515
+ lines.push(`afterAll(async () => {`);
516
+ lines.push(` // TODO: Close database connection`);
517
+ lines.push(`})`);
518
+ lines.push(``);
519
+ return lines.join('\n');
520
+ }
@@ -23,7 +23,11 @@ import type { Template } from '../../template/types';
23
23
  * 4. serviceGenerator - API client (only for external entities)
24
24
  * 5. apiGenerator - tRPC routers (adapts to source type)
25
25
  * 6. hooksGenerator - React hooks (always runs)
26
- * 7. i18nGenerator - Translation files (only if i18n configured)
26
+ * 7. crudHooksGenerator - Business logic hooks (only if hooks enabled)
27
+ * 8. i18nGenerator - Translation files (only if i18n configured)
28
+ * 9. testGenerator - Vitest test suites (always runs)
29
+ * 10. openapiGenerator - OpenAPI spec and Swagger UI (always runs)
30
+ * 11. seedGenerator - Seed data for development (always runs)
27
31
  */
28
32
  export declare const template: Template;
29
33
  export default template;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/nextjs-drizzle-trpc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAUpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,EAAE,QAgCtB,CAAA;AAED,eAAe,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/templates/nextjs-drizzle-trpc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAapD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ,EAAE,QAmCtB,CAAA;AAED,eAAe,QAAQ,CAAA"}
@@ -23,6 +23,9 @@ const api_1 = require("./generators/api");
23
23
  const hooks_1 = require("./generators/hooks");
24
24
  const crud_hooks_1 = require("./generators/crud-hooks");
25
25
  const i18n_1 = require("./generators/i18n");
26
+ const test_1 = require("./generators/test");
27
+ const openapi_1 = require("./generators/openapi");
28
+ const seed_1 = require("./generators/seed");
26
29
  /**
27
30
  * Template definition for Next.js + Drizzle + tRPC stack
28
31
  *
@@ -33,7 +36,11 @@ const i18n_1 = require("./generators/i18n");
33
36
  * 4. serviceGenerator - API client (only for external entities)
34
37
  * 5. apiGenerator - tRPC routers (adapts to source type)
35
38
  * 6. hooksGenerator - React hooks (always runs)
36
- * 7. i18nGenerator - Translation files (only if i18n configured)
39
+ * 7. crudHooksGenerator - Business logic hooks (only if hooks enabled)
40
+ * 8. i18nGenerator - Translation files (only if i18n configured)
41
+ * 9. testGenerator - Vitest test suites (always runs)
42
+ * 10. openapiGenerator - OpenAPI spec and Swagger UI (always runs)
43
+ * 11. seedGenerator - Seed data for development (always runs)
37
44
  */
38
45
  exports.template = {
39
46
  meta: {
@@ -66,6 +73,9 @@ exports.template = {
66
73
  hooks_1.hooksGenerator, // Always runs
67
74
  crud_hooks_1.crudHooksGenerator, // Only if hooks enabled
68
75
  i18n_1.i18nGenerator, // Only if i18n enabled
76
+ test_1.testGenerator, // Always runs - generates test suites
77
+ openapi_1.openapiGenerator, // Always runs - generates API docs
78
+ seed_1.seedGenerator, // Always runs - generates seed data
69
79
  ],
70
80
  };
71
81
  exports.default = exports.template;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archetype-engine",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Type-safe backend generator for Next.js. Define entities once, get Drizzle schemas, tRPC APIs, Zod validation, and React hooks instantly.",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "bin": {
20
- "archetype": "dist/src/cli.js"
20
+ "archetype": "dist/src/cli.js",
21
+ "archetype-mcp": "dist/src/mcp-server.js"
21
22
  },
22
23
  "files": [
23
24
  "dist",