@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,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