@zenstackhq/openapi 1.0.0-alpha.99 → 1.0.0-beta.2

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,843 @@
1
+ "use strict";
2
+ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || function (mod) {
20
+ if (mod && mod.__esModule) return mod;
21
+ var result = {};
22
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
+ __setModuleDefault(result, mod);
24
+ return result;
25
+ };
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.RESTfulOpenAPIGenerator = void 0;
31
+ const sdk_1 = require("@zenstackhq/sdk");
32
+ const ast_1 = require("@zenstackhq/sdk/ast");
33
+ const fs = __importStar(require("fs"));
34
+ const lower_case_first_1 = require("lower-case-first");
35
+ const path = __importStar(require("path"));
36
+ const pluralize_1 = __importDefault(require("pluralize"));
37
+ const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
38
+ const yaml_1 = __importDefault(require("yaml"));
39
+ const generator_base_1 = require("./generator-base");
40
+ const meta_1 = require("./meta");
41
+ /**
42
+ * Generates RESTful style OpenAPI specification.
43
+ */
44
+ class RESTfulOpenAPIGenerator extends generator_base_1.OpenAPIGeneratorBase {
45
+ constructor() {
46
+ super(...arguments);
47
+ this.warnings = [];
48
+ }
49
+ generate() {
50
+ let output = (0, sdk_1.requireOption)(this.options, 'output');
51
+ output = (0, sdk_1.resolvePath)(output, this.options);
52
+ const components = this.generateComponents();
53
+ const paths = this.generatePaths();
54
+ // prune unused component schemas
55
+ this.pruneComponents(paths, components);
56
+ // generate security schemes, and root-level security
57
+ components.securitySchemes = this.generateSecuritySchemes();
58
+ let security = undefined;
59
+ if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) {
60
+ security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] }));
61
+ }
62
+ const openapi = {
63
+ openapi: this.getOption('specVersion', '3.1.0'),
64
+ info: {
65
+ title: this.getOption('title', 'ZenStack Generated API'),
66
+ version: this.getOption('version', '1.0.0'),
67
+ description: this.getOption('description'),
68
+ summary: this.getOption('summary'),
69
+ },
70
+ tags: this.includedModels.map((model) => {
71
+ var _a;
72
+ const meta = (0, meta_1.getModelResourceMeta)(model);
73
+ return {
74
+ name: (0, lower_case_first_1.lowerCaseFirst)(model.name),
75
+ description: (_a = meta === null || meta === void 0 ? void 0 : meta.tagDescription) !== null && _a !== void 0 ? _a : `${model.name} operations`,
76
+ };
77
+ }),
78
+ paths,
79
+ components,
80
+ security,
81
+ };
82
+ const ext = path.extname(output);
83
+ if (ext && (ext.toLowerCase() === '.yaml' || ext.toLowerCase() === '.yml')) {
84
+ fs.writeFileSync(output, yaml_1.default.stringify(openapi));
85
+ }
86
+ else {
87
+ fs.writeFileSync(output, JSON.stringify(openapi, undefined, 2));
88
+ }
89
+ return this.warnings;
90
+ }
91
+ generatePaths() {
92
+ let result = {};
93
+ const includeModelNames = this.includedModels.map((d) => d.name);
94
+ for (const model of this.dmmf.datamodel.models) {
95
+ if (includeModelNames.includes(model.name)) {
96
+ const zmodel = this.model.declarations.find((d) => (0, ast_1.isDataModel)(d) && d.name === model.name);
97
+ if (zmodel) {
98
+ result = Object.assign(Object.assign({}, result), this.generatePathsForModel(model, zmodel));
99
+ }
100
+ else {
101
+ this.warnings.push(`Unable to load ZModel definition for: ${model.name}}`);
102
+ }
103
+ }
104
+ }
105
+ return result;
106
+ }
107
+ generatePathsForModel(model, zmodel) {
108
+ var _a;
109
+ const result = {};
110
+ // analyze access policies to determine default security
111
+ const policies = (0, sdk_1.analyzePolicies)(zmodel);
112
+ let prefix = this.getOption('prefix', '');
113
+ if (prefix.endsWith('/')) {
114
+ prefix = prefix.substring(0, prefix.length - 1);
115
+ }
116
+ const resourceMeta = (0, meta_1.getModelResourceMeta)(zmodel);
117
+ // GET /resource
118
+ // POST /resource
119
+ result[`${prefix}/${(0, lower_case_first_1.lowerCaseFirst)(model.name)}`] = {
120
+ get: this.makeResourceList(zmodel, policies, resourceMeta),
121
+ post: this.makeResourceCreate(zmodel, policies, resourceMeta),
122
+ };
123
+ // GET /resource/{id}
124
+ // PUT /resource/{id}
125
+ // PATCH /resource/{id}
126
+ // DELETE /resource/{id}
127
+ result[`${prefix}/${(0, lower_case_first_1.lowerCaseFirst)(model.name)}/{id}`] = {
128
+ get: this.makeResourceFetch(zmodel, policies, resourceMeta),
129
+ put: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-put`, resourceMeta),
130
+ patch: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-patch`, resourceMeta),
131
+ delete: this.makeResourceDelete(zmodel, policies, resourceMeta),
132
+ };
133
+ // paths for related resources and relationships
134
+ for (const field of zmodel.fields) {
135
+ const relationDecl = (_a = field.type.reference) === null || _a === void 0 ? void 0 : _a.ref;
136
+ if (!(0, ast_1.isDataModel)(relationDecl)) {
137
+ continue;
138
+ }
139
+ // GET /resource/{id}/{relationship}
140
+ const relatedDataPath = `${prefix}/${(0, lower_case_first_1.lowerCaseFirst)(model.name)}/{id}/${field.name}`;
141
+ let container = result[relatedDataPath];
142
+ if (!container) {
143
+ container = result[relatedDataPath] = {};
144
+ }
145
+ container.get = this.makeRelatedFetch(zmodel, field, relationDecl, resourceMeta);
146
+ const relationshipPath = `${prefix}/${(0, lower_case_first_1.lowerCaseFirst)(model.name)}/{id}/relationships/${field.name}`;
147
+ container = result[relationshipPath];
148
+ if (!container) {
149
+ container = result[relationshipPath] = {};
150
+ }
151
+ // GET /resource/{id}/relationships/{relationship}
152
+ container.get = this.makeRelationshipFetch(zmodel, field, policies, resourceMeta);
153
+ // PUT /resource/{id}/relationships/{relationship}
154
+ container.put = this.makeRelationshipUpdate(zmodel, field, policies, `update-${model.name}-relationship-${field.name}-put`, resourceMeta);
155
+ // PATCH /resource/{id}/relationships/{relationship}
156
+ container.patch = this.makeRelationshipUpdate(zmodel, field, policies, `update-${model.name}-relationship-${field.name}-patch`, resourceMeta);
157
+ if (field.type.array) {
158
+ // POST /resource/{id}/relationships/{relationship}
159
+ container.post = this.makeRelationshipCreate(zmodel, field, policies, resourceMeta);
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ makeResourceList(model, policies, resourceMeta) {
165
+ var _a;
166
+ return {
167
+ operationId: `list-${model.name}`,
168
+ description: `List "${model.name}" resources`,
169
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
170
+ parameters: [
171
+ this.parameter('include'),
172
+ this.parameter('sort'),
173
+ this.parameter('page-offset'),
174
+ this.parameter('page-limit'),
175
+ ...this.generateFilterParameters(model),
176
+ ],
177
+ responses: {
178
+ '200': this.success(`${model.name}ListResponse`),
179
+ '403': this.forbidden(),
180
+ },
181
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.read === true) ? [] : undefined,
182
+ };
183
+ }
184
+ makeResourceCreate(model, policies, resourceMeta) {
185
+ var _a;
186
+ return {
187
+ operationId: `create-${model.name}`,
188
+ description: `Create a "${model.name}" resource`,
189
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
190
+ requestBody: {
191
+ content: {
192
+ 'application/vnd.api+json': {
193
+ schema: this.ref(`${model.name}CreateRequest`),
194
+ },
195
+ },
196
+ },
197
+ responses: {
198
+ '201': this.success(`${model.name}Response`),
199
+ '403': this.forbidden(),
200
+ },
201
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.create === true) ? [] : undefined,
202
+ };
203
+ }
204
+ makeResourceFetch(model, policies, resourceMeta) {
205
+ var _a;
206
+ return {
207
+ operationId: `fetch-${model.name}`,
208
+ description: `Fetch a "${model.name}" resource`,
209
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
210
+ parameters: [this.parameter('id'), this.parameter('include')],
211
+ responses: {
212
+ '200': this.success(`${model.name}Response`),
213
+ '403': this.forbidden(),
214
+ '404': this.notFound(),
215
+ },
216
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.read === true) ? [] : undefined,
217
+ };
218
+ }
219
+ makeRelatedFetch(model, field, relationDecl, resourceMeta) {
220
+ var _a;
221
+ const policies = (0, sdk_1.analyzePolicies)(relationDecl);
222
+ const parameters = [this.parameter('id'), this.parameter('include')];
223
+ if (field.type.array) {
224
+ parameters.push(this.parameter('sort'), this.parameter('page-offset'), this.parameter('page-limit'), ...this.generateFilterParameters(model));
225
+ }
226
+ const result = {
227
+ operationId: `fetch-${model.name}-related-${field.name}`,
228
+ description: `Fetch the related "${field.name}" resource for "${model.name}"`,
229
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
230
+ parameters,
231
+ responses: {
232
+ '200': this.success(field.type.array ? `${relationDecl.name}ListResponse` : `${relationDecl.name}Response`),
233
+ '403': this.forbidden(),
234
+ '404': this.notFound(),
235
+ },
236
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.read === true) ? [] : undefined,
237
+ };
238
+ return result;
239
+ }
240
+ makeResourceUpdate(model, policies, operationId, resourceMeta) {
241
+ var _a;
242
+ return {
243
+ operationId,
244
+ description: `Update a "${model.name}" resource`,
245
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
246
+ parameters: [this.parameter('id')],
247
+ requestBody: {
248
+ content: {
249
+ 'application/vnd.api+json': {
250
+ schema: this.ref(`${model.name}UpdateRequest`),
251
+ },
252
+ },
253
+ },
254
+ responses: {
255
+ '200': this.success(`${model.name}Response`),
256
+ '403': this.forbidden(),
257
+ '404': this.notFound(),
258
+ },
259
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.update === true) ? [] : undefined,
260
+ };
261
+ }
262
+ makeResourceDelete(model, policies, resourceMeta) {
263
+ var _a;
264
+ return {
265
+ operationId: `delete-${model.name}`,
266
+ description: `Delete a "${model.name}" resource`,
267
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
268
+ parameters: [this.parameter('id')],
269
+ responses: {
270
+ '200': this.success(),
271
+ '403': this.forbidden(),
272
+ '404': this.notFound(),
273
+ },
274
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.delete === true) ? [] : undefined,
275
+ };
276
+ }
277
+ makeRelationshipFetch(model, field, policies, resourceMeta) {
278
+ var _a;
279
+ const parameters = [this.parameter('id')];
280
+ if (field.type.array) {
281
+ parameters.push(this.parameter('sort'), this.parameter('page-offset'), this.parameter('page-limit'), ...this.generateFilterParameters(model));
282
+ }
283
+ return {
284
+ operationId: `fetch-${model.name}-relationship-${field.name}`,
285
+ description: `Fetch the "${field.name}" relationships for a "${model.name}"`,
286
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
287
+ parameters,
288
+ responses: {
289
+ '200': field.type.array
290
+ ? this.success('_toManyRelationshipResponse')
291
+ : this.success('_toOneRelationshipResponse'),
292
+ '403': this.forbidden(),
293
+ '404': this.notFound(),
294
+ },
295
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.read === true) ? [] : undefined,
296
+ };
297
+ }
298
+ makeRelationshipCreate(model, field, policies, resourceMeta) {
299
+ var _a;
300
+ return {
301
+ operationId: `create-${model.name}-relationship-${field.name}`,
302
+ description: `Create new "${field.name}" relationships for a "${model.name}"`,
303
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
304
+ parameters: [this.parameter('id')],
305
+ requestBody: {
306
+ content: {
307
+ 'application/vnd.api+json': {
308
+ schema: this.ref('_toManyRelationshipRequest'),
309
+ },
310
+ },
311
+ },
312
+ responses: {
313
+ '200': this.success('_toManyRelationshipResponse'),
314
+ '403': this.forbidden(),
315
+ '404': this.notFound(),
316
+ },
317
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.update === true) ? [] : undefined,
318
+ };
319
+ }
320
+ makeRelationshipUpdate(model, field, policies, operationId, resourceMeta) {
321
+ var _a;
322
+ return {
323
+ operationId,
324
+ description: `Update "${field.name}" ${(0, pluralize_1.default)('relationship', field.type.array ? 2 : 1)} for a "${model.name}"`,
325
+ tags: [(0, lower_case_first_1.lowerCaseFirst)(model.name)],
326
+ parameters: [this.parameter('id')],
327
+ requestBody: {
328
+ content: {
329
+ 'application/vnd.api+json': {
330
+ schema: field.type.array
331
+ ? this.ref('_toManyRelationshipRequest')
332
+ : this.ref('_toOneRelationshipRequest'),
333
+ },
334
+ },
335
+ },
336
+ responses: {
337
+ '200': field.type.array
338
+ ? this.success('_toManyRelationshipResponse')
339
+ : this.success('_toOneRelationshipResponse'),
340
+ '403': this.forbidden(),
341
+ '404': this.notFound(),
342
+ },
343
+ security: ((_a = resourceMeta === null || resourceMeta === void 0 ? void 0 : resourceMeta.security) !== null && _a !== void 0 ? _a : policies.update === true) ? [] : undefined,
344
+ };
345
+ }
346
+ generateFilterParameters(model) {
347
+ const result = [];
348
+ for (const field of model.fields) {
349
+ if ((0, sdk_1.isForeignKeyField)(field)) {
350
+ // no filtering with foreign keys because one can filter
351
+ // directly on the relationship
352
+ continue;
353
+ }
354
+ if ((0, sdk_1.isIdField)(field)) {
355
+ // id filter
356
+ result.push(this.makeFilterParameter(field, 'id', 'Id filter'));
357
+ continue;
358
+ }
359
+ // equality filter
360
+ result.push(this.makeFilterParameter(field, '', 'Equality filter', field.type.array));
361
+ if ((0, sdk_1.isRelationshipField)(field)) {
362
+ // TODO: how to express nested filters?
363
+ continue;
364
+ }
365
+ if (field.type.array) {
366
+ // collection filters
367
+ result.push(this.makeFilterParameter(field, '$has', 'Collection contains filter'));
368
+ result.push(this.makeFilterParameter(field, '$hasEvery', 'Collection contains-all filter', true));
369
+ result.push(this.makeFilterParameter(field, '$hasSome', 'Collection contains-any filter', true));
370
+ result.push(this.makeFilterParameter(field, '$isEmpty', 'Collection is empty filter', false, {
371
+ type: 'boolean',
372
+ }));
373
+ }
374
+ else {
375
+ if (field.type.type && ['Int', 'BigInt', 'Float', 'Decimal', 'DateTime'].includes(field.type.type)) {
376
+ // comparison filters
377
+ result.push(this.makeFilterParameter(field, '$lt', 'Less-than filter'));
378
+ result.push(this.makeFilterParameter(field, '$lte', 'Less-than or equal filter'));
379
+ result.push(this.makeFilterParameter(field, '$gt', 'Greater-than filter'));
380
+ result.push(this.makeFilterParameter(field, '$gte', 'Greater-than or equal filter'));
381
+ }
382
+ if (field.type.type === 'String') {
383
+ result.push(this.makeFilterParameter(field, '$contains', 'String contains filter'));
384
+ result.push(this.makeFilterParameter(field, '$icontains', 'String case-insensitive contains filter'));
385
+ result.push(this.makeFilterParameter(field, '$search', 'String full-text search filter'));
386
+ result.push(this.makeFilterParameter(field, '$startsWith', 'String startsWith filter'));
387
+ result.push(this.makeFilterParameter(field, '$endsWith', 'String endsWith filter'));
388
+ }
389
+ }
390
+ }
391
+ return result;
392
+ }
393
+ makeFilterParameter(field, name, description, array = false, schemaOverride) {
394
+ var _a;
395
+ let schema;
396
+ if (schemaOverride) {
397
+ schema = schemaOverride;
398
+ }
399
+ else {
400
+ const fieldDecl = (_a = field.type.reference) === null || _a === void 0 ? void 0 : _a.ref;
401
+ if ((0, ast_1.isEnum)(fieldDecl)) {
402
+ schema = this.ref(fieldDecl.name);
403
+ }
404
+ else if ((0, ast_1.isDataModel)(fieldDecl)) {
405
+ schema = { type: 'string' };
406
+ }
407
+ else {
408
+ (0, tiny_invariant_1.default)(field.type.type);
409
+ schema = this.fieldTypeToOpenAPISchema(field.type);
410
+ }
411
+ }
412
+ if (array) {
413
+ schema = { type: 'array', items: schema };
414
+ }
415
+ return {
416
+ name: name === 'id' ? 'filter[id]' : `filter[${field.name}${name}]`,
417
+ required: false,
418
+ description: name === 'id' ? description : `${description} for "${field.name}"`,
419
+ in: 'query',
420
+ style: 'form',
421
+ explode: false,
422
+ schema,
423
+ };
424
+ }
425
+ generateComponents() {
426
+ const schemas = {};
427
+ const parameters = {};
428
+ const components = {
429
+ schemas,
430
+ parameters,
431
+ };
432
+ for (const [name, value] of Object.entries(this.generateSharedComponents())) {
433
+ schemas[name] = value;
434
+ }
435
+ for (const [name, value] of Object.entries(this.generateParameters())) {
436
+ parameters[name] = value;
437
+ }
438
+ for (const _enum of this.model.declarations.filter((d) => (0, ast_1.isEnum)(d))) {
439
+ schemas[_enum.name] = this.generateEnumComponent(_enum);
440
+ }
441
+ // data models
442
+ for (const model of (0, sdk_1.getDataModels)(this.model)) {
443
+ for (const [name, value] of Object.entries(this.generateDataModelComponents(model))) {
444
+ schemas[name] = value;
445
+ }
446
+ }
447
+ return components;
448
+ }
449
+ generateSharedComponents() {
450
+ return {
451
+ _jsonapi: {
452
+ type: 'object',
453
+ description: 'An object describing the server’s implementation',
454
+ required: ['version'],
455
+ properties: {
456
+ version: { type: 'string' },
457
+ meta: this.ref('_meta'),
458
+ },
459
+ },
460
+ _meta: {
461
+ type: 'object',
462
+ description: 'Meta information about the response',
463
+ additionalProperties: true,
464
+ },
465
+ _resourceIdentifier: {
466
+ type: 'object',
467
+ description: 'Identifier for a resource',
468
+ required: ['type', 'id'],
469
+ properties: {
470
+ type: { type: 'string', description: 'Resource type' },
471
+ id: { type: 'string', description: 'Resource id' },
472
+ },
473
+ },
474
+ _resource: this.allOf(this.ref('_resourceIdentifier'), {
475
+ type: 'object',
476
+ description: 'A resource with attributes and relationships',
477
+ properties: {
478
+ attributes: { type: 'object', description: 'Resource attributes' },
479
+ relationships: { type: 'object', description: 'Resource relationships' },
480
+ },
481
+ }),
482
+ _links: {
483
+ type: 'object',
484
+ required: ['self'],
485
+ description: 'Links related to the resource',
486
+ properties: { self: { type: 'string', description: 'Link for refetching the curent results' } },
487
+ },
488
+ _pagination: {
489
+ type: 'object',
490
+ description: 'Pagination information',
491
+ required: ['first', 'last', 'prev', 'next'],
492
+ properties: {
493
+ first: this.nullable({ type: 'string', description: 'Link to the first page' }),
494
+ last: this.nullable({ type: 'string', description: 'Link to the last page' }),
495
+ prev: this.nullable({ type: 'string', description: 'Link to the previous page' }),
496
+ next: this.nullable({ type: 'string', description: 'Link to the next page' }),
497
+ },
498
+ },
499
+ _errors: {
500
+ type: 'array',
501
+ description: 'An array of error objects',
502
+ items: {
503
+ type: 'object',
504
+ required: ['status', 'code'],
505
+ properties: {
506
+ status: { type: 'string', description: 'HTTP status' },
507
+ code: { type: 'string', description: 'Error code' },
508
+ prismaCode: {
509
+ type: 'string',
510
+ description: 'Prisma error code if the error is thrown by Prisma',
511
+ },
512
+ title: { type: 'string', description: 'Error title' },
513
+ detail: { type: 'string', description: 'Error detail' },
514
+ },
515
+ },
516
+ },
517
+ _errorResponse: {
518
+ type: 'object',
519
+ required: ['errors'],
520
+ description: 'An error response',
521
+ properties: {
522
+ jsonapi: this.ref('_jsonapi'),
523
+ errors: this.ref('_errors'),
524
+ },
525
+ },
526
+ _relationLinks: {
527
+ type: 'object',
528
+ required: ['self', 'related'],
529
+ description: 'Links related to a relationship',
530
+ properties: {
531
+ self: { type: 'string', description: 'Link for fetching this relationship' },
532
+ related: {
533
+ type: 'string',
534
+ description: 'Link for fetching the resource represented by this relationship',
535
+ },
536
+ },
537
+ },
538
+ _toOneRelationship: {
539
+ type: 'object',
540
+ description: 'A to-one relationship',
541
+ properties: {
542
+ data: this.nullable(this.ref('_resourceIdentifier')),
543
+ },
544
+ },
545
+ _toOneRelationshipWithLinks: {
546
+ type: 'object',
547
+ required: ['links', 'data'],
548
+ description: 'A to-one relationship with links',
549
+ properties: {
550
+ links: this.ref('_relationLinks'),
551
+ data: this.nullable(this.ref('_resourceIdentifier')),
552
+ },
553
+ },
554
+ _toManyRelationship: {
555
+ type: 'object',
556
+ required: ['data'],
557
+ description: 'A to-many relationship',
558
+ properties: {
559
+ data: this.array(this.ref('_resourceIdentifier')),
560
+ },
561
+ },
562
+ _toManyRelationshipWithLinks: {
563
+ type: 'object',
564
+ required: ['links', 'data'],
565
+ description: 'A to-many relationship with links',
566
+ properties: {
567
+ links: this.ref('_pagedRelationLinks'),
568
+ data: this.array(this.ref('_resourceIdentifier')),
569
+ },
570
+ },
571
+ _pagedRelationLinks: Object.assign({ description: 'Relationship links with pagination information' }, this.allOf(this.ref('_pagination'), this.ref('_relationLinks'))),
572
+ _toManyRelationshipRequest: {
573
+ type: 'object',
574
+ required: ['data'],
575
+ description: 'Input for manipulating a to-many relationship',
576
+ properties: {
577
+ data: {
578
+ type: 'array',
579
+ items: this.ref('_resourceIdentifier'),
580
+ },
581
+ },
582
+ },
583
+ _toOneRelationshipRequest: Object.assign({ description: 'Input for manipulating a to-one relationship' }, this.nullable({
584
+ type: 'object',
585
+ required: ['data'],
586
+ properties: {
587
+ data: this.ref('_resourceIdentifier'),
588
+ },
589
+ })),
590
+ _toManyRelationshipResponse: Object.assign({ description: 'Response for a to-many relationship' }, this.allOf(this.ref('_toManyRelationshipWithLinks'), {
591
+ type: 'object',
592
+ properties: {
593
+ jsonapi: this.ref('_jsonapi'),
594
+ },
595
+ })),
596
+ _toOneRelationshipResponse: Object.assign({ description: 'Response for a to-one relationship' }, this.allOf(this.ref('_toOneRelationshipWithLinks'), {
597
+ type: 'object',
598
+ properties: {
599
+ jsonapi: this.ref('_jsonapi'),
600
+ },
601
+ })),
602
+ };
603
+ }
604
+ generateParameters() {
605
+ return {
606
+ id: {
607
+ name: 'id',
608
+ in: 'path',
609
+ description: 'The resource id',
610
+ required: true,
611
+ schema: { type: 'string' },
612
+ },
613
+ include: {
614
+ name: 'include',
615
+ in: 'query',
616
+ description: 'Relationships to include',
617
+ required: false,
618
+ style: 'form',
619
+ schema: { type: 'string' },
620
+ },
621
+ sort: {
622
+ name: 'sort',
623
+ in: 'query',
624
+ description: 'Fields to sort by',
625
+ required: false,
626
+ style: 'form',
627
+ schema: { type: 'string' },
628
+ },
629
+ 'page-offset': {
630
+ name: 'page[offset]',
631
+ in: 'query',
632
+ description: 'Offset for pagination',
633
+ required: false,
634
+ style: 'form',
635
+ schema: { type: 'integer' },
636
+ },
637
+ 'page-limit': {
638
+ name: 'page[limit]',
639
+ in: 'query',
640
+ description: 'Limit for pagination',
641
+ required: false,
642
+ style: 'form',
643
+ schema: { type: 'integer' },
644
+ },
645
+ };
646
+ }
647
+ generateEnumComponent(_enum) {
648
+ const schema = {
649
+ type: 'string',
650
+ description: `The "${_enum.name}" Enum`,
651
+ enum: _enum.fields.map((f) => f.name),
652
+ };
653
+ return schema;
654
+ }
655
+ generateDataModelComponents(model) {
656
+ const result = {};
657
+ result[`${model.name}`] = this.generateModelEntity(model, 'read');
658
+ result[`${model.name}CreateRequest`] = {
659
+ type: 'object',
660
+ description: `Input for creating a "${model.name}"`,
661
+ required: ['data'],
662
+ properties: {
663
+ data: this.generateModelEntity(model, 'create'),
664
+ },
665
+ };
666
+ result[`${model.name}UpdateRequest`] = {
667
+ type: 'object',
668
+ description: `Input for updating a "${model.name}"`,
669
+ required: ['data'],
670
+ properties: { data: this.generateModelEntity(model, 'update') },
671
+ };
672
+ const relationships = {};
673
+ for (const field of model.fields) {
674
+ if ((0, sdk_1.isRelationshipField)(field)) {
675
+ if (field.type.array) {
676
+ relationships[field.name] = this.ref('_toManyRelationship');
677
+ }
678
+ else {
679
+ relationships[field.name] = this.ref('_toOneRelationship');
680
+ }
681
+ }
682
+ }
683
+ result[`${model.name}Response`] = {
684
+ type: 'object',
685
+ description: `Response for a "${model.name}"`,
686
+ required: ['data'],
687
+ properties: {
688
+ jsonapi: this.ref('_jsonapi'),
689
+ data: this.allOf(this.ref(`${model.name}`), {
690
+ type: 'object',
691
+ properties: { relationships: { type: 'object', properties: relationships } },
692
+ }),
693
+ included: {
694
+ type: 'array',
695
+ items: this.ref('_resource'),
696
+ },
697
+ links: this.ref('_links'),
698
+ },
699
+ };
700
+ result[`${model.name}ListResponse`] = {
701
+ type: 'object',
702
+ description: `Response for a list of "${model.name}"`,
703
+ required: ['data', 'links'],
704
+ properties: {
705
+ jsonapi: this.ref('_jsonapi'),
706
+ data: this.array(this.allOf(this.ref(`${model.name}`), {
707
+ type: 'object',
708
+ properties: { relationships: { type: 'object', properties: relationships } },
709
+ })),
710
+ included: {
711
+ type: 'array',
712
+ items: this.ref('_resource'),
713
+ },
714
+ links: this.allOf(this.ref('_links'), this.ref('_pagination')),
715
+ },
716
+ };
717
+ return result;
718
+ }
719
+ generateModelEntity(model, mode) {
720
+ var _a;
721
+ const fields = model.fields.filter((f) => !sdk_1.AUXILIARY_FIELDS.includes(f.name) && !(0, sdk_1.isIdField)(f));
722
+ const attributes = {};
723
+ const relationships = {};
724
+ const required = [];
725
+ for (const field of fields) {
726
+ if ((0, sdk_1.isRelationshipField)(field)) {
727
+ let relType;
728
+ if (mode === 'create' || mode === 'update') {
729
+ relType = field.type.array ? '_toManyRelationship' : '_toOneRelationship';
730
+ }
731
+ else {
732
+ relType = field.type.array ? '_toManyRelationshipWithLinks' : '_toOneRelationshipWithLinks';
733
+ }
734
+ relationships[field.name] = this.ref(relType);
735
+ }
736
+ else {
737
+ attributes[field.name] = this.generateField(field);
738
+ if (mode === 'create' &&
739
+ !field.type.optional &&
740
+ !(0, sdk_1.hasAttribute)(field, '@default') &&
741
+ // collection relation fields are implicitly optional
742
+ !((0, ast_1.isDataModel)((_a = field.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl) && field.type.array)) {
743
+ required.push(field.name);
744
+ }
745
+ }
746
+ }
747
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
748
+ const result = {
749
+ type: 'object',
750
+ description: `The "${model.name}" model`,
751
+ required: ['id', 'type', 'attributes'],
752
+ properties: {
753
+ type: { type: 'string' },
754
+ id: { type: 'string' },
755
+ attributes: {
756
+ type: 'object',
757
+ required: required.length > 0 ? required : undefined,
758
+ properties: attributes,
759
+ },
760
+ },
761
+ };
762
+ if (Object.keys(relationships).length > 0) {
763
+ result.properties.relationships = {
764
+ type: 'object',
765
+ properties: relationships,
766
+ };
767
+ }
768
+ return result;
769
+ }
770
+ generateField(field) {
771
+ return this.wrapArray(this.fieldTypeToOpenAPISchema(field.type), field.type.array);
772
+ }
773
+ get specVersion() {
774
+ return this.getOption('specVersion', '3.0.0');
775
+ }
776
+ fieldTypeToOpenAPISchema(type) {
777
+ var _a;
778
+ switch (type.type) {
779
+ case 'String':
780
+ return { type: 'string' };
781
+ case 'Int':
782
+ case 'BigInt':
783
+ return { type: 'integer' };
784
+ case 'Float':
785
+ case 'Decimal':
786
+ return { type: 'number' };
787
+ case 'Boolean':
788
+ return { type: 'boolean' };
789
+ case 'DateTime':
790
+ return { type: 'string', format: 'date-time' };
791
+ case 'Json':
792
+ return { type: 'object' };
793
+ default: {
794
+ const fieldDecl = (_a = type.reference) === null || _a === void 0 ? void 0 : _a.ref;
795
+ (0, tiny_invariant_1.default)(fieldDecl);
796
+ return this.ref(fieldDecl === null || fieldDecl === void 0 ? void 0 : fieldDecl.name);
797
+ }
798
+ }
799
+ }
800
+ ref(type) {
801
+ return { $ref: `#/components/schemas/${type}` };
802
+ }
803
+ nullable(schema) {
804
+ return this.specVersion === '3.0.0' ? Object.assign(Object.assign({}, schema), { nullable: true }) : this.oneOf(schema, { type: 'null' });
805
+ }
806
+ parameter(type) {
807
+ return { $ref: `#/components/parameters/${type}` };
808
+ }
809
+ forbidden() {
810
+ return {
811
+ description: 'Request is forbidden',
812
+ content: {
813
+ 'application/vnd.api+json': {
814
+ schema: this.ref('_errorResponse'),
815
+ },
816
+ },
817
+ };
818
+ }
819
+ notFound() {
820
+ return {
821
+ description: 'Resource is not found',
822
+ content: {
823
+ 'application/vnd.api+json': {
824
+ schema: this.ref('_errorResponse'),
825
+ },
826
+ },
827
+ };
828
+ }
829
+ success(responseComponent) {
830
+ return {
831
+ description: 'Successful operation',
832
+ content: responseComponent
833
+ ? {
834
+ 'application/vnd.api+json': {
835
+ schema: this.ref(responseComponent),
836
+ },
837
+ }
838
+ : undefined,
839
+ };
840
+ }
841
+ }
842
+ exports.RESTfulOpenAPIGenerator = RESTfulOpenAPIGenerator;
843
+ //# sourceMappingURL=rest-generator.js.map