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.
- package/dist/src/cli.js +6 -0
- package/dist/src/init/templates.d.ts +2 -0
- package/dist/src/init/templates.d.ts.map +1 -1
- package/dist/src/init/templates.js +418 -0
- package/dist/src/mcp-server.d.ts +15 -0
- package/dist/src/mcp-server.d.ts.map +1 -0
- package/dist/src/mcp-server.js +271 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.d.ts +3 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.d.ts.map +1 -1
- package/dist/src/templates/nextjs-drizzle-trpc/generators/index.js +7 -1
- package/dist/src/templates/nextjs-drizzle-trpc/generators/openapi.d.ts +26 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/openapi.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/openapi.js +690 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/seed.d.ts +27 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/seed.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/seed.js +407 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/test.d.ts +27 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/test.d.ts.map +1 -0
- package/dist/src/templates/nextjs-drizzle-trpc/generators/test.js +520 -0
- package/dist/src/templates/nextjs-drizzle-trpc/index.d.ts +5 -1
- package/dist/src/templates/nextjs-drizzle-trpc/index.d.ts.map +1 -1
- package/dist/src/templates/nextjs-drizzle-trpc/index.js +11 -1
- package/package.json +3 -2
|
@@ -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.
|
|
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;
|
|
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.
|
|
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
|
|
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",
|