archetype-engine 2.0.0 → 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/README.md +1 -1
- package/dist/src/cli.js +6 -0
- package/dist/src/fields.d.ts +9 -9
- package/dist/src/fields.d.ts.map +1 -1
- package/dist/src/fields.js +8 -8
- 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,690 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI/Swagger Documentation Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates OpenAPI 3.0 specification from entity definitions and tRPC routers.
|
|
6
|
+
* Provides interactive API documentation via Swagger UI.
|
|
7
|
+
*
|
|
8
|
+
* Generated files:
|
|
9
|
+
* - docs/openapi.json - OpenAPI 3.0 specification
|
|
10
|
+
* - docs/swagger.html - Interactive Swagger UI
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Complete API documentation for all CRUD endpoints
|
|
14
|
+
* - Request/response schemas derived from Zod validation
|
|
15
|
+
* - Authentication/authorization documentation
|
|
16
|
+
* - Filter, search, and pagination parameter docs
|
|
17
|
+
* - Batch operation endpoints
|
|
18
|
+
* - Interactive testing via Swagger UI
|
|
19
|
+
*
|
|
20
|
+
* @module generators/openapi
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.openapiGenerator = void 0;
|
|
24
|
+
const utils_1 = require("../../../core/utils");
|
|
25
|
+
/**
|
|
26
|
+
* Map field type to OpenAPI type
|
|
27
|
+
*/
|
|
28
|
+
function getOpenAPIType(field) {
|
|
29
|
+
switch (field.type) {
|
|
30
|
+
case 'text':
|
|
31
|
+
if (field.validations.some(v => v.type === 'email')) {
|
|
32
|
+
return { type: 'string', format: 'email' };
|
|
33
|
+
}
|
|
34
|
+
if (field.validations.some(v => v.type === 'url')) {
|
|
35
|
+
return { type: 'string', format: 'uri' };
|
|
36
|
+
}
|
|
37
|
+
if (field.enumValues) {
|
|
38
|
+
return { type: 'string' };
|
|
39
|
+
}
|
|
40
|
+
return { type: 'string' };
|
|
41
|
+
case 'number':
|
|
42
|
+
if (field.validations.some(v => v.type === 'integer')) {
|
|
43
|
+
return { type: 'integer', format: 'int32' };
|
|
44
|
+
}
|
|
45
|
+
return { type: 'number', format: 'double' };
|
|
46
|
+
case 'boolean':
|
|
47
|
+
return { type: 'boolean' };
|
|
48
|
+
case 'date':
|
|
49
|
+
return { type: 'string', format: 'date-time' };
|
|
50
|
+
case 'enum':
|
|
51
|
+
return { type: 'string' };
|
|
52
|
+
default:
|
|
53
|
+
return { type: 'string' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate OpenAPI schema for a field
|
|
58
|
+
*/
|
|
59
|
+
function generateFieldSchema(fieldName, field) {
|
|
60
|
+
const schema = getOpenAPIType(field);
|
|
61
|
+
// Add enum values
|
|
62
|
+
if (field.enumValues) {
|
|
63
|
+
schema.enum = field.enumValues;
|
|
64
|
+
}
|
|
65
|
+
// Add description from label
|
|
66
|
+
if (field.label) {
|
|
67
|
+
schema.description = field.label;
|
|
68
|
+
}
|
|
69
|
+
// Add validation constraints
|
|
70
|
+
if (field.type === 'text') {
|
|
71
|
+
const minLength = field.validations.find(v => v.type === 'minLength');
|
|
72
|
+
const maxLength = field.validations.find(v => v.type === 'maxLength');
|
|
73
|
+
if (minLength)
|
|
74
|
+
schema.minLength = minLength.value;
|
|
75
|
+
if (maxLength)
|
|
76
|
+
schema.maxLength = maxLength.value;
|
|
77
|
+
}
|
|
78
|
+
if (field.type === 'number') {
|
|
79
|
+
const min = field.validations.find(v => v.type === 'min');
|
|
80
|
+
const max = field.validations.find(v => v.type === 'max');
|
|
81
|
+
if (min)
|
|
82
|
+
schema.minimum = min.value;
|
|
83
|
+
if (max)
|
|
84
|
+
schema.maximum = max.value;
|
|
85
|
+
}
|
|
86
|
+
// Add default value
|
|
87
|
+
if (field.default !== undefined) {
|
|
88
|
+
schema.default = field.default;
|
|
89
|
+
}
|
|
90
|
+
return schema;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate OpenAPI schema for entity create input
|
|
94
|
+
*/
|
|
95
|
+
function generateCreateSchema(entity) {
|
|
96
|
+
const properties = {};
|
|
97
|
+
const required = [];
|
|
98
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
99
|
+
// Skip computed fields
|
|
100
|
+
if (field.type === 'computed')
|
|
101
|
+
continue;
|
|
102
|
+
properties[fieldName] = generateFieldSchema(fieldName, field);
|
|
103
|
+
if (field.required) {
|
|
104
|
+
required.push(fieldName);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties,
|
|
110
|
+
required: required.length > 0 ? required : undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generate OpenAPI schema for entity (full model with ID and timestamps)
|
|
115
|
+
*/
|
|
116
|
+
function generateEntitySchema(entity) {
|
|
117
|
+
const properties = {
|
|
118
|
+
id: { type: 'string', description: 'Unique identifier' },
|
|
119
|
+
};
|
|
120
|
+
// Add all fields including computed
|
|
121
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
122
|
+
properties[fieldName] = generateFieldSchema(fieldName, field);
|
|
123
|
+
}
|
|
124
|
+
// Add timestamps if enabled
|
|
125
|
+
if (entity.behaviors.timestamps) {
|
|
126
|
+
properties.createdAt = { type: 'string', format: 'date-time' };
|
|
127
|
+
properties.updatedAt = { type: 'string', format: 'date-time' };
|
|
128
|
+
}
|
|
129
|
+
// Add soft delete field
|
|
130
|
+
if (entity.behaviors.softDelete) {
|
|
131
|
+
properties.deletedAt = {
|
|
132
|
+
type: 'string',
|
|
133
|
+
format: 'date-time',
|
|
134
|
+
nullable: true,
|
|
135
|
+
description: 'Soft delete timestamp'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties,
|
|
141
|
+
required: ['id'],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate filter parameter schema for a field
|
|
146
|
+
*/
|
|
147
|
+
function generateFilterSchema(fieldName, field) {
|
|
148
|
+
const baseType = getOpenAPIType(field);
|
|
149
|
+
switch (field.type) {
|
|
150
|
+
case 'text':
|
|
151
|
+
return {
|
|
152
|
+
oneOf: [
|
|
153
|
+
{ type: 'string' },
|
|
154
|
+
{
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
eq: { type: 'string' },
|
|
158
|
+
ne: { type: 'string' },
|
|
159
|
+
contains: { type: 'string' },
|
|
160
|
+
startsWith: { type: 'string' },
|
|
161
|
+
endsWith: { type: 'string' },
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
case 'number':
|
|
167
|
+
return {
|
|
168
|
+
oneOf: [
|
|
169
|
+
{ type: 'number' },
|
|
170
|
+
{
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
eq: { type: 'number' },
|
|
174
|
+
ne: { type: 'number' },
|
|
175
|
+
gt: { type: 'number' },
|
|
176
|
+
gte: { type: 'number' },
|
|
177
|
+
lt: { type: 'number' },
|
|
178
|
+
lte: { type: 'number' },
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
case 'boolean':
|
|
184
|
+
return { type: 'boolean' };
|
|
185
|
+
case 'date':
|
|
186
|
+
return {
|
|
187
|
+
oneOf: [
|
|
188
|
+
{ type: 'string', format: 'date-time' },
|
|
189
|
+
{
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
eq: { type: 'string', format: 'date-time' },
|
|
193
|
+
ne: { type: 'string', format: 'date-time' },
|
|
194
|
+
gt: { type: 'string', format: 'date-time' },
|
|
195
|
+
gte: { type: 'string', format: 'date-time' },
|
|
196
|
+
lt: { type: 'string', format: 'date-time' },
|
|
197
|
+
lte: { type: 'string', format: 'date-time' },
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
};
|
|
202
|
+
default:
|
|
203
|
+
return baseType;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generate security requirement for operation
|
|
208
|
+
*/
|
|
209
|
+
function getSecurityRequirement(isProtected) {
|
|
210
|
+
return isProtected ? [{ bearerAuth: [] }] : [];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Generate OpenAPI paths for an entity
|
|
214
|
+
*/
|
|
215
|
+
function generateEntityPaths(entity, baseUrl) {
|
|
216
|
+
const routerName = (0, utils_1.toCamelCase)(entity.name);
|
|
217
|
+
const paths = {};
|
|
218
|
+
// List operation
|
|
219
|
+
paths[`${baseUrl}/${routerName}.list`] = {
|
|
220
|
+
get: {
|
|
221
|
+
summary: `List ${entity.name} records`,
|
|
222
|
+
description: `Retrieve a paginated list of ${entity.name} records with optional filtering and search`,
|
|
223
|
+
operationId: `${routerName}List`,
|
|
224
|
+
tags: [entity.name],
|
|
225
|
+
security: getSecurityRequirement(entity.protected.list),
|
|
226
|
+
parameters: [
|
|
227
|
+
{
|
|
228
|
+
name: 'page',
|
|
229
|
+
in: 'query',
|
|
230
|
+
schema: { type: 'integer', minimum: 1, default: 1 },
|
|
231
|
+
description: 'Page number for pagination'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'limit',
|
|
235
|
+
in: 'query',
|
|
236
|
+
schema: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
|
237
|
+
description: 'Number of items per page'
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'search',
|
|
241
|
+
in: 'query',
|
|
242
|
+
schema: { type: 'string' },
|
|
243
|
+
description: 'Search across all text fields'
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
responses: {
|
|
247
|
+
'200': {
|
|
248
|
+
description: 'Successful response',
|
|
249
|
+
content: {
|
|
250
|
+
'application/json': {
|
|
251
|
+
schema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
items: {
|
|
255
|
+
type: 'array',
|
|
256
|
+
items: { $ref: `#/components/schemas/${entity.name}` }
|
|
257
|
+
},
|
|
258
|
+
total: { type: 'integer', description: 'Total number of records' },
|
|
259
|
+
page: { type: 'integer', description: 'Current page number' },
|
|
260
|
+
limit: { type: 'integer', description: 'Items per page' },
|
|
261
|
+
hasMore: { type: 'boolean', description: 'Whether more pages exist' },
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
'401': entity.protected.list ? { description: 'Unauthorized' } : undefined,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
// Get operation
|
|
272
|
+
paths[`${baseUrl}/${routerName}.get`] = {
|
|
273
|
+
get: {
|
|
274
|
+
summary: `Get ${entity.name} by ID`,
|
|
275
|
+
description: `Retrieve a single ${entity.name} record by its unique identifier`,
|
|
276
|
+
operationId: `${routerName}Get`,
|
|
277
|
+
tags: [entity.name],
|
|
278
|
+
security: getSecurityRequirement(entity.protected.get),
|
|
279
|
+
parameters: [
|
|
280
|
+
{
|
|
281
|
+
name: 'id',
|
|
282
|
+
in: 'query',
|
|
283
|
+
required: true,
|
|
284
|
+
schema: { type: 'string' },
|
|
285
|
+
description: 'Unique identifier'
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
responses: {
|
|
289
|
+
'200': {
|
|
290
|
+
description: 'Successful response',
|
|
291
|
+
content: {
|
|
292
|
+
'application/json': {
|
|
293
|
+
schema: { $ref: `#/components/schemas/${entity.name}` }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
'401': entity.protected.get ? { description: 'Unauthorized' } : undefined,
|
|
298
|
+
'404': { description: 'Record not found' },
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
// Create operation
|
|
303
|
+
paths[`${baseUrl}/${routerName}.create`] = {
|
|
304
|
+
post: {
|
|
305
|
+
summary: `Create ${entity.name}`,
|
|
306
|
+
description: `Create a new ${entity.name} record`,
|
|
307
|
+
operationId: `${routerName}Create`,
|
|
308
|
+
tags: [entity.name],
|
|
309
|
+
security: getSecurityRequirement(entity.protected.create),
|
|
310
|
+
requestBody: {
|
|
311
|
+
required: true,
|
|
312
|
+
content: {
|
|
313
|
+
'application/json': {
|
|
314
|
+
schema: { $ref: `#/components/schemas/${entity.name}CreateInput` }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
responses: {
|
|
319
|
+
'201': {
|
|
320
|
+
description: 'Successfully created',
|
|
321
|
+
content: {
|
|
322
|
+
'application/json': {
|
|
323
|
+
schema: { $ref: `#/components/schemas/${entity.name}` }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
'400': { description: 'Validation error' },
|
|
328
|
+
'401': entity.protected.create ? { description: 'Unauthorized' } : undefined,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
// Update operation
|
|
333
|
+
paths[`${baseUrl}/${routerName}.update`] = {
|
|
334
|
+
patch: {
|
|
335
|
+
summary: `Update ${entity.name}`,
|
|
336
|
+
description: `Update an existing ${entity.name} record`,
|
|
337
|
+
operationId: `${routerName}Update`,
|
|
338
|
+
tags: [entity.name],
|
|
339
|
+
security: getSecurityRequirement(entity.protected.update),
|
|
340
|
+
requestBody: {
|
|
341
|
+
required: true,
|
|
342
|
+
content: {
|
|
343
|
+
'application/json': {
|
|
344
|
+
schema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
id: { type: 'string', description: 'Record ID to update' },
|
|
348
|
+
data: { $ref: `#/components/schemas/${entity.name}UpdateInput` }
|
|
349
|
+
},
|
|
350
|
+
required: ['id', 'data']
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
responses: {
|
|
356
|
+
'200': {
|
|
357
|
+
description: 'Successfully updated',
|
|
358
|
+
content: {
|
|
359
|
+
'application/json': {
|
|
360
|
+
schema: { $ref: `#/components/schemas/${entity.name}` }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
'400': { description: 'Validation error' },
|
|
365
|
+
'401': entity.protected.update ? { description: 'Unauthorized' } : undefined,
|
|
366
|
+
'404': { description: 'Record not found' },
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
// Remove operation
|
|
371
|
+
paths[`${baseUrl}/${routerName}.remove`] = {
|
|
372
|
+
delete: {
|
|
373
|
+
summary: `Delete ${entity.name}`,
|
|
374
|
+
description: entity.behaviors.softDelete
|
|
375
|
+
? `Soft delete a ${entity.name} record (sets deletedAt timestamp)`
|
|
376
|
+
: `Permanently delete a ${entity.name} record`,
|
|
377
|
+
operationId: `${routerName}Remove`,
|
|
378
|
+
tags: [entity.name],
|
|
379
|
+
security: getSecurityRequirement(entity.protected.remove),
|
|
380
|
+
parameters: [
|
|
381
|
+
{
|
|
382
|
+
name: 'id',
|
|
383
|
+
in: 'query',
|
|
384
|
+
required: true,
|
|
385
|
+
schema: { type: 'string' },
|
|
386
|
+
description: 'Record ID to delete'
|
|
387
|
+
}
|
|
388
|
+
],
|
|
389
|
+
responses: {
|
|
390
|
+
'200': {
|
|
391
|
+
description: 'Successfully deleted',
|
|
392
|
+
content: {
|
|
393
|
+
'application/json': {
|
|
394
|
+
schema: { $ref: `#/components/schemas/${entity.name}` }
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
'401': entity.protected.remove ? { description: 'Unauthorized' } : undefined,
|
|
399
|
+
'404': { description: 'Record not found' },
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
// Batch operations
|
|
404
|
+
paths[`${baseUrl}/${routerName}.createMany`] = {
|
|
405
|
+
post: {
|
|
406
|
+
summary: `Bulk create ${entity.name} records`,
|
|
407
|
+
description: `Create multiple ${entity.name} records in a single request (max 100)`,
|
|
408
|
+
operationId: `${routerName}CreateMany`,
|
|
409
|
+
tags: [entity.name],
|
|
410
|
+
security: getSecurityRequirement(entity.protected.create),
|
|
411
|
+
requestBody: {
|
|
412
|
+
required: true,
|
|
413
|
+
content: {
|
|
414
|
+
'application/json': {
|
|
415
|
+
schema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
items: {
|
|
419
|
+
type: 'array',
|
|
420
|
+
items: { $ref: `#/components/schemas/${entity.name}CreateInput` },
|
|
421
|
+
maxItems: 100
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
required: ['items']
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
responses: {
|
|
430
|
+
'201': {
|
|
431
|
+
description: 'Successfully created',
|
|
432
|
+
content: {
|
|
433
|
+
'application/json': {
|
|
434
|
+
schema: {
|
|
435
|
+
type: 'object',
|
|
436
|
+
properties: {
|
|
437
|
+
created: {
|
|
438
|
+
type: 'array',
|
|
439
|
+
items: { $ref: `#/components/schemas/${entity.name}` }
|
|
440
|
+
},
|
|
441
|
+
count: { type: 'integer' }
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
'400': { description: 'Validation error' },
|
|
448
|
+
'401': entity.protected.create ? { description: 'Unauthorized' } : undefined,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
return paths;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Generate complete OpenAPI specification
|
|
456
|
+
*/
|
|
457
|
+
function generateOpenAPISpec(manifest) {
|
|
458
|
+
const baseUrl = '/api/trpc';
|
|
459
|
+
const spec = {
|
|
460
|
+
openapi: '3.0.0',
|
|
461
|
+
info: {
|
|
462
|
+
title: 'Generated API Documentation',
|
|
463
|
+
description: 'Auto-generated API documentation from Archetype entity definitions',
|
|
464
|
+
version: '1.0.0',
|
|
465
|
+
},
|
|
466
|
+
servers: [
|
|
467
|
+
{ url: 'http://localhost:3000', description: 'Development server' },
|
|
468
|
+
{ url: 'https://api.example.com', description: 'Production server' },
|
|
469
|
+
],
|
|
470
|
+
paths: {},
|
|
471
|
+
components: {
|
|
472
|
+
schemas: {},
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
// Add security schemes if auth is enabled
|
|
476
|
+
if (manifest.auth.enabled) {
|
|
477
|
+
spec.components.securitySchemes = {
|
|
478
|
+
bearerAuth: {
|
|
479
|
+
type: 'http',
|
|
480
|
+
scheme: 'bearer',
|
|
481
|
+
bearerFormat: 'JWT',
|
|
482
|
+
description: 'JWT token from authentication'
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
// Generate schemas and paths for each entity
|
|
487
|
+
for (const entity of manifest.entities) {
|
|
488
|
+
// Add entity schemas
|
|
489
|
+
spec.components.schemas[entity.name] = generateEntitySchema(entity);
|
|
490
|
+
spec.components.schemas[`${entity.name}CreateInput`] = generateCreateSchema(entity);
|
|
491
|
+
spec.components.schemas[`${entity.name}UpdateInput`] = {
|
|
492
|
+
...generateCreateSchema(entity),
|
|
493
|
+
required: undefined, // All fields optional for updates
|
|
494
|
+
};
|
|
495
|
+
// Add entity paths
|
|
496
|
+
const entityPaths = generateEntityPaths(entity, baseUrl);
|
|
497
|
+
Object.assign(spec.paths, entityPaths);
|
|
498
|
+
}
|
|
499
|
+
return spec;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Generate Swagger UI HTML page
|
|
503
|
+
*/
|
|
504
|
+
function generateSwaggerUI() {
|
|
505
|
+
return `<!DOCTYPE html>
|
|
506
|
+
<html lang="en">
|
|
507
|
+
<head>
|
|
508
|
+
<meta charset="UTF-8">
|
|
509
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
510
|
+
<title>API Documentation - Swagger UI</title>
|
|
511
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
512
|
+
<style>
|
|
513
|
+
body { margin: 0; padding: 0; }
|
|
514
|
+
</style>
|
|
515
|
+
</head>
|
|
516
|
+
<body>
|
|
517
|
+
<div id="swagger-ui"></div>
|
|
518
|
+
|
|
519
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js"></script>
|
|
520
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js"></script>
|
|
521
|
+
<script>
|
|
522
|
+
window.onload = function() {
|
|
523
|
+
window.ui = SwaggerUIBundle({
|
|
524
|
+
url: './openapi.json',
|
|
525
|
+
dom_id: '#swagger-ui',
|
|
526
|
+
deepLinking: true,
|
|
527
|
+
presets: [
|
|
528
|
+
SwaggerUIBundle.presets.apis,
|
|
529
|
+
SwaggerUIStandalonePreset
|
|
530
|
+
],
|
|
531
|
+
plugins: [
|
|
532
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
533
|
+
],
|
|
534
|
+
layout: 'StandaloneLayout',
|
|
535
|
+
defaultModelsExpandDepth: 1,
|
|
536
|
+
defaultModelExpandDepth: 1,
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
</script>
|
|
540
|
+
</body>
|
|
541
|
+
</html>
|
|
542
|
+
`;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Generate Markdown documentation as alternative to Swagger UI
|
|
546
|
+
*/
|
|
547
|
+
function generateMarkdownDocs(manifest) {
|
|
548
|
+
const lines = [];
|
|
549
|
+
lines.push('# API Documentation');
|
|
550
|
+
lines.push('');
|
|
551
|
+
lines.push('Auto-generated API documentation from Archetype entity definitions.');
|
|
552
|
+
lines.push('');
|
|
553
|
+
lines.push('## Base URL');
|
|
554
|
+
lines.push('');
|
|
555
|
+
lines.push('```');
|
|
556
|
+
lines.push('http://localhost:3000/api/trpc');
|
|
557
|
+
lines.push('```');
|
|
558
|
+
lines.push('');
|
|
559
|
+
if (manifest.auth.enabled) {
|
|
560
|
+
lines.push('## Authentication');
|
|
561
|
+
lines.push('');
|
|
562
|
+
lines.push('Protected endpoints require a JWT token in the Authorization header:');
|
|
563
|
+
lines.push('');
|
|
564
|
+
lines.push('```');
|
|
565
|
+
lines.push('Authorization: Bearer <your-jwt-token>');
|
|
566
|
+
lines.push('```');
|
|
567
|
+
lines.push('');
|
|
568
|
+
}
|
|
569
|
+
lines.push('## Entities');
|
|
570
|
+
lines.push('');
|
|
571
|
+
for (const entity of manifest.entities) {
|
|
572
|
+
const routerName = (0, utils_1.toCamelCase)(entity.name);
|
|
573
|
+
lines.push(`### ${entity.name}`);
|
|
574
|
+
lines.push('');
|
|
575
|
+
// Fields
|
|
576
|
+
lines.push('#### Fields');
|
|
577
|
+
lines.push('');
|
|
578
|
+
lines.push('| Field | Type | Required | Description |');
|
|
579
|
+
lines.push('|-------|------|----------|-------------|');
|
|
580
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
581
|
+
const type = getOpenAPIType(field);
|
|
582
|
+
const required = field.required ? 'Yes' : 'No';
|
|
583
|
+
const description = field.label || '-';
|
|
584
|
+
lines.push(`| ${fieldName} | ${type.type} | ${required} | ${description} |`);
|
|
585
|
+
}
|
|
586
|
+
lines.push('');
|
|
587
|
+
// Endpoints
|
|
588
|
+
lines.push('#### Endpoints');
|
|
589
|
+
lines.push('');
|
|
590
|
+
// List
|
|
591
|
+
const listAuth = entity.protected.list ? '🔒 Protected' : '🌐 Public';
|
|
592
|
+
lines.push(`**List ${entity.name}s** ${listAuth}`);
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push('```');
|
|
595
|
+
lines.push(`GET /api/trpc/${routerName}.list?page=1&limit=10`);
|
|
596
|
+
lines.push('```');
|
|
597
|
+
lines.push('');
|
|
598
|
+
// Get
|
|
599
|
+
const getAuth = entity.protected.get ? '🔒 Protected' : '🌐 Public';
|
|
600
|
+
lines.push(`**Get ${entity.name} by ID** ${getAuth}`);
|
|
601
|
+
lines.push('');
|
|
602
|
+
lines.push('```');
|
|
603
|
+
lines.push(`GET /api/trpc/${routerName}.get?id=<id>`);
|
|
604
|
+
lines.push('```');
|
|
605
|
+
lines.push('');
|
|
606
|
+
// Create
|
|
607
|
+
const createAuth = entity.protected.create ? '🔒 Protected' : '🌐 Public';
|
|
608
|
+
lines.push(`**Create ${entity.name}** ${createAuth}`);
|
|
609
|
+
lines.push('');
|
|
610
|
+
lines.push('```');
|
|
611
|
+
lines.push(`POST /api/trpc/${routerName}.create`);
|
|
612
|
+
lines.push('Content-Type: application/json');
|
|
613
|
+
lines.push('');
|
|
614
|
+
// Generate example
|
|
615
|
+
const exampleFields = [];
|
|
616
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
617
|
+
if (field.type === 'computed')
|
|
618
|
+
continue;
|
|
619
|
+
if (!field.required)
|
|
620
|
+
continue;
|
|
621
|
+
let exampleValue = 'value';
|
|
622
|
+
if (field.type === 'text')
|
|
623
|
+
exampleValue = `"example ${fieldName}"`;
|
|
624
|
+
if (field.type === 'number')
|
|
625
|
+
exampleValue = '42';
|
|
626
|
+
if (field.type === 'boolean')
|
|
627
|
+
exampleValue = 'true';
|
|
628
|
+
if (field.enumValues)
|
|
629
|
+
exampleValue = `"${field.enumValues[0]}"`;
|
|
630
|
+
exampleFields.push(` "${fieldName}": ${exampleValue}`);
|
|
631
|
+
}
|
|
632
|
+
lines.push('{');
|
|
633
|
+
lines.push(exampleFields.join(',\n'));
|
|
634
|
+
lines.push('}');
|
|
635
|
+
lines.push('```');
|
|
636
|
+
lines.push('');
|
|
637
|
+
// Update
|
|
638
|
+
const updateAuth = entity.protected.update ? '🔒 Protected' : '🌐 Public';
|
|
639
|
+
lines.push(`**Update ${entity.name}** ${updateAuth}`);
|
|
640
|
+
lines.push('');
|
|
641
|
+
lines.push('```');
|
|
642
|
+
lines.push(`PATCH /api/trpc/${routerName}.update`);
|
|
643
|
+
lines.push('Content-Type: application/json');
|
|
644
|
+
lines.push('');
|
|
645
|
+
lines.push('{');
|
|
646
|
+
lines.push(' "id": "<id>",');
|
|
647
|
+
lines.push(' "data": { /* fields to update */ }');
|
|
648
|
+
lines.push('}');
|
|
649
|
+
lines.push('```');
|
|
650
|
+
lines.push('');
|
|
651
|
+
// Delete
|
|
652
|
+
const deleteAuth = entity.protected.remove ? '🔒 Protected' : '🌐 Public';
|
|
653
|
+
lines.push(`**Delete ${entity.name}** ${deleteAuth}`);
|
|
654
|
+
lines.push('');
|
|
655
|
+
lines.push('```');
|
|
656
|
+
lines.push(`DELETE /api/trpc/${routerName}.remove?id=<id>`);
|
|
657
|
+
lines.push('```');
|
|
658
|
+
lines.push('');
|
|
659
|
+
lines.push('---');
|
|
660
|
+
lines.push('');
|
|
661
|
+
}
|
|
662
|
+
return lines.join('\n');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* OpenAPI documentation generator
|
|
666
|
+
*/
|
|
667
|
+
exports.openapiGenerator = {
|
|
668
|
+
name: 'openapi-docs',
|
|
669
|
+
description: 'Generates OpenAPI specification and interactive Swagger UI documentation',
|
|
670
|
+
generate(manifest, ctx) {
|
|
671
|
+
const files = [];
|
|
672
|
+
// Generate OpenAPI JSON spec
|
|
673
|
+
const spec = generateOpenAPISpec(manifest);
|
|
674
|
+
files.push({
|
|
675
|
+
path: 'docs/openapi.json',
|
|
676
|
+
content: JSON.stringify(spec, null, 2),
|
|
677
|
+
});
|
|
678
|
+
// Generate Swagger UI HTML
|
|
679
|
+
files.push({
|
|
680
|
+
path: 'docs/swagger.html',
|
|
681
|
+
content: generateSwaggerUI(),
|
|
682
|
+
});
|
|
683
|
+
// Generate Markdown documentation
|
|
684
|
+
files.push({
|
|
685
|
+
path: 'docs/API.md',
|
|
686
|
+
content: generateMarkdownDocs(manifest),
|
|
687
|
+
});
|
|
688
|
+
return files;
|
|
689
|
+
},
|
|
690
|
+
};
|