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