codehooks-js 1.3.25 → 1.4.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 +28 -0
- package/crudlify/index.mjs +4 -0
- package/crudlify/lib/schema/json-schema/index.mjs +35 -19
- package/index.js +76 -7
- package/openapi/crudlify-docs.mjs +823 -0
- package/openapi/generator.mjs +417 -0
- package/openapi/index.mjs +221 -0
- package/openapi/schema-converter.mjs +668 -0
- package/openapi/swagger-ui.mjs +92 -0
- package/package.json +12 -3
- package/types/index.d.ts +256 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crudlify route documentation generator
|
|
3
|
+
* Generates OpenAPI paths and schemas for auto-generated CRUD routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { convertToJsonSchema } from './schema-converter.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate OpenAPI paths and components for crudlify routes
|
|
10
|
+
* @param {object} schemas - Map of collection name to schema
|
|
11
|
+
* @param {object} options - Crudlify options
|
|
12
|
+
* @param {string} [options.prefix] - Route prefix
|
|
13
|
+
* @returns {Promise<{paths: object, components: object}>}
|
|
14
|
+
*/
|
|
15
|
+
export async function generateCrudlifyPaths(schemas, options = {}) {
|
|
16
|
+
const prefix = options.prefix || '';
|
|
17
|
+
const paths = {};
|
|
18
|
+
const components = { schemas: {} };
|
|
19
|
+
|
|
20
|
+
// If no schemas defined, generate generic collection docs
|
|
21
|
+
if (!schemas || Object.keys(schemas).length === 0) {
|
|
22
|
+
return generateGenericCrudPaths(prefix);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const [collection, schemaOrConfig] of Object.entries(schemas)) {
|
|
26
|
+
// Handle both formats:
|
|
27
|
+
// { users: zodSchema } - direct schema
|
|
28
|
+
// { users: { schema: zodSchema } } - wrapped schema
|
|
29
|
+
let schema = schemaOrConfig;
|
|
30
|
+
if (schemaOrConfig && typeof schemaOrConfig === 'object' && schemaOrConfig.schema) {
|
|
31
|
+
schema = schemaOrConfig.schema;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Skip null schemas (allows any data)
|
|
35
|
+
const hasSchema = schema !== null && schema !== undefined;
|
|
36
|
+
|
|
37
|
+
// Check if user provided explicit openApiSchema in config
|
|
38
|
+
let explicitOpenApiSchema = null;
|
|
39
|
+
if (schemaOrConfig && typeof schemaOrConfig === 'object' && schemaOrConfig.openApiSchema) {
|
|
40
|
+
explicitOpenApiSchema = schemaOrConfig.openApiSchema;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Convert schema to JSON Schema
|
|
44
|
+
let jsonSchema = {
|
|
45
|
+
type: 'object',
|
|
46
|
+
additionalProperties: true,
|
|
47
|
+
description: 'Accepts any valid JSON object (no schema validation)'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (explicitOpenApiSchema) {
|
|
51
|
+
// User provided explicit OpenAPI schema - use it directly
|
|
52
|
+
jsonSchema = explicitOpenApiSchema;
|
|
53
|
+
} else if (hasSchema) {
|
|
54
|
+
try {
|
|
55
|
+
jsonSchema = await convertToJsonSchema(schema);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn(`Failed to convert schema for ${collection}:`, error.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Store in components
|
|
62
|
+
const schemaName = capitalizeFirst(collection);
|
|
63
|
+
components.schemas[schemaName] = jsonSchema;
|
|
64
|
+
|
|
65
|
+
// Collection routes: POST (create), GET (list)
|
|
66
|
+
const collectionPath = `${prefix}/${collection}`;
|
|
67
|
+
paths[collectionPath] = {
|
|
68
|
+
get: generateGetManySpec(collection, schemaName),
|
|
69
|
+
post: generateCreateSpec(collection, schemaName, hasSchema)
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Document routes: GET, PUT, PATCH, DELETE by ID
|
|
73
|
+
const documentPath = `${prefix}/${collection}/{ID}`;
|
|
74
|
+
paths[documentPath] = {
|
|
75
|
+
get: generateGetOneSpec(collection, schemaName),
|
|
76
|
+
put: generateReplaceSpec(collection, schemaName, hasSchema),
|
|
77
|
+
patch: generatePatchSpec(collection, schemaName),
|
|
78
|
+
delete: generateDeleteSpec(collection, schemaName)
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Batch operations: PATCH, DELETE by query
|
|
82
|
+
const byQueryPath = `${prefix}/${collection}/_byquery`;
|
|
83
|
+
paths[byQueryPath] = {
|
|
84
|
+
patch: generatePatchManySpec(collection, schemaName),
|
|
85
|
+
delete: generateDeleteManySpec(collection, schemaName)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { paths, components };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate GET (list) endpoint spec
|
|
94
|
+
*/
|
|
95
|
+
function generateGetManySpec(collection, schemaName) {
|
|
96
|
+
return {
|
|
97
|
+
summary: `List ${collection}`,
|
|
98
|
+
description: `Retrieve a list of ${collection} with optional filtering and pagination`,
|
|
99
|
+
tags: [capitalizeFirst(collection)],
|
|
100
|
+
operationId: `list${capitalizeFirst(collection)}`,
|
|
101
|
+
parameters: [
|
|
102
|
+
{
|
|
103
|
+
name: 'q',
|
|
104
|
+
in: 'query',
|
|
105
|
+
description: 'MongoDB-style query filter as JSON string, e.g. {"status":"active"}',
|
|
106
|
+
required: false,
|
|
107
|
+
schema: { type: 'string' }
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'h',
|
|
111
|
+
in: 'query',
|
|
112
|
+
description: 'Query hints as JSON: {"$limit":10,"$offset":0,"$fields":{"name":1},"$sort":{"created":-1}}',
|
|
113
|
+
required: false,
|
|
114
|
+
schema: { type: 'string' }
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'limit',
|
|
118
|
+
in: 'query',
|
|
119
|
+
description: 'Maximum number of items to return',
|
|
120
|
+
required: false,
|
|
121
|
+
schema: { type: 'integer', default: 100 }
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'offset',
|
|
125
|
+
in: 'query',
|
|
126
|
+
description: 'Number of items to skip',
|
|
127
|
+
required: false,
|
|
128
|
+
schema: { type: 'integer', default: 0 }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'sort',
|
|
132
|
+
in: 'query',
|
|
133
|
+
description: 'Sort field (prefix with - for descending)',
|
|
134
|
+
required: false,
|
|
135
|
+
schema: { type: 'string' }
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
responses: {
|
|
139
|
+
200: {
|
|
140
|
+
description: `List of ${collection}`,
|
|
141
|
+
content: {
|
|
142
|
+
'application/json': {
|
|
143
|
+
schema: {
|
|
144
|
+
type: 'array',
|
|
145
|
+
items: { $ref: `#/components/schemas/${schemaName}` }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
400: { description: 'Invalid query parameters' },
|
|
151
|
+
404: { description: 'Collection schema not found' }
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate POST (create) endpoint spec
|
|
158
|
+
*/
|
|
159
|
+
function generateCreateSpec(collection, schemaName, hasSchema) {
|
|
160
|
+
return {
|
|
161
|
+
summary: `Create ${singularize(collection)}`,
|
|
162
|
+
description: `Create a new document in ${collection}${hasSchema ? ' (validated against schema)' : ''}`,
|
|
163
|
+
tags: [capitalizeFirst(collection)],
|
|
164
|
+
operationId: `create${capitalizeFirst(singularize(collection))}`,
|
|
165
|
+
requestBody: {
|
|
166
|
+
required: true,
|
|
167
|
+
description: `${capitalizeFirst(singularize(collection))} data`,
|
|
168
|
+
content: {
|
|
169
|
+
'application/json': {
|
|
170
|
+
schema: { $ref: `#/components/schemas/${schemaName}` }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
responses: {
|
|
175
|
+
201: {
|
|
176
|
+
description: `${capitalizeFirst(singularize(collection))} created successfully`,
|
|
177
|
+
content: {
|
|
178
|
+
'application/json': {
|
|
179
|
+
schema: {
|
|
180
|
+
allOf: [
|
|
181
|
+
{ $ref: `#/components/schemas/${schemaName}` },
|
|
182
|
+
{
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
400: { description: 'Validation error or invalid request' }
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generate GET (one) endpoint spec
|
|
200
|
+
*/
|
|
201
|
+
function generateGetOneSpec(collection, schemaName) {
|
|
202
|
+
return {
|
|
203
|
+
summary: `Get ${singularize(collection)} by ID`,
|
|
204
|
+
description: `Retrieve a single ${singularize(collection)} by its unique identifier`,
|
|
205
|
+
tags: [capitalizeFirst(collection)],
|
|
206
|
+
operationId: `get${capitalizeFirst(singularize(collection))}`,
|
|
207
|
+
parameters: [
|
|
208
|
+
{
|
|
209
|
+
name: 'ID',
|
|
210
|
+
in: 'path',
|
|
211
|
+
required: true,
|
|
212
|
+
description: `Unique ${singularize(collection)} identifier`,
|
|
213
|
+
schema: { type: 'string' }
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
responses: {
|
|
217
|
+
200: {
|
|
218
|
+
description: `${capitalizeFirst(singularize(collection))} found`,
|
|
219
|
+
content: {
|
|
220
|
+
'application/json': {
|
|
221
|
+
schema: {
|
|
222
|
+
allOf: [
|
|
223
|
+
{ $ref: `#/components/schemas/${schemaName}` },
|
|
224
|
+
{
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {
|
|
227
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
404: { description: `${capitalizeFirst(singularize(collection))} not found` }
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Generate PUT (replace) endpoint spec
|
|
242
|
+
*/
|
|
243
|
+
function generateReplaceSpec(collection, schemaName, hasSchema) {
|
|
244
|
+
return {
|
|
245
|
+
summary: `Replace ${singularize(collection)}`,
|
|
246
|
+
description: `Replace an existing ${singularize(collection)} document entirely${hasSchema ? ' (validated against schema)' : ''}`,
|
|
247
|
+
tags: [capitalizeFirst(collection)],
|
|
248
|
+
operationId: `replace${capitalizeFirst(singularize(collection))}`,
|
|
249
|
+
parameters: [
|
|
250
|
+
{
|
|
251
|
+
name: 'ID',
|
|
252
|
+
in: 'path',
|
|
253
|
+
required: true,
|
|
254
|
+
description: `Unique ${singularize(collection)} identifier`,
|
|
255
|
+
schema: { type: 'string' }
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
requestBody: {
|
|
259
|
+
required: true,
|
|
260
|
+
description: `Complete ${singularize(collection)} data`,
|
|
261
|
+
content: {
|
|
262
|
+
'application/json': {
|
|
263
|
+
schema: { $ref: `#/components/schemas/${schemaName}` }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
responses: {
|
|
268
|
+
200: {
|
|
269
|
+
description: `${capitalizeFirst(singularize(collection))} replaced successfully`,
|
|
270
|
+
content: {
|
|
271
|
+
'application/json': {
|
|
272
|
+
schema: {
|
|
273
|
+
allOf: [
|
|
274
|
+
{ $ref: `#/components/schemas/${schemaName}` },
|
|
275
|
+
{
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
400: { description: 'Validation error' },
|
|
287
|
+
404: { description: `${capitalizeFirst(singularize(collection))} not found` }
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Generate PATCH (update) endpoint spec
|
|
294
|
+
*/
|
|
295
|
+
function generatePatchSpec(collection, schemaName) {
|
|
296
|
+
return {
|
|
297
|
+
summary: `Update ${singularize(collection)}`,
|
|
298
|
+
description: `Partially update an existing ${singularize(collection)} document`,
|
|
299
|
+
tags: [capitalizeFirst(collection)],
|
|
300
|
+
operationId: `update${capitalizeFirst(singularize(collection))}`,
|
|
301
|
+
parameters: [
|
|
302
|
+
{
|
|
303
|
+
name: 'ID',
|
|
304
|
+
in: 'path',
|
|
305
|
+
required: true,
|
|
306
|
+
description: `Unique ${singularize(collection)} identifier`,
|
|
307
|
+
schema: { type: 'string' }
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
requestBody: {
|
|
311
|
+
required: true,
|
|
312
|
+
description: `Partial ${singularize(collection)} data to update`,
|
|
313
|
+
content: {
|
|
314
|
+
'application/json': {
|
|
315
|
+
schema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
description: 'Fields to update'
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
responses: {
|
|
323
|
+
200: {
|
|
324
|
+
description: `${capitalizeFirst(singularize(collection))} updated successfully`,
|
|
325
|
+
content: {
|
|
326
|
+
'application/json': {
|
|
327
|
+
schema: {
|
|
328
|
+
allOf: [
|
|
329
|
+
{ $ref: `#/components/schemas/${schemaName}` },
|
|
330
|
+
{
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
400: { description: 'Validation error' },
|
|
342
|
+
404: { description: `${capitalizeFirst(singularize(collection))} not found` }
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate DELETE endpoint spec
|
|
349
|
+
*/
|
|
350
|
+
function generateDeleteSpec(collection, schemaName) {
|
|
351
|
+
return {
|
|
352
|
+
summary: `Delete ${singularize(collection)}`,
|
|
353
|
+
description: `Delete a ${singularize(collection)} by its unique identifier`,
|
|
354
|
+
tags: [capitalizeFirst(collection)],
|
|
355
|
+
operationId: `delete${capitalizeFirst(singularize(collection))}`,
|
|
356
|
+
parameters: [
|
|
357
|
+
{
|
|
358
|
+
name: 'ID',
|
|
359
|
+
in: 'path',
|
|
360
|
+
required: true,
|
|
361
|
+
description: `Unique ${singularize(collection)} identifier`,
|
|
362
|
+
schema: { type: 'string' }
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
responses: {
|
|
366
|
+
200: {
|
|
367
|
+
description: `${capitalizeFirst(singularize(collection))} deleted successfully`,
|
|
368
|
+
content: {
|
|
369
|
+
'application/json': {
|
|
370
|
+
schema: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
_id: { type: 'string', description: 'ID of deleted document' }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
404: { description: `${capitalizeFirst(singularize(collection))} not found` }
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generate PATCH (batch update) endpoint spec
|
|
386
|
+
*/
|
|
387
|
+
function generatePatchManySpec(collection, schemaName) {
|
|
388
|
+
return {
|
|
389
|
+
summary: `Batch update ${collection}`,
|
|
390
|
+
description: `Update multiple ${collection} documents matching a query`,
|
|
391
|
+
tags: [capitalizeFirst(collection)],
|
|
392
|
+
operationId: `updateMany${capitalizeFirst(collection)}`,
|
|
393
|
+
parameters: [
|
|
394
|
+
{
|
|
395
|
+
name: 'q',
|
|
396
|
+
in: 'query',
|
|
397
|
+
description: 'MongoDB-style query filter as JSON string',
|
|
398
|
+
required: true,
|
|
399
|
+
schema: { type: 'string' }
|
|
400
|
+
}
|
|
401
|
+
],
|
|
402
|
+
requestBody: {
|
|
403
|
+
required: true,
|
|
404
|
+
description: 'Fields to update on matching documents',
|
|
405
|
+
content: {
|
|
406
|
+
'application/json': {
|
|
407
|
+
schema: {
|
|
408
|
+
type: 'object',
|
|
409
|
+
description: 'Update operations'
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
responses: {
|
|
415
|
+
200: {
|
|
416
|
+
description: 'Documents updated successfully',
|
|
417
|
+
content: {
|
|
418
|
+
'application/json': {
|
|
419
|
+
schema: {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
modifiedCount: { type: 'integer', description: 'Number of documents modified' }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
400: { description: 'Invalid query or validation error' },
|
|
429
|
+
404: { description: 'Collection schema not found' }
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Generate DELETE (batch) endpoint spec
|
|
436
|
+
*/
|
|
437
|
+
function generateDeleteManySpec(collection, schemaName) {
|
|
438
|
+
return {
|
|
439
|
+
summary: `Batch delete ${collection}`,
|
|
440
|
+
description: `Delete multiple ${collection} documents matching a query`,
|
|
441
|
+
tags: [capitalizeFirst(collection)],
|
|
442
|
+
operationId: `deleteMany${capitalizeFirst(collection)}`,
|
|
443
|
+
parameters: [
|
|
444
|
+
{
|
|
445
|
+
name: 'q',
|
|
446
|
+
in: 'query',
|
|
447
|
+
description: 'MongoDB-style query filter as JSON string',
|
|
448
|
+
required: true,
|
|
449
|
+
schema: { type: 'string' }
|
|
450
|
+
}
|
|
451
|
+
],
|
|
452
|
+
responses: {
|
|
453
|
+
200: {
|
|
454
|
+
description: 'Documents deleted successfully',
|
|
455
|
+
content: {
|
|
456
|
+
'application/json': {
|
|
457
|
+
schema: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: {
|
|
460
|
+
deletedCount: { type: 'integer', description: 'Number of documents deleted' }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
400: { description: 'Invalid query' },
|
|
467
|
+
404: { description: 'Collection schema not found' }
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Capitalize first letter
|
|
474
|
+
* @param {string} str
|
|
475
|
+
* @returns {string}
|
|
476
|
+
*/
|
|
477
|
+
function capitalizeFirst(str) {
|
|
478
|
+
if (!str) return '';
|
|
479
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Simple singularize (removes trailing 's')
|
|
484
|
+
* @param {string} str
|
|
485
|
+
* @returns {string}
|
|
486
|
+
*/
|
|
487
|
+
function singularize(str) {
|
|
488
|
+
if (!str) return '';
|
|
489
|
+
if (str.endsWith('ies')) {
|
|
490
|
+
return str.slice(0, -3) + 'y';
|
|
491
|
+
}
|
|
492
|
+
if (str.endsWith('es') && (str.endsWith('sses') || str.endsWith('xes') || str.endsWith('ches') || str.endsWith('shes'))) {
|
|
493
|
+
return str.slice(0, -2);
|
|
494
|
+
}
|
|
495
|
+
if (str.endsWith('s') && !str.endsWith('ss')) {
|
|
496
|
+
return str.slice(0, -1);
|
|
497
|
+
}
|
|
498
|
+
return str;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Generate generic CRUD documentation when no schemas are defined
|
|
503
|
+
* Documents the /{collection} routes that accept any collection name
|
|
504
|
+
* @param {string} prefix - Route prefix
|
|
505
|
+
* @returns {{paths: object, components: object}}
|
|
506
|
+
*/
|
|
507
|
+
function generateGenericCrudPaths(prefix) {
|
|
508
|
+
const paths = {};
|
|
509
|
+
const components = {
|
|
510
|
+
schemas: {
|
|
511
|
+
Document: {
|
|
512
|
+
type: 'object',
|
|
513
|
+
additionalProperties: true,
|
|
514
|
+
description: 'Any valid JSON object'
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const collectionParam = {
|
|
520
|
+
name: 'collection',
|
|
521
|
+
in: 'path',
|
|
522
|
+
required: true,
|
|
523
|
+
description: 'Collection name (e.g., users, products, orders)',
|
|
524
|
+
schema: { type: 'string' }
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const idParam = {
|
|
528
|
+
name: 'ID',
|
|
529
|
+
in: 'path',
|
|
530
|
+
required: true,
|
|
531
|
+
description: 'Unique document identifier',
|
|
532
|
+
schema: { type: 'string' }
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Collection routes: /{collection}
|
|
536
|
+
const collectionPath = `${prefix}/{collection}`;
|
|
537
|
+
paths[collectionPath] = {
|
|
538
|
+
get: {
|
|
539
|
+
summary: 'List documents',
|
|
540
|
+
description: 'Retrieve documents from any collection with optional filtering and pagination',
|
|
541
|
+
tags: ['Collections'],
|
|
542
|
+
operationId: 'listDocuments',
|
|
543
|
+
parameters: [
|
|
544
|
+
collectionParam,
|
|
545
|
+
{
|
|
546
|
+
name: 'q',
|
|
547
|
+
in: 'query',
|
|
548
|
+
description: 'MongoDB-style query filter as JSON string, e.g. {"status":"active"}',
|
|
549
|
+
required: false,
|
|
550
|
+
schema: { type: 'string' }
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'h',
|
|
554
|
+
in: 'query',
|
|
555
|
+
description: 'Query hints as JSON: {"$limit":10,"$offset":0,"$fields":{"name":1},"$sort":{"created":-1}}',
|
|
556
|
+
required: false,
|
|
557
|
+
schema: { type: 'string' }
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'limit',
|
|
561
|
+
in: 'query',
|
|
562
|
+
description: 'Maximum number of items to return',
|
|
563
|
+
required: false,
|
|
564
|
+
schema: { type: 'integer', default: 100 }
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'offset',
|
|
568
|
+
in: 'query',
|
|
569
|
+
description: 'Number of items to skip',
|
|
570
|
+
required: false,
|
|
571
|
+
schema: { type: 'integer', default: 0 }
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
responses: {
|
|
575
|
+
200: {
|
|
576
|
+
description: 'List of documents',
|
|
577
|
+
content: {
|
|
578
|
+
'application/json': {
|
|
579
|
+
schema: {
|
|
580
|
+
type: 'array',
|
|
581
|
+
items: { $ref: '#/components/schemas/Document' }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
400: { description: 'Invalid query parameters' }
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
post: {
|
|
590
|
+
summary: 'Create document',
|
|
591
|
+
description: 'Create a new document in any collection. No schema validation is applied.',
|
|
592
|
+
tags: ['Collections'],
|
|
593
|
+
operationId: 'createDocument',
|
|
594
|
+
parameters: [collectionParam],
|
|
595
|
+
requestBody: {
|
|
596
|
+
required: true,
|
|
597
|
+
description: 'Document data (any valid JSON)',
|
|
598
|
+
content: {
|
|
599
|
+
'application/json': {
|
|
600
|
+
schema: { $ref: '#/components/schemas/Document' }
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
responses: {
|
|
605
|
+
201: {
|
|
606
|
+
description: 'Document created successfully',
|
|
607
|
+
content: {
|
|
608
|
+
'application/json': {
|
|
609
|
+
schema: {
|
|
610
|
+
allOf: [
|
|
611
|
+
{ $ref: '#/components/schemas/Document' },
|
|
612
|
+
{
|
|
613
|
+
type: 'object',
|
|
614
|
+
properties: {
|
|
615
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
400: { description: 'Invalid request body' }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Document routes: /{collection}/{ID}
|
|
629
|
+
const documentPath = `${prefix}/{collection}/{ID}`;
|
|
630
|
+
paths[documentPath] = {
|
|
631
|
+
get: {
|
|
632
|
+
summary: 'Get document by ID',
|
|
633
|
+
description: 'Retrieve a single document by its unique identifier',
|
|
634
|
+
tags: ['Collections'],
|
|
635
|
+
operationId: 'getDocument',
|
|
636
|
+
parameters: [collectionParam, idParam],
|
|
637
|
+
responses: {
|
|
638
|
+
200: {
|
|
639
|
+
description: 'Document found',
|
|
640
|
+
content: {
|
|
641
|
+
'application/json': {
|
|
642
|
+
schema: {
|
|
643
|
+
allOf: [
|
|
644
|
+
{ $ref: '#/components/schemas/Document' },
|
|
645
|
+
{
|
|
646
|
+
type: 'object',
|
|
647
|
+
properties: {
|
|
648
|
+
_id: { type: 'string', description: 'Unique document ID' }
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
404: { description: 'Document not found' }
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
put: {
|
|
660
|
+
summary: 'Replace document',
|
|
661
|
+
description: 'Replace an existing document entirely',
|
|
662
|
+
tags: ['Collections'],
|
|
663
|
+
operationId: 'replaceDocument',
|
|
664
|
+
parameters: [collectionParam, idParam],
|
|
665
|
+
requestBody: {
|
|
666
|
+
required: true,
|
|
667
|
+
description: 'Complete document data',
|
|
668
|
+
content: {
|
|
669
|
+
'application/json': {
|
|
670
|
+
schema: { $ref: '#/components/schemas/Document' }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
responses: {
|
|
675
|
+
200: {
|
|
676
|
+
description: 'Document replaced successfully',
|
|
677
|
+
content: {
|
|
678
|
+
'application/json': {
|
|
679
|
+
schema: { $ref: '#/components/schemas/Document' }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
404: { description: 'Document not found' }
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
patch: {
|
|
687
|
+
summary: 'Update document',
|
|
688
|
+
description: 'Partially update an existing document',
|
|
689
|
+
tags: ['Collections'],
|
|
690
|
+
operationId: 'updateDocument',
|
|
691
|
+
parameters: [collectionParam, idParam],
|
|
692
|
+
requestBody: {
|
|
693
|
+
required: true,
|
|
694
|
+
description: 'Fields to update',
|
|
695
|
+
content: {
|
|
696
|
+
'application/json': {
|
|
697
|
+
schema: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
description: 'Partial document data'
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
responses: {
|
|
705
|
+
200: {
|
|
706
|
+
description: 'Document updated successfully',
|
|
707
|
+
content: {
|
|
708
|
+
'application/json': {
|
|
709
|
+
schema: { $ref: '#/components/schemas/Document' }
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
404: { description: 'Document not found' }
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
delete: {
|
|
717
|
+
summary: 'Delete document',
|
|
718
|
+
description: 'Delete a document by its unique identifier',
|
|
719
|
+
tags: ['Collections'],
|
|
720
|
+
operationId: 'deleteDocument',
|
|
721
|
+
parameters: [collectionParam, idParam],
|
|
722
|
+
responses: {
|
|
723
|
+
200: {
|
|
724
|
+
description: 'Document deleted successfully',
|
|
725
|
+
content: {
|
|
726
|
+
'application/json': {
|
|
727
|
+
schema: {
|
|
728
|
+
type: 'object',
|
|
729
|
+
properties: {
|
|
730
|
+
_id: { type: 'string', description: 'ID of deleted document' }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
404: { description: 'Document not found' }
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// Batch operations: /{collection}/_byquery
|
|
742
|
+
const byQueryPath = `${prefix}/{collection}/_byquery`;
|
|
743
|
+
paths[byQueryPath] = {
|
|
744
|
+
patch: {
|
|
745
|
+
summary: 'Batch update documents',
|
|
746
|
+
description: 'Update multiple documents matching a query',
|
|
747
|
+
tags: ['Collections'],
|
|
748
|
+
operationId: 'updateDocuments',
|
|
749
|
+
parameters: [
|
|
750
|
+
collectionParam,
|
|
751
|
+
{
|
|
752
|
+
name: 'q',
|
|
753
|
+
in: 'query',
|
|
754
|
+
description: 'MongoDB-style query filter as JSON string',
|
|
755
|
+
required: true,
|
|
756
|
+
schema: { type: 'string' }
|
|
757
|
+
}
|
|
758
|
+
],
|
|
759
|
+
requestBody: {
|
|
760
|
+
required: true,
|
|
761
|
+
description: 'Fields to update on matching documents',
|
|
762
|
+
content: {
|
|
763
|
+
'application/json': {
|
|
764
|
+
schema: {
|
|
765
|
+
type: 'object',
|
|
766
|
+
description: 'Update operations'
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
responses: {
|
|
772
|
+
200: {
|
|
773
|
+
description: 'Documents updated successfully',
|
|
774
|
+
content: {
|
|
775
|
+
'application/json': {
|
|
776
|
+
schema: {
|
|
777
|
+
type: 'object',
|
|
778
|
+
properties: {
|
|
779
|
+
modifiedCount: { type: 'integer', description: 'Number of documents modified' }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
400: { description: 'Invalid query' }
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
delete: {
|
|
789
|
+
summary: 'Batch delete documents',
|
|
790
|
+
description: 'Delete multiple documents matching a query',
|
|
791
|
+
tags: ['Collections'],
|
|
792
|
+
operationId: 'deleteDocuments',
|
|
793
|
+
parameters: [
|
|
794
|
+
collectionParam,
|
|
795
|
+
{
|
|
796
|
+
name: 'q',
|
|
797
|
+
in: 'query',
|
|
798
|
+
description: 'MongoDB-style query filter as JSON string',
|
|
799
|
+
required: true,
|
|
800
|
+
schema: { type: 'string' }
|
|
801
|
+
}
|
|
802
|
+
],
|
|
803
|
+
responses: {
|
|
804
|
+
200: {
|
|
805
|
+
description: 'Documents deleted successfully',
|
|
806
|
+
content: {
|
|
807
|
+
'application/json': {
|
|
808
|
+
schema: {
|
|
809
|
+
type: 'object',
|
|
810
|
+
properties: {
|
|
811
|
+
deletedCount: { type: 'integer', description: 'Number of documents deleted' }
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
400: { description: 'Invalid query' }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
return { paths, components };
|
|
823
|
+
}
|