@tellescope/schema 1.238.0 → 1.239.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/generate-openapi.d.ts +106 -0
- package/lib/cjs/generate-openapi.d.ts.map +1 -0
- package/lib/cjs/generate-openapi.js +1148 -0
- package/lib/cjs/generate-openapi.js.map +1 -0
- package/lib/cjs/schema.d.ts +2 -0
- package/lib/cjs/schema.d.ts.map +1 -1
- package/lib/cjs/schema.js +6 -4
- package/lib/cjs/schema.js.map +1 -1
- package/lib/esm/generate-openapi.d.ts +106 -0
- package/lib/esm/generate-openapi.d.ts.map +1 -0
- package/lib/esm/generate-openapi.js +1123 -0
- package/lib/esm/generate-openapi.js.map +1 -0
- package/lib/esm/schema.d.ts +2 -0
- package/lib/esm/schema.d.ts.map +1 -1
- package/lib/esm/schema.js +7 -5
- package/lib/esm/schema.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/openapi.json +139944 -0
- package/package.json +9 -9
- package/src/generate-openapi.ts +1268 -0
- package/src/schema.ts +12 -4
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI 3.0 Specification Generator for Tellescope API
|
|
4
|
+
*
|
|
5
|
+
* This script generates an OpenAPI 3.0 JSON specification from the Tellescope schema.
|
|
6
|
+
* It reads the schema definitions and produces a complete API documentation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* cd packages/public/schema
|
|
10
|
+
* npx ts-node src/generate-openapi.ts [output-path]
|
|
11
|
+
*
|
|
12
|
+
* Default output: ./openapi.json
|
|
13
|
+
*/
|
|
14
|
+
var __assign = (this && this.__assign) || function () {
|
|
15
|
+
__assign = Object.assign || function(t) {
|
|
16
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
17
|
+
s = arguments[i];
|
|
18
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
19
|
+
t[p] = s[p];
|
|
20
|
+
}
|
|
21
|
+
return t;
|
|
22
|
+
};
|
|
23
|
+
return __assign.apply(this, arguments);
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
35
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
36
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
37
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
38
|
+
function step(op) {
|
|
39
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
40
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
41
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
42
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
43
|
+
switch (op[0]) {
|
|
44
|
+
case 0: case 1: t = op; break;
|
|
45
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
46
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
47
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
48
|
+
default:
|
|
49
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
50
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
51
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
52
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
53
|
+
if (t[2]) _.ops.pop();
|
|
54
|
+
_.trys.pop(); continue;
|
|
55
|
+
}
|
|
56
|
+
op = body.call(thisArg, _);
|
|
57
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
58
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
62
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
63
|
+
if (ar || !(i in from)) {
|
|
64
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
65
|
+
ar[i] = from[i];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
69
|
+
};
|
|
70
|
+
import * as fs from 'fs';
|
|
71
|
+
import * as path from 'path';
|
|
72
|
+
import { schema } from './schema';
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Helper Functions
|
|
75
|
+
// ============================================================================
|
|
76
|
+
/**
|
|
77
|
+
* Convert a model name to PascalCase
|
|
78
|
+
*/
|
|
79
|
+
function pascalCase(str) {
|
|
80
|
+
return str
|
|
81
|
+
.split('_')
|
|
82
|
+
.map(function (s) { return s.charAt(0).toUpperCase() + s.slice(1); })
|
|
83
|
+
.join('');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Convert underscores to hyphens for URL-safe paths (matching url_safe_path from utilities)
|
|
87
|
+
*/
|
|
88
|
+
function urlSafePath(p) {
|
|
89
|
+
return p.replace(/_/g, '-');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the singular form of a model name for URL paths
|
|
93
|
+
*/
|
|
94
|
+
function getSingularName(modelName) {
|
|
95
|
+
var safeName = urlSafePath(modelName);
|
|
96
|
+
// Remove trailing 's' for singular
|
|
97
|
+
return safeName.endsWith('s') ? safeName.slice(0, -1) : safeName;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the plural form of a model name for URL paths
|
|
101
|
+
*/
|
|
102
|
+
function getPluralName(modelName) {
|
|
103
|
+
return urlSafePath(modelName);
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Validator to OpenAPI Type Mapping
|
|
107
|
+
// ============================================================================
|
|
108
|
+
/**
|
|
109
|
+
* Convert a Tellescope validator to an OpenAPI schema type
|
|
110
|
+
*/
|
|
111
|
+
function validatorToOpenAPIType(validator) {
|
|
112
|
+
try {
|
|
113
|
+
var typeInfo = validator.getType();
|
|
114
|
+
var example = validator.getExample();
|
|
115
|
+
// Handle primitive string types
|
|
116
|
+
if (typeInfo === 'string') {
|
|
117
|
+
return { type: 'string', example: typeof example === 'string' ? example : undefined };
|
|
118
|
+
}
|
|
119
|
+
if (typeInfo === 'number') {
|
|
120
|
+
return { type: 'number', example: typeof example === 'number' ? example : undefined };
|
|
121
|
+
}
|
|
122
|
+
if (typeInfo === 'boolean') {
|
|
123
|
+
return { type: 'boolean', example: typeof example === 'boolean' ? example : undefined };
|
|
124
|
+
}
|
|
125
|
+
if (typeInfo === 'Date') {
|
|
126
|
+
return { type: 'string', format: 'date-time', example: typeof example === 'string' ? example : undefined };
|
|
127
|
+
}
|
|
128
|
+
// Handle arrays - getType() returns [innerType] or [example]
|
|
129
|
+
if (Array.isArray(typeInfo)) {
|
|
130
|
+
var innerExample = typeInfo[0];
|
|
131
|
+
// Check if it's a primitive array
|
|
132
|
+
if (typeof innerExample === 'string') {
|
|
133
|
+
// It's an array of strings
|
|
134
|
+
return {
|
|
135
|
+
type: 'array',
|
|
136
|
+
items: { type: 'string' },
|
|
137
|
+
example: Array.isArray(example) ? example : undefined
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (typeof innerExample === 'number') {
|
|
141
|
+
return {
|
|
142
|
+
type: 'array',
|
|
143
|
+
items: { type: 'number' },
|
|
144
|
+
example: Array.isArray(example) ? example : undefined
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof innerExample === 'boolean') {
|
|
148
|
+
return {
|
|
149
|
+
type: 'array',
|
|
150
|
+
items: { type: 'boolean' },
|
|
151
|
+
example: Array.isArray(example) ? example : undefined
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (typeof innerExample === 'object' && innerExample !== null) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: objectTypeToSchema(innerExample),
|
|
158
|
+
example: Array.isArray(example) ? example : undefined
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Fallback for arrays
|
|
162
|
+
return {
|
|
163
|
+
type: 'array',
|
|
164
|
+
items: { type: 'string' },
|
|
165
|
+
example: Array.isArray(example) ? example : undefined
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Handle objects - getType() returns { field: type, ... }
|
|
169
|
+
if (typeof typeInfo === 'object' && typeInfo !== null) {
|
|
170
|
+
return objectTypeToSchema(typeInfo, example);
|
|
171
|
+
}
|
|
172
|
+
// Fallback for unknown types
|
|
173
|
+
return { type: 'object', additionalProperties: true };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
// If validator doesn't have getType/getExample, return generic type
|
|
177
|
+
return { type: 'object', additionalProperties: true };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Convert an object type definition to OpenAPI schema
|
|
182
|
+
*/
|
|
183
|
+
function objectTypeToSchema(typeObj, example) {
|
|
184
|
+
var properties = {};
|
|
185
|
+
for (var _i = 0, _a = Object.entries(typeObj); _i < _a.length; _i++) {
|
|
186
|
+
var _b = _a[_i], key = _b[0], value = _b[1];
|
|
187
|
+
if (typeof value === 'string') {
|
|
188
|
+
// Direct type string
|
|
189
|
+
if (value === 'string') {
|
|
190
|
+
properties[key] = { type: 'string' };
|
|
191
|
+
}
|
|
192
|
+
else if (value === 'number') {
|
|
193
|
+
properties[key] = { type: 'number' };
|
|
194
|
+
}
|
|
195
|
+
else if (value === 'boolean') {
|
|
196
|
+
properties[key] = { type: 'boolean' };
|
|
197
|
+
}
|
|
198
|
+
else if (value === 'Date') {
|
|
199
|
+
properties[key] = { type: 'string', format: 'date-time' };
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Treat as string enum value or literal
|
|
203
|
+
properties[key] = { type: 'string' };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (typeof value === 'number') {
|
|
207
|
+
properties[key] = { type: 'number', example: value };
|
|
208
|
+
}
|
|
209
|
+
else if (typeof value === 'boolean') {
|
|
210
|
+
properties[key] = { type: 'boolean', example: value };
|
|
211
|
+
}
|
|
212
|
+
else if (Array.isArray(value)) {
|
|
213
|
+
// Array type
|
|
214
|
+
var innerType = value[0];
|
|
215
|
+
if (typeof innerType === 'string') {
|
|
216
|
+
properties[key] = { type: 'array', items: { type: 'string' } };
|
|
217
|
+
}
|
|
218
|
+
else if (typeof innerType === 'number') {
|
|
219
|
+
properties[key] = { type: 'array', items: { type: 'number' } };
|
|
220
|
+
}
|
|
221
|
+
else if (typeof innerType === 'object' && innerType !== null) {
|
|
222
|
+
properties[key] = { type: 'array', items: objectTypeToSchema(innerType) };
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
properties[key] = { type: 'array', items: { type: 'string' } };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (typeof value === 'object' && value !== null) {
|
|
229
|
+
// Nested object
|
|
230
|
+
properties[key] = objectTypeToSchema(value);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: properties,
|
|
236
|
+
example: typeof example === 'object' ? example : undefined
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// Schema Component Generators
|
|
241
|
+
// ============================================================================
|
|
242
|
+
/**
|
|
243
|
+
* Generate the full model schema for responses (includes all fields except redacted)
|
|
244
|
+
*/
|
|
245
|
+
function generateModelSchema(modelName, fields) {
|
|
246
|
+
var _a, _b;
|
|
247
|
+
var properties = {};
|
|
248
|
+
var required = [];
|
|
249
|
+
// Add id field
|
|
250
|
+
properties['id'] = {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Unique identifier',
|
|
253
|
+
pattern: '^[0-9a-fA-F]{24}$'
|
|
254
|
+
};
|
|
255
|
+
for (var _i = 0, _c = Object.entries(fields); _i < _c.length; _i++) {
|
|
256
|
+
var _d = _c[_i], fieldName = _d[0], fieldInfo = _d[1];
|
|
257
|
+
var info = fieldInfo;
|
|
258
|
+
// Skip fields redacted for all users
|
|
259
|
+
if ((_a = info.redactions) === null || _a === void 0 ? void 0 : _a.includes('all'))
|
|
260
|
+
continue;
|
|
261
|
+
// Skip internal _id field (we use 'id' instead)
|
|
262
|
+
if (fieldName === '_id')
|
|
263
|
+
continue;
|
|
264
|
+
try {
|
|
265
|
+
var schema_1 = validatorToOpenAPIType(info.validator);
|
|
266
|
+
// Add description for redacted fields
|
|
267
|
+
if ((_b = info.redactions) === null || _b === void 0 ? void 0 : _b.includes('enduser')) {
|
|
268
|
+
schema_1.description = 'Not visible to endusers';
|
|
269
|
+
}
|
|
270
|
+
properties[fieldName] = schema_1;
|
|
271
|
+
if (info.required) {
|
|
272
|
+
required.push(fieldName);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
// Skip fields with invalid validators
|
|
277
|
+
properties[fieldName] = { type: 'object', additionalProperties: true };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: properties,
|
|
283
|
+
required: required.length > 0 ? required : undefined
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate schema for create operations (excludes readonly fields)
|
|
288
|
+
*/
|
|
289
|
+
function generateCreateSchema(modelName, fields) {
|
|
290
|
+
var _a;
|
|
291
|
+
var properties = {};
|
|
292
|
+
var required = [];
|
|
293
|
+
for (var _i = 0, _b = Object.entries(fields); _i < _b.length; _i++) {
|
|
294
|
+
var _c = _b[_i], fieldName = _c[0], fieldInfo = _c[1];
|
|
295
|
+
var info = fieldInfo;
|
|
296
|
+
// Skip readonly fields for create
|
|
297
|
+
if (info.readonly)
|
|
298
|
+
continue;
|
|
299
|
+
// Skip fields redacted for all users
|
|
300
|
+
if ((_a = info.redactions) === null || _a === void 0 ? void 0 : _a.includes('all'))
|
|
301
|
+
continue;
|
|
302
|
+
// Skip internal fields
|
|
303
|
+
if (fieldName === '_id')
|
|
304
|
+
continue;
|
|
305
|
+
try {
|
|
306
|
+
properties[fieldName] = validatorToOpenAPIType(info.validator);
|
|
307
|
+
if (info.required) {
|
|
308
|
+
required.push(fieldName);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
properties[fieldName] = { type: 'object', additionalProperties: true };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: properties,
|
|
318
|
+
required: required.length > 0 ? required : undefined
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Generate schema for update operations (excludes readonly and updatesDisabled fields)
|
|
323
|
+
*/
|
|
324
|
+
function generateUpdateSchema(modelName, fields) {
|
|
325
|
+
var _a;
|
|
326
|
+
var properties = {};
|
|
327
|
+
for (var _i = 0, _b = Object.entries(fields); _i < _b.length; _i++) {
|
|
328
|
+
var _c = _b[_i], fieldName = _c[0], fieldInfo = _c[1];
|
|
329
|
+
var info = fieldInfo;
|
|
330
|
+
// Skip readonly fields
|
|
331
|
+
if (info.readonly)
|
|
332
|
+
continue;
|
|
333
|
+
// Skip fields where updates are disabled
|
|
334
|
+
if (info.updatesDisabled)
|
|
335
|
+
continue;
|
|
336
|
+
// Skip fields redacted for all users
|
|
337
|
+
if ((_a = info.redactions) === null || _a === void 0 ? void 0 : _a.includes('all'))
|
|
338
|
+
continue;
|
|
339
|
+
// Skip internal fields
|
|
340
|
+
if (fieldName === '_id')
|
|
341
|
+
continue;
|
|
342
|
+
try {
|
|
343
|
+
properties[fieldName] = validatorToOpenAPIType(info.validator);
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
properties[fieldName] = { type: 'object', additionalProperties: true };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
type: 'object',
|
|
351
|
+
properties: properties,
|
|
352
|
+
description: 'Fields to update (all optional)'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Generate all schema components
|
|
357
|
+
*/
|
|
358
|
+
function generateComponents() {
|
|
359
|
+
var schemas = {};
|
|
360
|
+
for (var _i = 0, _a = Object.entries(schema); _i < _a.length; _i++) {
|
|
361
|
+
var _b = _a[_i], modelName = _b[0], modelDef = _b[1];
|
|
362
|
+
var model = modelDef;
|
|
363
|
+
var pascalName = pascalCase(modelName);
|
|
364
|
+
// Generate full model schema (for responses)
|
|
365
|
+
schemas[pascalName] = generateModelSchema(modelName, model.fields);
|
|
366
|
+
// Generate create schema
|
|
367
|
+
schemas["".concat(pascalName, "Create")] = generateCreateSchema(modelName, model.fields);
|
|
368
|
+
// Generate update schema
|
|
369
|
+
schemas["".concat(pascalName, "Update")] = generateUpdateSchema(modelName, model.fields);
|
|
370
|
+
}
|
|
371
|
+
// Add common schemas
|
|
372
|
+
schemas['Error'] = {
|
|
373
|
+
type: 'object',
|
|
374
|
+
properties: {
|
|
375
|
+
message: { type: 'string', description: 'Error message' },
|
|
376
|
+
code: { type: 'integer', description: 'Error code' },
|
|
377
|
+
info: { type: 'object', additionalProperties: true, description: 'Additional error information' }
|
|
378
|
+
},
|
|
379
|
+
required: ['message']
|
|
380
|
+
};
|
|
381
|
+
schemas['ObjectId'] = {
|
|
382
|
+
type: 'string',
|
|
383
|
+
pattern: '^[0-9a-fA-F]{24}$',
|
|
384
|
+
description: 'MongoDB ObjectId',
|
|
385
|
+
example: '60398b0231a295e64f084fd9'
|
|
386
|
+
};
|
|
387
|
+
return schemas;
|
|
388
|
+
}
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Path Generators
|
|
391
|
+
// ============================================================================
|
|
392
|
+
/**
|
|
393
|
+
* Get common query parameters for readMany operations
|
|
394
|
+
*/
|
|
395
|
+
function getReadManyParameters() {
|
|
396
|
+
return [
|
|
397
|
+
{
|
|
398
|
+
name: 'limit',
|
|
399
|
+
in: 'query',
|
|
400
|
+
schema: { type: 'integer', minimum: 1, maximum: 1000 },
|
|
401
|
+
description: 'Maximum number of records to return (default varies by model, max 1000)'
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'lastId',
|
|
405
|
+
in: 'query',
|
|
406
|
+
schema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' },
|
|
407
|
+
description: 'Cursor for pagination - ID of the last record from previous page'
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: 'sort',
|
|
411
|
+
in: 'query',
|
|
412
|
+
schema: { type: 'string', enum: ['oldFirst', 'newFirst'] },
|
|
413
|
+
description: 'Sort order by creation date'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'sortBy',
|
|
417
|
+
in: 'query',
|
|
418
|
+
schema: { type: 'string' },
|
|
419
|
+
description: 'Field to sort by'
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'mdbFilter',
|
|
423
|
+
in: 'query',
|
|
424
|
+
schema: { type: 'string' },
|
|
425
|
+
description: 'JSON-encoded MongoDB-style filter object'
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'search',
|
|
429
|
+
in: 'query',
|
|
430
|
+
schema: { type: 'string' },
|
|
431
|
+
description: 'Text search query'
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'from',
|
|
435
|
+
in: 'query',
|
|
436
|
+
schema: { type: 'string', format: 'date-time' },
|
|
437
|
+
description: 'Filter records created after this date'
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: 'to',
|
|
441
|
+
in: 'query',
|
|
442
|
+
schema: { type: 'string', format: 'date-time' },
|
|
443
|
+
description: 'Filter records created before this date'
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'fromToField',
|
|
447
|
+
in: 'query',
|
|
448
|
+
schema: { type: 'string' },
|
|
449
|
+
description: 'Field to use for date range filtering (default: createdAt)'
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'ids',
|
|
453
|
+
in: 'query',
|
|
454
|
+
schema: { type: 'string' },
|
|
455
|
+
description: 'Comma-separated list of IDs to filter by'
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'returnCount',
|
|
459
|
+
in: 'query',
|
|
460
|
+
schema: { type: 'boolean' },
|
|
461
|
+
description: 'If true, return only the count of matching records'
|
|
462
|
+
}
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Generate default CRUD operation paths for a model
|
|
467
|
+
*/
|
|
468
|
+
function generateDefaultOperationPaths(modelName, model) {
|
|
469
|
+
var _a;
|
|
470
|
+
var paths = {};
|
|
471
|
+
var singular = getSingularName(modelName);
|
|
472
|
+
var plural = getPluralName(modelName);
|
|
473
|
+
var pascalName = pascalCase(modelName);
|
|
474
|
+
var defaultActions = model.defaultActions || {};
|
|
475
|
+
var description = ((_a = model.info) === null || _a === void 0 ? void 0 : _a.description) || "".concat(pascalName, " resource");
|
|
476
|
+
// CREATE: POST /v1/{singular}
|
|
477
|
+
if (defaultActions.create !== undefined) {
|
|
478
|
+
var pathKey = "/v1/".concat(singular);
|
|
479
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
480
|
+
paths[pathKey].post = {
|
|
481
|
+
summary: "Create ".concat(singular),
|
|
482
|
+
description: "Creates a new ".concat(singular, ". ").concat(description),
|
|
483
|
+
tags: [pascalName],
|
|
484
|
+
operationId: "create".concat(pascalName),
|
|
485
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
486
|
+
requestBody: {
|
|
487
|
+
required: true,
|
|
488
|
+
content: {
|
|
489
|
+
'application/json': {
|
|
490
|
+
schema: { $ref: "#/components/schemas/".concat(pascalName, "Create") }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
responses: {
|
|
495
|
+
'200': {
|
|
496
|
+
description: "Created ".concat(singular),
|
|
497
|
+
content: {
|
|
498
|
+
'application/json': {
|
|
499
|
+
schema: { $ref: "#/components/schemas/".concat(pascalName) }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
'400': { $ref: '#/components/responses/BadRequest' },
|
|
504
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// CREATE_MANY: POST /v1/{plural}
|
|
509
|
+
if (defaultActions.createMany !== undefined) {
|
|
510
|
+
var pathKey = "/v1/".concat(plural);
|
|
511
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
512
|
+
paths[pathKey].post = {
|
|
513
|
+
summary: "Create multiple ".concat(plural),
|
|
514
|
+
description: "Creates multiple ".concat(plural, " in a single request"),
|
|
515
|
+
tags: [pascalName],
|
|
516
|
+
operationId: "createMany".concat(pascalName),
|
|
517
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
518
|
+
requestBody: {
|
|
519
|
+
required: true,
|
|
520
|
+
content: {
|
|
521
|
+
'application/json': {
|
|
522
|
+
schema: {
|
|
523
|
+
type: 'object',
|
|
524
|
+
properties: {
|
|
525
|
+
create: {
|
|
526
|
+
type: 'array',
|
|
527
|
+
items: { $ref: "#/components/schemas/".concat(pascalName, "Create") },
|
|
528
|
+
description: 'Array of records to create'
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
required: ['create']
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
responses: {
|
|
537
|
+
'200': {
|
|
538
|
+
description: "Created ".concat(plural),
|
|
539
|
+
content: {
|
|
540
|
+
'application/json': {
|
|
541
|
+
schema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {
|
|
544
|
+
created: {
|
|
545
|
+
type: 'array',
|
|
546
|
+
items: { $ref: "#/components/schemas/".concat(pascalName) }
|
|
547
|
+
},
|
|
548
|
+
errors: {
|
|
549
|
+
type: 'array',
|
|
550
|
+
items: {
|
|
551
|
+
type: 'object',
|
|
552
|
+
properties: {
|
|
553
|
+
index: { type: 'integer' },
|
|
554
|
+
error: { type: 'string' }
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
'400': { $ref: '#/components/responses/BadRequest' },
|
|
564
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// READ by ID: GET /v1/{singular}/{id}
|
|
569
|
+
if (defaultActions.read !== undefined) {
|
|
570
|
+
var pathKey = "/v1/".concat(singular, "/{id}");
|
|
571
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
572
|
+
paths[pathKey].get = {
|
|
573
|
+
summary: "Get ".concat(singular, " by ID"),
|
|
574
|
+
description: "Retrieves a single ".concat(singular, " by its ID"),
|
|
575
|
+
tags: [pascalName],
|
|
576
|
+
operationId: "get".concat(pascalName, "ById"),
|
|
577
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
578
|
+
parameters: [
|
|
579
|
+
{
|
|
580
|
+
name: 'id',
|
|
581
|
+
in: 'path',
|
|
582
|
+
required: true,
|
|
583
|
+
schema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' },
|
|
584
|
+
description: 'The unique identifier of the record'
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
responses: {
|
|
588
|
+
'200': {
|
|
589
|
+
description: "".concat(pascalName, " record"),
|
|
590
|
+
content: {
|
|
591
|
+
'application/json': {
|
|
592
|
+
schema: { $ref: "#/components/schemas/".concat(pascalName) }
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
'401': { $ref: '#/components/responses/Unauthorized' },
|
|
597
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// READ_MANY: GET /v1/{plural}
|
|
602
|
+
if (defaultActions.readMany !== undefined) {
|
|
603
|
+
var pathKey = "/v1/".concat(plural);
|
|
604
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
605
|
+
paths[pathKey].get = {
|
|
606
|
+
summary: "List ".concat(plural),
|
|
607
|
+
description: "Retrieves a list of ".concat(plural, " with optional filtering and pagination"),
|
|
608
|
+
tags: [pascalName],
|
|
609
|
+
operationId: "list".concat(pascalName),
|
|
610
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
611
|
+
parameters: getReadManyParameters(),
|
|
612
|
+
responses: {
|
|
613
|
+
'200': {
|
|
614
|
+
description: "List of ".concat(plural),
|
|
615
|
+
content: {
|
|
616
|
+
'application/json': {
|
|
617
|
+
schema: {
|
|
618
|
+
oneOf: [
|
|
619
|
+
{
|
|
620
|
+
type: 'array',
|
|
621
|
+
items: { $ref: "#/components/schemas/".concat(pascalName) }
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
type: 'object',
|
|
625
|
+
properties: {
|
|
626
|
+
count: { type: 'integer', description: 'Total count when returnCount=true' }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
]
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// UPDATE: PATCH /v1/{singular}/{id}
|
|
639
|
+
if (defaultActions.update !== undefined) {
|
|
640
|
+
var pathKey = "/v1/".concat(singular, "/{id}");
|
|
641
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
642
|
+
paths[pathKey].patch = {
|
|
643
|
+
summary: "Update ".concat(singular),
|
|
644
|
+
description: "Updates a ".concat(singular, " by ID"),
|
|
645
|
+
tags: [pascalName],
|
|
646
|
+
operationId: "update".concat(pascalName),
|
|
647
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
648
|
+
parameters: [
|
|
649
|
+
{
|
|
650
|
+
name: 'id',
|
|
651
|
+
in: 'path',
|
|
652
|
+
required: true,
|
|
653
|
+
schema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' },
|
|
654
|
+
description: 'The unique identifier of the record to update'
|
|
655
|
+
}
|
|
656
|
+
],
|
|
657
|
+
requestBody: {
|
|
658
|
+
required: true,
|
|
659
|
+
content: {
|
|
660
|
+
'application/json': {
|
|
661
|
+
schema: {
|
|
662
|
+
type: 'object',
|
|
663
|
+
properties: {
|
|
664
|
+
updates: { $ref: "#/components/schemas/".concat(pascalName, "Update") },
|
|
665
|
+
options: {
|
|
666
|
+
type: 'object',
|
|
667
|
+
properties: {
|
|
668
|
+
replaceObjectFields: {
|
|
669
|
+
type: 'boolean',
|
|
670
|
+
description: 'If true, replace object fields entirely instead of merging'
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
required: ['updates']
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
responses: {
|
|
681
|
+
'200': {
|
|
682
|
+
description: "Updated ".concat(singular),
|
|
683
|
+
content: {
|
|
684
|
+
'application/json': {
|
|
685
|
+
schema: { $ref: "#/components/schemas/".concat(pascalName) }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
'400': { $ref: '#/components/responses/BadRequest' },
|
|
690
|
+
'401': { $ref: '#/components/responses/Unauthorized' },
|
|
691
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
// DELETE: DELETE /v1/{singular}/{id}
|
|
696
|
+
if (defaultActions.delete !== undefined) {
|
|
697
|
+
var pathKey = "/v1/".concat(singular, "/{id}");
|
|
698
|
+
paths[pathKey] = paths[pathKey] || {};
|
|
699
|
+
paths[pathKey].delete = {
|
|
700
|
+
summary: "Delete ".concat(singular),
|
|
701
|
+
description: "Deletes a ".concat(singular, " by ID"),
|
|
702
|
+
tags: [pascalName],
|
|
703
|
+
operationId: "delete".concat(pascalName),
|
|
704
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
705
|
+
parameters: [
|
|
706
|
+
{
|
|
707
|
+
name: 'id',
|
|
708
|
+
in: 'path',
|
|
709
|
+
required: true,
|
|
710
|
+
schema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' },
|
|
711
|
+
description: 'The unique identifier of the record to delete'
|
|
712
|
+
}
|
|
713
|
+
],
|
|
714
|
+
responses: {
|
|
715
|
+
'204': { description: 'Successfully deleted' },
|
|
716
|
+
'401': { $ref: '#/components/responses/Unauthorized' },
|
|
717
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
return paths;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Map CRUD access type to default HTTP method
|
|
725
|
+
*/
|
|
726
|
+
function getDefaultMethodForAccess(access) {
|
|
727
|
+
switch (access) {
|
|
728
|
+
case 'create': return 'post';
|
|
729
|
+
case 'read': return 'get';
|
|
730
|
+
case 'update': return 'patch';
|
|
731
|
+
case 'delete': return 'delete';
|
|
732
|
+
default: return 'post';
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Generate paths for custom actions
|
|
737
|
+
*/
|
|
738
|
+
function generateCustomActionPaths(modelName, customActions) {
|
|
739
|
+
var paths = {};
|
|
740
|
+
var singular = getSingularName(modelName);
|
|
741
|
+
var pascalName = pascalCase(modelName);
|
|
742
|
+
for (var _i = 0, _a = Object.entries(customActions); _i < _a.length; _i++) {
|
|
743
|
+
var _b = _a[_i], actionName = _b[0], action = _b[1];
|
|
744
|
+
// Determine the path
|
|
745
|
+
var actionPath = void 0;
|
|
746
|
+
if (action.path) {
|
|
747
|
+
actionPath = action.path.startsWith('/v1') ? action.path : "/v1".concat(action.path);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
// Generate path from action name
|
|
751
|
+
var safeName = actionName.replace(/_/g, '-');
|
|
752
|
+
actionPath = "/v1/".concat(singular, "/").concat(safeName);
|
|
753
|
+
}
|
|
754
|
+
// Determine HTTP method
|
|
755
|
+
var method = (action.method || getDefaultMethodForAccess(action.access)).toLowerCase();
|
|
756
|
+
if (!['get', 'post', 'patch', 'put', 'delete'].includes(method))
|
|
757
|
+
continue;
|
|
758
|
+
// Build operation
|
|
759
|
+
var operation = {
|
|
760
|
+
summary: action.name || actionName.replace(/_/g, ' '),
|
|
761
|
+
description: buildActionDescription(action),
|
|
762
|
+
tags: [pascalName],
|
|
763
|
+
operationId: "".concat(modelName, "_").concat(actionName),
|
|
764
|
+
security: action.enduserOnly
|
|
765
|
+
? [{ enduserAuth: [] }]
|
|
766
|
+
: [{ bearerAuth: [] }, { apiKey: [] }],
|
|
767
|
+
responses: {}
|
|
768
|
+
};
|
|
769
|
+
// Add admin-only note
|
|
770
|
+
if (action.adminOnly) {
|
|
771
|
+
operation.description = "**Admin only.** ".concat(operation.description || '');
|
|
772
|
+
}
|
|
773
|
+
if (action.rootAdminOnly) {
|
|
774
|
+
operation.description = "**Root admin only.** ".concat(operation.description || '');
|
|
775
|
+
}
|
|
776
|
+
// Generate parameters
|
|
777
|
+
var _c = generateActionParameters(action, method, actionPath), pathParams = _c.pathParams, queryParams = _c.queryParams, bodySchema = _c.bodySchema;
|
|
778
|
+
if (pathParams.length > 0 || queryParams.length > 0) {
|
|
779
|
+
operation.parameters = __spreadArray(__spreadArray([], pathParams, true), queryParams, true);
|
|
780
|
+
}
|
|
781
|
+
if (bodySchema && ['post', 'patch', 'put'].includes(method)) {
|
|
782
|
+
operation.requestBody = {
|
|
783
|
+
required: true,
|
|
784
|
+
content: {
|
|
785
|
+
'application/json': { schema: bodySchema }
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
// Generate responses
|
|
790
|
+
operation.responses = generateActionResponses(action, pascalName);
|
|
791
|
+
// Add to paths
|
|
792
|
+
paths[actionPath] = paths[actionPath] || {};
|
|
793
|
+
paths[actionPath][method] = operation;
|
|
794
|
+
}
|
|
795
|
+
return paths;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Build description for a custom action including warnings and notes
|
|
799
|
+
*/
|
|
800
|
+
function buildActionDescription(action) {
|
|
801
|
+
var description = action.description || '';
|
|
802
|
+
if (action.warnings && action.warnings.length > 0) {
|
|
803
|
+
description += '\n\n**Warnings:**\n' + action.warnings.map(function (w) { return "- ".concat(w); }).join('\n');
|
|
804
|
+
}
|
|
805
|
+
if (action.notes && action.notes.length > 0) {
|
|
806
|
+
description += '\n\n**Notes:**\n' + action.notes.map(function (n) { return "- ".concat(n); }).join('\n');
|
|
807
|
+
}
|
|
808
|
+
return description.trim();
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Generate parameters for a custom action
|
|
812
|
+
*/
|
|
813
|
+
function generateActionParameters(action, method, actionPath) {
|
|
814
|
+
var pathParams = [];
|
|
815
|
+
var queryParams = [];
|
|
816
|
+
var bodyProperties = {};
|
|
817
|
+
var requiredBody = [];
|
|
818
|
+
// Extract path parameters from the path
|
|
819
|
+
var pathParamMatches = actionPath.match(/\{(\w+)\}/g) || [];
|
|
820
|
+
var pathParamNames = pathParamMatches.map(function (m) { return m.slice(1, -1); });
|
|
821
|
+
// Process action parameters
|
|
822
|
+
if (action.parameters) {
|
|
823
|
+
for (var _i = 0, _a = Object.entries(action.parameters); _i < _a.length; _i++) {
|
|
824
|
+
var _b = _a[_i], paramName = _b[0], paramInfo = _b[1];
|
|
825
|
+
var info = paramInfo;
|
|
826
|
+
try {
|
|
827
|
+
var schema_2 = validatorToOpenAPIType(info.validator);
|
|
828
|
+
if (pathParamNames.includes(paramName)) {
|
|
829
|
+
pathParams.push({
|
|
830
|
+
name: paramName,
|
|
831
|
+
in: 'path',
|
|
832
|
+
required: true,
|
|
833
|
+
schema: schema_2
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
else if (method === 'get' || method === 'delete') {
|
|
837
|
+
// GET/DELETE use query parameters
|
|
838
|
+
queryParams.push({
|
|
839
|
+
name: paramName,
|
|
840
|
+
in: 'query',
|
|
841
|
+
required: !!info.required,
|
|
842
|
+
schema: schema_2
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// POST/PATCH/PUT use request body
|
|
847
|
+
bodyProperties[paramName] = schema_2;
|
|
848
|
+
if (info.required) {
|
|
849
|
+
requiredBody.push(paramName);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
catch (e) {
|
|
854
|
+
// Skip parameters with invalid validators
|
|
855
|
+
bodyProperties[paramName] = { type: 'object', additionalProperties: true };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
var bodySchema = Object.keys(bodyProperties).length > 0
|
|
860
|
+
? {
|
|
861
|
+
type: 'object',
|
|
862
|
+
properties: bodyProperties,
|
|
863
|
+
required: requiredBody.length > 0 ? requiredBody : undefined
|
|
864
|
+
}
|
|
865
|
+
: null;
|
|
866
|
+
return { pathParams: pathParams, queryParams: queryParams, bodySchema: bodySchema };
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Generate response schemas for a custom action
|
|
870
|
+
*/
|
|
871
|
+
function generateActionResponses(action, pascalName) {
|
|
872
|
+
var responses = {
|
|
873
|
+
'400': { $ref: '#/components/responses/BadRequest' },
|
|
874
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
875
|
+
};
|
|
876
|
+
// Handle returns field
|
|
877
|
+
if (!action.returns) {
|
|
878
|
+
responses['200'] = { description: 'Success' };
|
|
879
|
+
return responses;
|
|
880
|
+
}
|
|
881
|
+
// Handle string reference to a model (e.g., returns: 'meeting')
|
|
882
|
+
if (typeof action.returns === 'string') {
|
|
883
|
+
var modelRef = pascalCase(action.returns);
|
|
884
|
+
responses['200'] = {
|
|
885
|
+
description: 'Success',
|
|
886
|
+
content: {
|
|
887
|
+
'application/json': {
|
|
888
|
+
schema: { $ref: "#/components/schemas/".concat(modelRef) }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
return responses;
|
|
893
|
+
}
|
|
894
|
+
// Handle empty object
|
|
895
|
+
if (typeof action.returns === 'object' && Object.keys(action.returns).length === 0) {
|
|
896
|
+
responses['200'] = { description: 'Success' };
|
|
897
|
+
return responses;
|
|
898
|
+
}
|
|
899
|
+
// Check if returns has a 'validator' property directly (single field return type)
|
|
900
|
+
if (typeof action.returns === 'object' && 'validator' in action.returns) {
|
|
901
|
+
var returnInfo = action.returns;
|
|
902
|
+
try {
|
|
903
|
+
responses['200'] = {
|
|
904
|
+
description: 'Success',
|
|
905
|
+
content: {
|
|
906
|
+
'application/json': {
|
|
907
|
+
schema: validatorToOpenAPIType(returnInfo.validator)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
catch (e) {
|
|
913
|
+
responses['200'] = { description: 'Success' };
|
|
914
|
+
}
|
|
915
|
+
return responses;
|
|
916
|
+
}
|
|
917
|
+
// Returns is ModelFields (object with multiple fields)
|
|
918
|
+
var properties = {};
|
|
919
|
+
var required = [];
|
|
920
|
+
for (var _i = 0, _a = Object.entries(action.returns); _i < _a.length; _i++) {
|
|
921
|
+
var _b = _a[_i], fieldName = _b[0], fieldInfo = _b[1];
|
|
922
|
+
// Skip if fieldInfo is not an object with validator
|
|
923
|
+
if (typeof fieldInfo !== 'object' || fieldInfo === null)
|
|
924
|
+
continue;
|
|
925
|
+
var info = fieldInfo;
|
|
926
|
+
// Check if this field has a validator
|
|
927
|
+
if (!info.validator) {
|
|
928
|
+
properties[fieldName] = { type: 'object', additionalProperties: true };
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
properties[fieldName] = validatorToOpenAPIType(info.validator);
|
|
933
|
+
if (info.required) {
|
|
934
|
+
required.push(fieldName);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch (e) {
|
|
938
|
+
properties[fieldName] = { type: 'object', additionalProperties: true };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (Object.keys(properties).length === 0) {
|
|
942
|
+
responses['200'] = { description: 'Success' };
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
responses['200'] = {
|
|
946
|
+
description: 'Success',
|
|
947
|
+
content: {
|
|
948
|
+
'application/json': {
|
|
949
|
+
schema: {
|
|
950
|
+
type: 'object',
|
|
951
|
+
properties: properties,
|
|
952
|
+
required: required.length > 0 ? required : undefined
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return responses;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Merge path items, combining operations from multiple sources
|
|
962
|
+
*/
|
|
963
|
+
function mergePaths(existing, newPaths) {
|
|
964
|
+
var result = __assign({}, existing);
|
|
965
|
+
for (var _i = 0, _a = Object.entries(newPaths); _i < _a.length; _i++) {
|
|
966
|
+
var _b = _a[_i], pathKey = _b[0], methods = _b[1];
|
|
967
|
+
if (result[pathKey]) {
|
|
968
|
+
result[pathKey] = __assign(__assign({}, result[pathKey]), methods);
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
result[pathKey] = methods;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
// ============================================================================
|
|
977
|
+
// Security Schemes
|
|
978
|
+
// ============================================================================
|
|
979
|
+
function generateSecuritySchemes() {
|
|
980
|
+
return {
|
|
981
|
+
bearerAuth: {
|
|
982
|
+
type: 'http',
|
|
983
|
+
scheme: 'bearer',
|
|
984
|
+
bearerFormat: 'JWT',
|
|
985
|
+
description: 'JWT token obtained from user login. Pass as Authorization header: Bearer <token>'
|
|
986
|
+
},
|
|
987
|
+
apiKey: {
|
|
988
|
+
type: 'apiKey',
|
|
989
|
+
in: 'header',
|
|
990
|
+
name: 'Authorization',
|
|
991
|
+
description: 'API key for service accounts. Pass as Authorization header with format: API_KEY {your_key}'
|
|
992
|
+
},
|
|
993
|
+
enduserAuth: {
|
|
994
|
+
type: 'http',
|
|
995
|
+
scheme: 'bearer',
|
|
996
|
+
bearerFormat: 'JWT',
|
|
997
|
+
description: 'JWT token for enduser (patient) authentication'
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
// ============================================================================
|
|
1002
|
+
// Main Generator
|
|
1003
|
+
// ============================================================================
|
|
1004
|
+
/**
|
|
1005
|
+
* Generate the complete OpenAPI specification
|
|
1006
|
+
*/
|
|
1007
|
+
function generateOpenAPISpec() {
|
|
1008
|
+
var _a;
|
|
1009
|
+
var spec = {
|
|
1010
|
+
openapi: '3.0.3',
|
|
1011
|
+
info: {
|
|
1012
|
+
title: 'Tellescope API',
|
|
1013
|
+
version: '1.0.0',
|
|
1014
|
+
description: "Healthcare platform API for patient management, communications, and automation.\n\n## Authentication\n\nThe API supports multiple authentication methods:\n\n- **Bearer Token (JWT)**: Obtain a token via login and pass as `Authorization: Bearer <token>`\n- **API Key**: Pass as header `Authorization: API_KEY <key>`\n\n## Pagination\n\nList endpoints use cursor-based pagination:\n- `limit`: Maximum records to return (default varies, max 1000)\n- `lastId`: ID of the last record from previous page\n\n## Filtering\n\nUse the `mdbFilter` query parameter with JSON-encoded MongoDB-style queries:\n```\n?mdbFilter={\"status\":\"active\",\"priority\":{\"$in\":[\"high\",\"urgent\"]}}\n```\n\nSupported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`, `$or`, `$and`\n",
|
|
1015
|
+
contact: {
|
|
1016
|
+
email: 'support@tellescope.com',
|
|
1017
|
+
url: 'https://tellescope.com'
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
servers: [
|
|
1021
|
+
{ url: 'https://api.tellescope.com', description: 'Production' },
|
|
1022
|
+
{ url: 'https://staging-api.tellescope.com', description: 'Staging' }
|
|
1023
|
+
],
|
|
1024
|
+
paths: {},
|
|
1025
|
+
components: {
|
|
1026
|
+
schemas: {},
|
|
1027
|
+
securitySchemes: generateSecuritySchemes(),
|
|
1028
|
+
responses: {
|
|
1029
|
+
BadRequest: {
|
|
1030
|
+
description: 'Bad Request - Invalid input or validation error',
|
|
1031
|
+
content: {
|
|
1032
|
+
'application/json': {
|
|
1033
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
Unauthorized: {
|
|
1038
|
+
description: 'Unauthorized - Invalid or missing authentication',
|
|
1039
|
+
content: {
|
|
1040
|
+
'application/json': {
|
|
1041
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
NotFound: {
|
|
1046
|
+
description: 'Not Found - Resource does not exist',
|
|
1047
|
+
content: {
|
|
1048
|
+
'application/json': {
|
|
1049
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
tags: [],
|
|
1056
|
+
security: [{ bearerAuth: [] }, { apiKey: [] }]
|
|
1057
|
+
};
|
|
1058
|
+
// Generate schemas
|
|
1059
|
+
spec.components.schemas = generateComponents();
|
|
1060
|
+
// Generate paths for each model
|
|
1061
|
+
for (var _i = 0, _b = Object.entries(schema); _i < _b.length; _i++) {
|
|
1062
|
+
var _c = _b[_i], modelName = _c[0], modelDef = _c[1];
|
|
1063
|
+
var model = modelDef;
|
|
1064
|
+
// Add tag for this model
|
|
1065
|
+
spec.tags.push({
|
|
1066
|
+
name: pascalCase(modelName),
|
|
1067
|
+
description: ((_a = model.info) === null || _a === void 0 ? void 0 : _a.description) || "".concat(pascalCase(modelName), " resource operations")
|
|
1068
|
+
});
|
|
1069
|
+
// Generate default CRUD paths
|
|
1070
|
+
var crudPaths = generateDefaultOperationPaths(modelName, model);
|
|
1071
|
+
spec.paths = mergePaths(spec.paths, crudPaths);
|
|
1072
|
+
// Generate custom action paths
|
|
1073
|
+
if (model.customActions && Object.keys(model.customActions).length > 0) {
|
|
1074
|
+
var customPaths = generateCustomActionPaths(modelName, model.customActions);
|
|
1075
|
+
spec.paths = mergePaths(spec.paths, customPaths);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// Sort tags alphabetically
|
|
1079
|
+
spec.tags.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
1080
|
+
// Sort paths alphabetically
|
|
1081
|
+
var sortedPaths = {};
|
|
1082
|
+
for (var _d = 0, _e = Object.keys(spec.paths).sort(); _d < _e.length; _d++) {
|
|
1083
|
+
var key = _e[_d];
|
|
1084
|
+
sortedPaths[key] = spec.paths[key];
|
|
1085
|
+
}
|
|
1086
|
+
spec.paths = sortedPaths;
|
|
1087
|
+
return spec;
|
|
1088
|
+
}
|
|
1089
|
+
// ============================================================================
|
|
1090
|
+
// CLI Execution
|
|
1091
|
+
// ============================================================================
|
|
1092
|
+
function main() {
|
|
1093
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
1094
|
+
var spec, outputPath, absolutePath, pathCount, schemaCount, tagCount;
|
|
1095
|
+
return __generator(this, function (_a) {
|
|
1096
|
+
console.log('Generating OpenAPI specification from Tellescope schema...\n');
|
|
1097
|
+
spec = generateOpenAPISpec();
|
|
1098
|
+
outputPath = process.argv[2] || path.join(__dirname, '..', 'openapi.json');
|
|
1099
|
+
absolutePath = path.isAbsolute(outputPath) ? outputPath : path.resolve(process.cwd(), outputPath);
|
|
1100
|
+
// Write the spec
|
|
1101
|
+
fs.writeFileSync(absolutePath, JSON.stringify(spec, null, 2), 'utf-8');
|
|
1102
|
+
pathCount = Object.keys(spec.paths).length;
|
|
1103
|
+
schemaCount = Object.keys(spec.components.schemas).length;
|
|
1104
|
+
tagCount = spec.tags.length;
|
|
1105
|
+
console.log('OpenAPI specification generated successfully!');
|
|
1106
|
+
console.log(" Output: ".concat(absolutePath));
|
|
1107
|
+
console.log(" Paths: ".concat(pathCount));
|
|
1108
|
+
console.log(" Schemas: ".concat(schemaCount));
|
|
1109
|
+
console.log(" Tags (models): ".concat(tagCount));
|
|
1110
|
+
return [2 /*return*/];
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
// Run if executed directly
|
|
1115
|
+
if (require.main === module) {
|
|
1116
|
+
main().catch(function (err) {
|
|
1117
|
+
console.error('Error generating OpenAPI spec:', err);
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
// Export for programmatic use
|
|
1122
|
+
export { generateOpenAPISpec };
|
|
1123
|
+
//# sourceMappingURL=generate-openapi.js.map
|