@zenstackhq/server 3.5.0-beta.3 → 3.5.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.cjs +1465 -42
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +37 -5
- package/dist/api.d.ts +37 -5
- package/dist/api.js +1453 -30
- package/dist/api.js.map +1 -1
- package/package.json +10 -7
package/dist/api.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/api/rest/index.ts
|
|
5
|
-
import { clone, enumerate, lowerCaseFirst, paramCase } from "@zenstackhq/common-helpers";
|
|
5
|
+
import { clone, enumerate, lowerCaseFirst as lowerCaseFirst3, paramCase, safeJSONStringify } from "@zenstackhq/common-helpers";
|
|
6
6
|
import { ORMError as ORMError2, ORMErrorReason } from "@zenstackhq/orm";
|
|
7
7
|
import { Decimal as Decimal2 } from "decimal.js";
|
|
8
8
|
import SuperJSON3 from "superjson";
|
|
@@ -92,6 +92,26 @@ var loggerSchema = z.union([
|
|
|
92
92
|
]).array(),
|
|
93
93
|
z.function()
|
|
94
94
|
]);
|
|
95
|
+
var fieldSlicingSchema = z.looseObject({
|
|
96
|
+
includedFilterKinds: z.string().array().optional(),
|
|
97
|
+
excludedFilterKinds: z.string().array().optional()
|
|
98
|
+
});
|
|
99
|
+
var modelSlicingSchema = z.looseObject({
|
|
100
|
+
includedOperations: z.array(z.string()).optional(),
|
|
101
|
+
excludedOperations: z.array(z.string()).optional(),
|
|
102
|
+
fields: z.record(z.string(), fieldSlicingSchema).optional()
|
|
103
|
+
});
|
|
104
|
+
var slicingSchema = z.looseObject({
|
|
105
|
+
includedModels: z.array(z.string()).optional(),
|
|
106
|
+
excludedModels: z.array(z.string()).optional(),
|
|
107
|
+
models: z.record(z.string(), modelSlicingSchema).optional(),
|
|
108
|
+
includedProcedures: z.array(z.string()).optional(),
|
|
109
|
+
excludedProcedures: z.array(z.string()).optional()
|
|
110
|
+
});
|
|
111
|
+
var queryOptionsSchema = z.looseObject({
|
|
112
|
+
omit: z.record(z.string(), z.record(z.string(), z.boolean())).optional(),
|
|
113
|
+
slicing: slicingSchema.optional()
|
|
114
|
+
});
|
|
95
115
|
|
|
96
116
|
// src/api/common/utils.ts
|
|
97
117
|
import SuperJSON from "superjson";
|
|
@@ -194,6 +214,1401 @@ function getZodErrorMessage(error) {
|
|
|
194
214
|
}
|
|
195
215
|
__name(getZodErrorMessage, "getZodErrorMessage");
|
|
196
216
|
|
|
217
|
+
// src/api/rest/openapi.ts
|
|
218
|
+
import { lowerCaseFirst as lowerCaseFirst2 } from "@zenstackhq/common-helpers";
|
|
219
|
+
|
|
220
|
+
// src/api/common/spec-utils.ts
|
|
221
|
+
import { lowerCaseFirst } from "@zenstackhq/common-helpers";
|
|
222
|
+
import { ExpressionUtils } from "@zenstackhq/orm/schema";
|
|
223
|
+
function isModelIncluded(modelName, queryOptions) {
|
|
224
|
+
const slicing = queryOptions?.slicing;
|
|
225
|
+
if (!slicing) return true;
|
|
226
|
+
const excluded = slicing.excludedModels;
|
|
227
|
+
if (excluded?.includes(modelName)) return false;
|
|
228
|
+
const included = slicing.includedModels;
|
|
229
|
+
if (included && !included.includes(modelName)) return false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
__name(isModelIncluded, "isModelIncluded");
|
|
233
|
+
function isOperationIncluded(modelName, op, queryOptions) {
|
|
234
|
+
const slicing = queryOptions?.slicing;
|
|
235
|
+
if (!slicing?.models) return true;
|
|
236
|
+
const modelKey = lowerCaseFirst(modelName);
|
|
237
|
+
const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
|
|
238
|
+
if (!modelSlicing) return true;
|
|
239
|
+
const excluded = modelSlicing.excludedOperations;
|
|
240
|
+
if (excluded?.includes(op)) return false;
|
|
241
|
+
const included = modelSlicing.includedOperations;
|
|
242
|
+
if (included && !included.includes(op)) return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
__name(isOperationIncluded, "isOperationIncluded");
|
|
246
|
+
function isProcedureIncluded(procName, queryOptions) {
|
|
247
|
+
const slicing = queryOptions?.slicing;
|
|
248
|
+
if (!slicing) return true;
|
|
249
|
+
const excluded = slicing.excludedProcedures;
|
|
250
|
+
if (excluded?.includes(procName)) return false;
|
|
251
|
+
const included = slicing.includedProcedures;
|
|
252
|
+
if (included && !included.includes(procName)) return false;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
__name(isProcedureIncluded, "isProcedureIncluded");
|
|
256
|
+
function isFieldOmitted(modelName, fieldName, queryOptions) {
|
|
257
|
+
const omit = queryOptions?.omit;
|
|
258
|
+
return omit?.[modelName]?.[fieldName] === true;
|
|
259
|
+
}
|
|
260
|
+
__name(isFieldOmitted, "isFieldOmitted");
|
|
261
|
+
function getIncludedModels(schema, queryOptions) {
|
|
262
|
+
return Object.keys(schema.models).filter((name) => isModelIncluded(name, queryOptions));
|
|
263
|
+
}
|
|
264
|
+
__name(getIncludedModels, "getIncludedModels");
|
|
265
|
+
function isFilterKindIncluded(modelName, fieldName, filterKind, queryOptions) {
|
|
266
|
+
const slicing = queryOptions?.slicing;
|
|
267
|
+
if (!slicing?.models) return true;
|
|
268
|
+
const modelKey = lowerCaseFirst(modelName);
|
|
269
|
+
const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
|
|
270
|
+
if (!modelSlicing?.fields) return true;
|
|
271
|
+
const fieldSlicing = modelSlicing.fields[fieldName] ?? modelSlicing.fields.$all;
|
|
272
|
+
if (!fieldSlicing) return true;
|
|
273
|
+
const excluded = fieldSlicing.excludedFilterKinds;
|
|
274
|
+
if (excluded?.includes(filterKind)) return false;
|
|
275
|
+
const included = fieldSlicing.includedFilterKinds;
|
|
276
|
+
if (included && !included.includes(filterKind)) return false;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
__name(isFilterKindIncluded, "isFilterKindIncluded");
|
|
280
|
+
function getMetaDescription(attributes) {
|
|
281
|
+
if (!attributes) return void 0;
|
|
282
|
+
for (const attr of attributes) {
|
|
283
|
+
if (attr.name !== "@meta" && attr.name !== "@@meta") continue;
|
|
284
|
+
const nameArg = attr.args?.find((a) => a.name === "name");
|
|
285
|
+
if (!nameArg || ExpressionUtils.getLiteralValue(nameArg.value) !== "description") continue;
|
|
286
|
+
const valueArg = attr.args?.find((a) => a.name === "value");
|
|
287
|
+
if (valueArg) {
|
|
288
|
+
return ExpressionUtils.getLiteralValue(valueArg.value);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
__name(getMetaDescription, "getMetaDescription");
|
|
294
|
+
|
|
295
|
+
// src/api/rest/openapi.ts
|
|
296
|
+
function errorResponse(description) {
|
|
297
|
+
return {
|
|
298
|
+
description,
|
|
299
|
+
content: {
|
|
300
|
+
"application/vnd.api+json": {
|
|
301
|
+
schema: {
|
|
302
|
+
$ref: "#/components/schemas/_errorResponse"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
__name(errorResponse, "errorResponse");
|
|
309
|
+
var ERROR_400 = errorResponse("Error occurred while processing the request");
|
|
310
|
+
var ERROR_403 = errorResponse("Forbidden: insufficient permissions to perform this operation");
|
|
311
|
+
var ERROR_404 = errorResponse("Resource not found");
|
|
312
|
+
var ERROR_422 = errorResponse("Operation is unprocessable due to validation errors");
|
|
313
|
+
var SCALAR_STRING_OPS = [
|
|
314
|
+
"$contains",
|
|
315
|
+
"$icontains",
|
|
316
|
+
"$search",
|
|
317
|
+
"$startsWith",
|
|
318
|
+
"$endsWith"
|
|
319
|
+
];
|
|
320
|
+
var SCALAR_COMPARABLE_OPS = [
|
|
321
|
+
"$lt",
|
|
322
|
+
"$lte",
|
|
323
|
+
"$gt",
|
|
324
|
+
"$gte"
|
|
325
|
+
];
|
|
326
|
+
var SCALAR_ARRAY_OPS = [
|
|
327
|
+
"$has",
|
|
328
|
+
"$hasEvery",
|
|
329
|
+
"$hasSome",
|
|
330
|
+
"$isEmpty"
|
|
331
|
+
];
|
|
332
|
+
var RestApiSpecGenerator = class {
|
|
333
|
+
static {
|
|
334
|
+
__name(this, "RestApiSpecGenerator");
|
|
335
|
+
}
|
|
336
|
+
handlerOptions;
|
|
337
|
+
specOptions;
|
|
338
|
+
constructor(handlerOptions) {
|
|
339
|
+
this.handlerOptions = handlerOptions;
|
|
340
|
+
}
|
|
341
|
+
get schema() {
|
|
342
|
+
return this.handlerOptions.schema;
|
|
343
|
+
}
|
|
344
|
+
get modelNameMapping() {
|
|
345
|
+
const mapping = {};
|
|
346
|
+
if (this.handlerOptions.modelNameMapping) {
|
|
347
|
+
for (const [k, v] of Object.entries(this.handlerOptions.modelNameMapping)) {
|
|
348
|
+
mapping[lowerCaseFirst2(k)] = v;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return mapping;
|
|
352
|
+
}
|
|
353
|
+
get queryOptions() {
|
|
354
|
+
return this.handlerOptions?.queryOptions;
|
|
355
|
+
}
|
|
356
|
+
generateSpec(options) {
|
|
357
|
+
this.specOptions = options;
|
|
358
|
+
return {
|
|
359
|
+
openapi: "3.1.0",
|
|
360
|
+
info: {
|
|
361
|
+
title: options?.title ?? "ZenStack Generated API",
|
|
362
|
+
version: options?.version ?? "1.0.0",
|
|
363
|
+
...options?.description && {
|
|
364
|
+
description: options.description
|
|
365
|
+
},
|
|
366
|
+
...options?.summary && {
|
|
367
|
+
summary: options.summary
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
tags: this.generateTags(),
|
|
371
|
+
paths: this.generatePaths(),
|
|
372
|
+
components: {
|
|
373
|
+
schemas: this.generateSchemas(),
|
|
374
|
+
parameters: this.generateSharedParams()
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
generateTags() {
|
|
379
|
+
return getIncludedModels(this.schema, this.queryOptions).map((modelName) => ({
|
|
380
|
+
name: lowerCaseFirst2(modelName),
|
|
381
|
+
description: `${modelName} operations`
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
getModelPath(modelName) {
|
|
385
|
+
const lower = lowerCaseFirst2(modelName);
|
|
386
|
+
return this.modelNameMapping[lower] ?? lower;
|
|
387
|
+
}
|
|
388
|
+
generatePaths() {
|
|
389
|
+
const paths = {};
|
|
390
|
+
for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
|
|
391
|
+
const modelDef = this.schema.models[modelName];
|
|
392
|
+
const idFields = this.getIdFields(modelDef);
|
|
393
|
+
if (idFields.length === 0) continue;
|
|
394
|
+
const modelPath = this.getModelPath(modelName);
|
|
395
|
+
const tag = lowerCaseFirst2(modelName);
|
|
396
|
+
const collectionPath = this.buildCollectionPath(modelName, modelDef, tag);
|
|
397
|
+
if (Object.keys(collectionPath).length > 0) {
|
|
398
|
+
paths[`/${modelPath}`] = collectionPath;
|
|
399
|
+
}
|
|
400
|
+
const singlePath = this.buildSinglePath(modelDef, tag);
|
|
401
|
+
if (Object.keys(singlePath).length > 0) {
|
|
402
|
+
paths[`/${modelPath}/{id}`] = singlePath;
|
|
403
|
+
}
|
|
404
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
405
|
+
if (!fieldDef.relation) continue;
|
|
406
|
+
if (!isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
407
|
+
const relModelDef = this.schema.models[fieldDef.type];
|
|
408
|
+
if (!relModelDef) continue;
|
|
409
|
+
const relIdFields = this.getIdFields(relModelDef);
|
|
410
|
+
if (relIdFields.length === 0) continue;
|
|
411
|
+
paths[`/${modelPath}/{id}/${fieldName}`] = this.buildFetchRelatedPath(modelName, fieldName, fieldDef, tag);
|
|
412
|
+
paths[`/${modelPath}/{id}/relationships/${fieldName}`] = this.buildRelationshipPath(modelDef, fieldName, fieldDef, tag);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (this.schema.procedures) {
|
|
416
|
+
for (const [procName, procDef] of Object.entries(this.schema.procedures)) {
|
|
417
|
+
if (!isProcedureIncluded(procName, this.queryOptions)) continue;
|
|
418
|
+
const isMutation = !!procDef.mutation;
|
|
419
|
+
if (isMutation) {
|
|
420
|
+
paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
|
|
421
|
+
post: this.buildProcedureOperation(procName, "post")
|
|
422
|
+
};
|
|
423
|
+
} else {
|
|
424
|
+
paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
|
|
425
|
+
get: this.buildProcedureOperation(procName, "get")
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return paths;
|
|
431
|
+
}
|
|
432
|
+
buildCollectionPath(modelName, modelDef, tag) {
|
|
433
|
+
const filterParams = this.buildFilterParams(modelName, modelDef);
|
|
434
|
+
const listOp = {
|
|
435
|
+
tags: [
|
|
436
|
+
tag
|
|
437
|
+
],
|
|
438
|
+
summary: `List ${modelName} resources`,
|
|
439
|
+
operationId: `list${modelName}`,
|
|
440
|
+
parameters: [
|
|
441
|
+
{
|
|
442
|
+
$ref: "#/components/parameters/include"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
$ref: "#/components/parameters/sort"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
$ref: "#/components/parameters/pageOffset"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
$ref: "#/components/parameters/pageLimit"
|
|
452
|
+
},
|
|
453
|
+
...filterParams
|
|
454
|
+
],
|
|
455
|
+
responses: {
|
|
456
|
+
"200": {
|
|
457
|
+
description: `List of ${modelName} resources`,
|
|
458
|
+
content: {
|
|
459
|
+
"application/vnd.api+json": {
|
|
460
|
+
schema: {
|
|
461
|
+
$ref: `#/components/schemas/${modelName}ListResponse`
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
"400": ERROR_400
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const createOp = {
|
|
470
|
+
tags: [
|
|
471
|
+
tag
|
|
472
|
+
],
|
|
473
|
+
summary: `Create a ${modelName} resource`,
|
|
474
|
+
operationId: `create${modelName}`,
|
|
475
|
+
requestBody: {
|
|
476
|
+
required: true,
|
|
477
|
+
content: {
|
|
478
|
+
"application/vnd.api+json": {
|
|
479
|
+
schema: {
|
|
480
|
+
$ref: `#/components/schemas/${modelName}CreateRequest`
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
responses: {
|
|
486
|
+
"201": {
|
|
487
|
+
description: `Created ${modelName} resource`,
|
|
488
|
+
content: {
|
|
489
|
+
"application/vnd.api+json": {
|
|
490
|
+
schema: {
|
|
491
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
"400": ERROR_400,
|
|
497
|
+
...this.mayDenyAccess(modelDef, "create") && {
|
|
498
|
+
"403": ERROR_403
|
|
499
|
+
},
|
|
500
|
+
"422": ERROR_422
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
const result = {};
|
|
504
|
+
if (isOperationIncluded(modelName, "findMany", this.queryOptions)) {
|
|
505
|
+
result["get"] = listOp;
|
|
506
|
+
}
|
|
507
|
+
if (isOperationIncluded(modelName, "create", this.queryOptions)) {
|
|
508
|
+
result["post"] = createOp;
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
buildSinglePath(modelDef, tag) {
|
|
513
|
+
const modelName = modelDef.name;
|
|
514
|
+
const idParam = {
|
|
515
|
+
$ref: "#/components/parameters/id"
|
|
516
|
+
};
|
|
517
|
+
const result = {};
|
|
518
|
+
if (isOperationIncluded(modelName, "findUnique", this.queryOptions)) {
|
|
519
|
+
result["get"] = {
|
|
520
|
+
tags: [
|
|
521
|
+
tag
|
|
522
|
+
],
|
|
523
|
+
summary: `Get a ${modelName} resource by ID`,
|
|
524
|
+
operationId: `get${modelName}`,
|
|
525
|
+
parameters: [
|
|
526
|
+
idParam,
|
|
527
|
+
{
|
|
528
|
+
$ref: "#/components/parameters/include"
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
responses: {
|
|
532
|
+
"200": {
|
|
533
|
+
description: `${modelName} resource`,
|
|
534
|
+
content: {
|
|
535
|
+
"application/vnd.api+json": {
|
|
536
|
+
schema: {
|
|
537
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
"404": ERROR_404
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (isOperationIncluded(modelName, "update", this.queryOptions)) {
|
|
547
|
+
result["patch"] = {
|
|
548
|
+
tags: [
|
|
549
|
+
tag
|
|
550
|
+
],
|
|
551
|
+
summary: `Update a ${modelName} resource`,
|
|
552
|
+
operationId: `update${modelName}`,
|
|
553
|
+
parameters: [
|
|
554
|
+
idParam
|
|
555
|
+
],
|
|
556
|
+
requestBody: {
|
|
557
|
+
required: true,
|
|
558
|
+
content: {
|
|
559
|
+
"application/vnd.api+json": {
|
|
560
|
+
schema: {
|
|
561
|
+
$ref: `#/components/schemas/${modelName}UpdateRequest`
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
responses: {
|
|
567
|
+
"200": {
|
|
568
|
+
description: `Updated ${modelName} resource`,
|
|
569
|
+
content: {
|
|
570
|
+
"application/vnd.api+json": {
|
|
571
|
+
schema: {
|
|
572
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
"400": ERROR_400,
|
|
578
|
+
...this.mayDenyAccess(modelDef, "update") && {
|
|
579
|
+
"403": ERROR_403
|
|
580
|
+
},
|
|
581
|
+
"404": ERROR_404,
|
|
582
|
+
"422": ERROR_422
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (isOperationIncluded(modelName, "delete", this.queryOptions)) {
|
|
587
|
+
result["delete"] = {
|
|
588
|
+
tags: [
|
|
589
|
+
tag
|
|
590
|
+
],
|
|
591
|
+
summary: `Delete a ${modelName} resource`,
|
|
592
|
+
operationId: `delete${modelName}`,
|
|
593
|
+
parameters: [
|
|
594
|
+
idParam
|
|
595
|
+
],
|
|
596
|
+
responses: {
|
|
597
|
+
"200": {
|
|
598
|
+
description: "Deleted successfully"
|
|
599
|
+
},
|
|
600
|
+
...this.mayDenyAccess(modelDef, "delete") && {
|
|
601
|
+
"403": ERROR_403
|
|
602
|
+
},
|
|
603
|
+
"404": ERROR_404
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
buildFetchRelatedPath(modelName, fieldName, fieldDef, tag) {
|
|
610
|
+
const isCollection = !!fieldDef.array;
|
|
611
|
+
const params = [
|
|
612
|
+
{
|
|
613
|
+
$ref: "#/components/parameters/id"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
$ref: "#/components/parameters/include"
|
|
617
|
+
}
|
|
618
|
+
];
|
|
619
|
+
if (isCollection && this.schema.models[fieldDef.type]) {
|
|
620
|
+
const relModelDef = this.schema.models[fieldDef.type];
|
|
621
|
+
params.push({
|
|
622
|
+
$ref: "#/components/parameters/sort"
|
|
623
|
+
}, {
|
|
624
|
+
$ref: "#/components/parameters/pageOffset"
|
|
625
|
+
}, {
|
|
626
|
+
$ref: "#/components/parameters/pageLimit"
|
|
627
|
+
}, ...this.buildFilterParams(fieldDef.type, relModelDef));
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
get: {
|
|
631
|
+
tags: [
|
|
632
|
+
tag
|
|
633
|
+
],
|
|
634
|
+
summary: `Fetch related ${fieldDef.type} for ${modelName}`,
|
|
635
|
+
operationId: `get${modelName}_${fieldName}`,
|
|
636
|
+
parameters: params,
|
|
637
|
+
responses: {
|
|
638
|
+
"200": {
|
|
639
|
+
description: `Related ${fieldDef.type} resource(s)`,
|
|
640
|
+
content: {
|
|
641
|
+
"application/vnd.api+json": {
|
|
642
|
+
schema: isCollection ? {
|
|
643
|
+
$ref: `#/components/schemas/${fieldDef.type}ListResponse`
|
|
644
|
+
} : {
|
|
645
|
+
$ref: `#/components/schemas/${fieldDef.type}Response`
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
"404": ERROR_404
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
buildRelationshipPath(modelDef, fieldName, fieldDef, tag) {
|
|
656
|
+
const modelName = modelDef.name;
|
|
657
|
+
const isCollection = !!fieldDef.array;
|
|
658
|
+
const idParam = {
|
|
659
|
+
$ref: "#/components/parameters/id"
|
|
660
|
+
};
|
|
661
|
+
const relSchemaRef = isCollection ? {
|
|
662
|
+
$ref: "#/components/schemas/_toManyRelationshipWithLinks"
|
|
663
|
+
} : {
|
|
664
|
+
$ref: "#/components/schemas/_toOneRelationshipWithLinks"
|
|
665
|
+
};
|
|
666
|
+
const relRequestRef = isCollection ? {
|
|
667
|
+
$ref: "#/components/schemas/_toManyRelationshipRequest"
|
|
668
|
+
} : {
|
|
669
|
+
$ref: "#/components/schemas/_toOneRelationshipRequest"
|
|
670
|
+
};
|
|
671
|
+
const mayDeny = this.mayDenyAccess(modelDef, "update");
|
|
672
|
+
const pathItem = {
|
|
673
|
+
get: {
|
|
674
|
+
tags: [
|
|
675
|
+
tag
|
|
676
|
+
],
|
|
677
|
+
summary: `Fetch ${fieldName} relationship`,
|
|
678
|
+
operationId: `get${modelName}_relationships_${fieldName}`,
|
|
679
|
+
parameters: [
|
|
680
|
+
idParam
|
|
681
|
+
],
|
|
682
|
+
responses: {
|
|
683
|
+
"200": {
|
|
684
|
+
description: `${fieldName} relationship`,
|
|
685
|
+
content: {
|
|
686
|
+
"application/vnd.api+json": {
|
|
687
|
+
schema: relSchemaRef
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
"404": ERROR_404
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
put: {
|
|
695
|
+
tags: [
|
|
696
|
+
tag
|
|
697
|
+
],
|
|
698
|
+
summary: `Replace ${fieldName} relationship`,
|
|
699
|
+
operationId: `put${modelName}_relationships_${fieldName}`,
|
|
700
|
+
parameters: [
|
|
701
|
+
idParam
|
|
702
|
+
],
|
|
703
|
+
requestBody: {
|
|
704
|
+
required: true,
|
|
705
|
+
content: {
|
|
706
|
+
"application/vnd.api+json": {
|
|
707
|
+
schema: relRequestRef
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
responses: {
|
|
712
|
+
"200": {
|
|
713
|
+
description: "Relationship updated"
|
|
714
|
+
},
|
|
715
|
+
"400": ERROR_400,
|
|
716
|
+
...mayDeny && {
|
|
717
|
+
"403": ERROR_403
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
patch: {
|
|
722
|
+
tags: [
|
|
723
|
+
tag
|
|
724
|
+
],
|
|
725
|
+
summary: `Update ${fieldName} relationship`,
|
|
726
|
+
operationId: `patch${modelName}_relationships_${fieldName}`,
|
|
727
|
+
parameters: [
|
|
728
|
+
idParam
|
|
729
|
+
],
|
|
730
|
+
requestBody: {
|
|
731
|
+
required: true,
|
|
732
|
+
content: {
|
|
733
|
+
"application/vnd.api+json": {
|
|
734
|
+
schema: relRequestRef
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
responses: {
|
|
739
|
+
"200": {
|
|
740
|
+
description: "Relationship updated"
|
|
741
|
+
},
|
|
742
|
+
"400": ERROR_400,
|
|
743
|
+
...mayDeny && {
|
|
744
|
+
"403": ERROR_403
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
if (isCollection) {
|
|
750
|
+
pathItem["post"] = {
|
|
751
|
+
tags: [
|
|
752
|
+
tag
|
|
753
|
+
],
|
|
754
|
+
summary: `Add to ${fieldName} collection relationship`,
|
|
755
|
+
operationId: `post${modelName}_relationships_${fieldName}`,
|
|
756
|
+
parameters: [
|
|
757
|
+
idParam
|
|
758
|
+
],
|
|
759
|
+
requestBody: {
|
|
760
|
+
required: true,
|
|
761
|
+
content: {
|
|
762
|
+
"application/vnd.api+json": {
|
|
763
|
+
schema: {
|
|
764
|
+
$ref: "#/components/schemas/_toManyRelationshipRequest"
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
responses: {
|
|
770
|
+
"200": {
|
|
771
|
+
description: "Added to relationship collection"
|
|
772
|
+
},
|
|
773
|
+
"400": ERROR_400,
|
|
774
|
+
...mayDeny && {
|
|
775
|
+
"403": ERROR_403
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return pathItem;
|
|
781
|
+
}
|
|
782
|
+
buildProcedureOperation(procName, method) {
|
|
783
|
+
const op = {
|
|
784
|
+
tags: [
|
|
785
|
+
"$procs"
|
|
786
|
+
],
|
|
787
|
+
summary: `Execute procedure ${procName}`,
|
|
788
|
+
operationId: `proc_${procName}`,
|
|
789
|
+
responses: {
|
|
790
|
+
"200": {
|
|
791
|
+
description: `Result of ${procName}`
|
|
792
|
+
},
|
|
793
|
+
"400": ERROR_400
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
if (method === "get") {
|
|
797
|
+
op["parameters"] = [
|
|
798
|
+
{
|
|
799
|
+
name: "q",
|
|
800
|
+
in: "query",
|
|
801
|
+
description: "Procedure arguments as JSON",
|
|
802
|
+
schema: {
|
|
803
|
+
type: "string"
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
];
|
|
807
|
+
} else {
|
|
808
|
+
op["requestBody"] = {
|
|
809
|
+
content: {
|
|
810
|
+
"application/json": {
|
|
811
|
+
schema: {
|
|
812
|
+
type: "object"
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return op;
|
|
819
|
+
}
|
|
820
|
+
buildFilterParams(modelName, modelDef) {
|
|
821
|
+
const params = [];
|
|
822
|
+
const idFieldNames = new Set(modelDef.idFields);
|
|
823
|
+
if (isFilterKindIncluded(modelName, "id", "Equality", this.queryOptions)) {
|
|
824
|
+
params.push({
|
|
825
|
+
name: "filter[id]",
|
|
826
|
+
in: "query",
|
|
827
|
+
schema: {
|
|
828
|
+
type: "string"
|
|
829
|
+
},
|
|
830
|
+
description: `Filter by ${modelName} ID`
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
834
|
+
if (fieldDef.relation) continue;
|
|
835
|
+
if (idFieldNames.has(fieldName)) continue;
|
|
836
|
+
const type = fieldDef.type;
|
|
837
|
+
if (isFilterKindIncluded(modelName, fieldName, "Equality", this.queryOptions)) {
|
|
838
|
+
params.push({
|
|
839
|
+
name: `filter[${fieldName}]`,
|
|
840
|
+
in: "query",
|
|
841
|
+
schema: {
|
|
842
|
+
type: "string"
|
|
843
|
+
},
|
|
844
|
+
description: `Filter by ${fieldName}`
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (type === "String" && isFilterKindIncluded(modelName, fieldName, "Like", this.queryOptions)) {
|
|
848
|
+
for (const op of SCALAR_STRING_OPS) {
|
|
849
|
+
params.push({
|
|
850
|
+
name: `filter[${fieldName}][${op}]`,
|
|
851
|
+
in: "query",
|
|
852
|
+
schema: {
|
|
853
|
+
type: "string"
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
} else if ((type === "Int" || type === "Float" || type === "BigInt" || type === "Decimal" || type === "DateTime") && isFilterKindIncluded(modelName, fieldName, "Range", this.queryOptions)) {
|
|
858
|
+
for (const op of SCALAR_COMPARABLE_OPS) {
|
|
859
|
+
params.push({
|
|
860
|
+
name: `filter[${fieldName}][${op}]`,
|
|
861
|
+
in: "query",
|
|
862
|
+
schema: {
|
|
863
|
+
type: "string"
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, "List", this.queryOptions)) {
|
|
869
|
+
for (const op of SCALAR_ARRAY_OPS) {
|
|
870
|
+
params.push({
|
|
871
|
+
name: `filter[${fieldName}][${op}]`,
|
|
872
|
+
in: "query",
|
|
873
|
+
schema: {
|
|
874
|
+
type: "string"
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return params;
|
|
881
|
+
}
|
|
882
|
+
generateSchemas() {
|
|
883
|
+
const schemas = {};
|
|
884
|
+
Object.assign(schemas, this.buildSharedSchemas());
|
|
885
|
+
if (this.schema.enums) {
|
|
886
|
+
for (const [_enumName, enumDef] of Object.entries(this.schema.enums)) {
|
|
887
|
+
schemas[_enumName] = this.buildEnumSchema(enumDef);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (this.schema.typeDefs) {
|
|
891
|
+
for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) {
|
|
892
|
+
schemas[typeName] = this.buildTypeDefSchema(typeDef);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
|
|
896
|
+
const modelDef = this.schema.models[modelName];
|
|
897
|
+
const idFields = this.getIdFields(modelDef);
|
|
898
|
+
if (idFields.length === 0) continue;
|
|
899
|
+
schemas[modelName] = this.buildModelReadSchema(modelName, modelDef);
|
|
900
|
+
schemas[`${modelName}CreateRequest`] = this.buildCreateRequestSchema(modelName, modelDef);
|
|
901
|
+
schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef);
|
|
902
|
+
schemas[`${modelName}Response`] = this.buildModelResponseSchema(modelName);
|
|
903
|
+
schemas[`${modelName}ListResponse`] = this.buildModelListResponseSchema(modelName);
|
|
904
|
+
}
|
|
905
|
+
return schemas;
|
|
906
|
+
}
|
|
907
|
+
buildSharedSchemas() {
|
|
908
|
+
const nullableString = {
|
|
909
|
+
oneOf: [
|
|
910
|
+
{
|
|
911
|
+
type: "string"
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
type: "null"
|
|
915
|
+
}
|
|
916
|
+
]
|
|
917
|
+
};
|
|
918
|
+
return {
|
|
919
|
+
_jsonapi: {
|
|
920
|
+
type: "object",
|
|
921
|
+
properties: {
|
|
922
|
+
version: {
|
|
923
|
+
type: "string"
|
|
924
|
+
},
|
|
925
|
+
meta: {
|
|
926
|
+
type: "object"
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
_meta: {
|
|
931
|
+
type: "object",
|
|
932
|
+
additionalProperties: true
|
|
933
|
+
},
|
|
934
|
+
_links: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
self: {
|
|
938
|
+
type: "string"
|
|
939
|
+
},
|
|
940
|
+
related: {
|
|
941
|
+
type: "string"
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
_pagination: {
|
|
946
|
+
type: "object",
|
|
947
|
+
properties: {
|
|
948
|
+
first: nullableString,
|
|
949
|
+
last: nullableString,
|
|
950
|
+
prev: nullableString,
|
|
951
|
+
next: nullableString
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
_errors: {
|
|
955
|
+
type: "array",
|
|
956
|
+
items: {
|
|
957
|
+
type: "object",
|
|
958
|
+
properties: {
|
|
959
|
+
status: {
|
|
960
|
+
type: "integer"
|
|
961
|
+
},
|
|
962
|
+
code: {
|
|
963
|
+
type: "string"
|
|
964
|
+
},
|
|
965
|
+
title: {
|
|
966
|
+
type: "string"
|
|
967
|
+
},
|
|
968
|
+
detail: {
|
|
969
|
+
type: "string"
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
required: [
|
|
973
|
+
"status",
|
|
974
|
+
"title"
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
_errorResponse: {
|
|
979
|
+
type: "object",
|
|
980
|
+
properties: {
|
|
981
|
+
errors: {
|
|
982
|
+
$ref: "#/components/schemas/_errors"
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
required: [
|
|
986
|
+
"errors"
|
|
987
|
+
]
|
|
988
|
+
},
|
|
989
|
+
_resourceIdentifier: {
|
|
990
|
+
type: "object",
|
|
991
|
+
properties: {
|
|
992
|
+
type: {
|
|
993
|
+
type: "string"
|
|
994
|
+
},
|
|
995
|
+
id: {
|
|
996
|
+
type: "string"
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
required: [
|
|
1000
|
+
"type",
|
|
1001
|
+
"id"
|
|
1002
|
+
]
|
|
1003
|
+
},
|
|
1004
|
+
_resource: {
|
|
1005
|
+
type: "object",
|
|
1006
|
+
properties: {
|
|
1007
|
+
type: {
|
|
1008
|
+
type: "string"
|
|
1009
|
+
},
|
|
1010
|
+
id: {
|
|
1011
|
+
type: "string"
|
|
1012
|
+
},
|
|
1013
|
+
attributes: {
|
|
1014
|
+
type: "object"
|
|
1015
|
+
},
|
|
1016
|
+
relationships: {
|
|
1017
|
+
type: "object"
|
|
1018
|
+
},
|
|
1019
|
+
links: {
|
|
1020
|
+
$ref: "#/components/schemas/_links"
|
|
1021
|
+
},
|
|
1022
|
+
meta: {
|
|
1023
|
+
$ref: "#/components/schemas/_meta"
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
required: [
|
|
1027
|
+
"type",
|
|
1028
|
+
"id"
|
|
1029
|
+
]
|
|
1030
|
+
},
|
|
1031
|
+
_relationLinks: {
|
|
1032
|
+
type: "object",
|
|
1033
|
+
properties: {
|
|
1034
|
+
self: {
|
|
1035
|
+
type: "string"
|
|
1036
|
+
},
|
|
1037
|
+
related: {
|
|
1038
|
+
type: "string"
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
_pagedRelationLinks: {
|
|
1043
|
+
type: "object",
|
|
1044
|
+
allOf: [
|
|
1045
|
+
{
|
|
1046
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
$ref: "#/components/schemas/_pagination"
|
|
1050
|
+
}
|
|
1051
|
+
]
|
|
1052
|
+
},
|
|
1053
|
+
_toOneRelationship: {
|
|
1054
|
+
type: "object",
|
|
1055
|
+
properties: {
|
|
1056
|
+
data: {
|
|
1057
|
+
oneOf: [
|
|
1058
|
+
{
|
|
1059
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
type: "null"
|
|
1063
|
+
}
|
|
1064
|
+
]
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
_toManyRelationship: {
|
|
1069
|
+
type: "object",
|
|
1070
|
+
properties: {
|
|
1071
|
+
data: {
|
|
1072
|
+
type: "array",
|
|
1073
|
+
items: {
|
|
1074
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
_toOneRelationshipWithLinks: {
|
|
1080
|
+
type: "object",
|
|
1081
|
+
allOf: [
|
|
1082
|
+
{
|
|
1083
|
+
$ref: "#/components/schemas/_toOneRelationship"
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
properties: {
|
|
1087
|
+
links: {
|
|
1088
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
]
|
|
1093
|
+
},
|
|
1094
|
+
_toManyRelationshipWithLinks: {
|
|
1095
|
+
type: "object",
|
|
1096
|
+
allOf: [
|
|
1097
|
+
{
|
|
1098
|
+
$ref: "#/components/schemas/_toManyRelationship"
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
properties: {
|
|
1102
|
+
links: {
|
|
1103
|
+
$ref: "#/components/schemas/_pagedRelationLinks"
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
]
|
|
1108
|
+
},
|
|
1109
|
+
_toManyRelationshipRequest: {
|
|
1110
|
+
type: "object",
|
|
1111
|
+
properties: {
|
|
1112
|
+
data: {
|
|
1113
|
+
type: "array",
|
|
1114
|
+
items: {
|
|
1115
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
required: [
|
|
1120
|
+
"data"
|
|
1121
|
+
]
|
|
1122
|
+
},
|
|
1123
|
+
_toOneRelationshipRequest: {
|
|
1124
|
+
type: "object",
|
|
1125
|
+
properties: {
|
|
1126
|
+
data: {
|
|
1127
|
+
oneOf: [
|
|
1128
|
+
{
|
|
1129
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
type: "null"
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
required: [
|
|
1138
|
+
"data"
|
|
1139
|
+
]
|
|
1140
|
+
},
|
|
1141
|
+
_toManyRelationshipResponse: {
|
|
1142
|
+
type: "object",
|
|
1143
|
+
properties: {
|
|
1144
|
+
data: {
|
|
1145
|
+
type: "array",
|
|
1146
|
+
items: {
|
|
1147
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
links: {
|
|
1151
|
+
$ref: "#/components/schemas/_pagedRelationLinks"
|
|
1152
|
+
},
|
|
1153
|
+
meta: {
|
|
1154
|
+
$ref: "#/components/schemas/_meta"
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
_toOneRelationshipResponse: {
|
|
1159
|
+
type: "object",
|
|
1160
|
+
properties: {
|
|
1161
|
+
data: {
|
|
1162
|
+
oneOf: [
|
|
1163
|
+
{
|
|
1164
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
type: "null"
|
|
1168
|
+
}
|
|
1169
|
+
]
|
|
1170
|
+
},
|
|
1171
|
+
links: {
|
|
1172
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1173
|
+
},
|
|
1174
|
+
meta: {
|
|
1175
|
+
$ref: "#/components/schemas/_meta"
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
buildEnumSchema(enumDef) {
|
|
1182
|
+
return {
|
|
1183
|
+
type: "string",
|
|
1184
|
+
enum: Object.values(enumDef.values)
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
buildTypeDefSchema(typeDef) {
|
|
1188
|
+
const properties = {};
|
|
1189
|
+
const required = [];
|
|
1190
|
+
for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) {
|
|
1191
|
+
properties[fieldName] = this.fieldToSchema(fieldDef);
|
|
1192
|
+
if (!fieldDef.optional && !fieldDef.array) {
|
|
1193
|
+
required.push(fieldName);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const result = {
|
|
1197
|
+
type: "object",
|
|
1198
|
+
properties
|
|
1199
|
+
};
|
|
1200
|
+
if (required.length > 0) {
|
|
1201
|
+
result.required = required;
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
}
|
|
1205
|
+
buildModelReadSchema(modelName, modelDef) {
|
|
1206
|
+
const attrProperties = {};
|
|
1207
|
+
const attrRequired = [];
|
|
1208
|
+
const relProperties = {};
|
|
1209
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1210
|
+
if (fieldDef.omit) continue;
|
|
1211
|
+
if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
|
|
1212
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1213
|
+
if (fieldDef.relation) {
|
|
1214
|
+
const relRef = fieldDef.array ? {
|
|
1215
|
+
$ref: "#/components/schemas/_toManyRelationshipWithLinks"
|
|
1216
|
+
} : {
|
|
1217
|
+
$ref: "#/components/schemas/_toOneRelationshipWithLinks"
|
|
1218
|
+
};
|
|
1219
|
+
relProperties[fieldName] = fieldDef.optional ? {
|
|
1220
|
+
oneOf: [
|
|
1221
|
+
{
|
|
1222
|
+
type: "null"
|
|
1223
|
+
},
|
|
1224
|
+
relRef
|
|
1225
|
+
]
|
|
1226
|
+
} : relRef;
|
|
1227
|
+
} else {
|
|
1228
|
+
const schema = this.fieldToSchema(fieldDef);
|
|
1229
|
+
const fieldDescription = getMetaDescription(fieldDef.attributes);
|
|
1230
|
+
if (fieldDescription && !("$ref" in schema)) {
|
|
1231
|
+
schema.description = fieldDescription;
|
|
1232
|
+
}
|
|
1233
|
+
attrProperties[fieldName] = schema;
|
|
1234
|
+
if (!fieldDef.optional && !fieldDef.array) {
|
|
1235
|
+
attrRequired.push(fieldName);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const properties = {};
|
|
1240
|
+
if (Object.keys(attrProperties).length > 0) {
|
|
1241
|
+
const attrSchema = {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
properties: attrProperties
|
|
1244
|
+
};
|
|
1245
|
+
if (attrRequired.length > 0) attrSchema.required = attrRequired;
|
|
1246
|
+
properties["attributes"] = attrSchema;
|
|
1247
|
+
}
|
|
1248
|
+
if (Object.keys(relProperties).length > 0) {
|
|
1249
|
+
properties["relationships"] = {
|
|
1250
|
+
type: "object",
|
|
1251
|
+
properties: relProperties
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const result = {
|
|
1255
|
+
type: "object",
|
|
1256
|
+
properties
|
|
1257
|
+
};
|
|
1258
|
+
const description = getMetaDescription(modelDef.attributes);
|
|
1259
|
+
if (description) {
|
|
1260
|
+
result.description = description;
|
|
1261
|
+
}
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
buildCreateRequestSchema(_modelName, modelDef) {
|
|
1265
|
+
const idFieldNames = new Set(modelDef.idFields);
|
|
1266
|
+
const attributes = {};
|
|
1267
|
+
const attrRequired = [];
|
|
1268
|
+
const relationships = {};
|
|
1269
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1270
|
+
if (fieldDef.updatedAt) continue;
|
|
1271
|
+
if (fieldDef.foreignKeyFor) continue;
|
|
1272
|
+
if (idFieldNames.has(fieldName) && fieldDef.default !== void 0) continue;
|
|
1273
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1274
|
+
if (fieldDef.relation) {
|
|
1275
|
+
relationships[fieldName] = fieldDef.array ? {
|
|
1276
|
+
type: "object",
|
|
1277
|
+
properties: {
|
|
1278
|
+
data: {
|
|
1279
|
+
type: "array",
|
|
1280
|
+
items: {
|
|
1281
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} : {
|
|
1286
|
+
type: "object",
|
|
1287
|
+
properties: {
|
|
1288
|
+
data: {
|
|
1289
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
} else {
|
|
1294
|
+
attributes[fieldName] = this.fieldToSchema(fieldDef);
|
|
1295
|
+
if (!fieldDef.optional && fieldDef.default === void 0 && !fieldDef.array) {
|
|
1296
|
+
attrRequired.push(fieldName);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const dataProperties = {
|
|
1301
|
+
type: {
|
|
1302
|
+
type: "string"
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
if (Object.keys(attributes).length > 0) {
|
|
1306
|
+
const attrSchema = {
|
|
1307
|
+
type: "object",
|
|
1308
|
+
properties: attributes
|
|
1309
|
+
};
|
|
1310
|
+
if (attrRequired.length > 0) attrSchema.required = attrRequired;
|
|
1311
|
+
dataProperties["attributes"] = attrSchema;
|
|
1312
|
+
}
|
|
1313
|
+
if (Object.keys(relationships).length > 0) {
|
|
1314
|
+
dataProperties["relationships"] = {
|
|
1315
|
+
type: "object",
|
|
1316
|
+
properties: relationships
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
type: "object",
|
|
1321
|
+
properties: {
|
|
1322
|
+
data: {
|
|
1323
|
+
type: "object",
|
|
1324
|
+
properties: dataProperties,
|
|
1325
|
+
required: [
|
|
1326
|
+
"type"
|
|
1327
|
+
]
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
required: [
|
|
1331
|
+
"data"
|
|
1332
|
+
]
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
buildUpdateRequestSchema(modelDef) {
|
|
1336
|
+
const attributes = {};
|
|
1337
|
+
const relationships = {};
|
|
1338
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1339
|
+
if (fieldDef.updatedAt) continue;
|
|
1340
|
+
if (fieldDef.foreignKeyFor) continue;
|
|
1341
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1342
|
+
if (fieldDef.relation) {
|
|
1343
|
+
relationships[fieldName] = fieldDef.array ? {
|
|
1344
|
+
type: "object",
|
|
1345
|
+
properties: {
|
|
1346
|
+
data: {
|
|
1347
|
+
type: "array",
|
|
1348
|
+
items: {
|
|
1349
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
} : {
|
|
1354
|
+
type: "object",
|
|
1355
|
+
properties: {
|
|
1356
|
+
data: {
|
|
1357
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
} else {
|
|
1362
|
+
attributes[fieldName] = this.fieldToSchema(fieldDef);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const dataProperties = {
|
|
1366
|
+
type: {
|
|
1367
|
+
type: "string"
|
|
1368
|
+
},
|
|
1369
|
+
id: {
|
|
1370
|
+
type: "string"
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
if (Object.keys(attributes).length > 0) {
|
|
1374
|
+
dataProperties["attributes"] = {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: attributes
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
if (Object.keys(relationships).length > 0) {
|
|
1380
|
+
dataProperties["relationships"] = {
|
|
1381
|
+
type: "object",
|
|
1382
|
+
properties: relationships
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
type: "object",
|
|
1387
|
+
properties: {
|
|
1388
|
+
data: {
|
|
1389
|
+
type: "object",
|
|
1390
|
+
properties: dataProperties,
|
|
1391
|
+
required: [
|
|
1392
|
+
"type",
|
|
1393
|
+
"id"
|
|
1394
|
+
]
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
required: [
|
|
1398
|
+
"data"
|
|
1399
|
+
]
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
buildModelResponseSchema(modelName) {
|
|
1403
|
+
return {
|
|
1404
|
+
type: "object",
|
|
1405
|
+
properties: {
|
|
1406
|
+
jsonapi: {
|
|
1407
|
+
$ref: "#/components/schemas/_jsonapi"
|
|
1408
|
+
},
|
|
1409
|
+
data: {
|
|
1410
|
+
allOf: [
|
|
1411
|
+
{
|
|
1412
|
+
$ref: `#/components/schemas/${modelName}`
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
$ref: "#/components/schemas/_resource"
|
|
1416
|
+
}
|
|
1417
|
+
]
|
|
1418
|
+
},
|
|
1419
|
+
meta: {
|
|
1420
|
+
$ref: "#/components/schemas/_meta"
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
buildModelListResponseSchema(modelName) {
|
|
1426
|
+
return {
|
|
1427
|
+
type: "object",
|
|
1428
|
+
properties: {
|
|
1429
|
+
jsonapi: {
|
|
1430
|
+
$ref: "#/components/schemas/_jsonapi"
|
|
1431
|
+
},
|
|
1432
|
+
data: {
|
|
1433
|
+
type: "array",
|
|
1434
|
+
items: {
|
|
1435
|
+
allOf: [
|
|
1436
|
+
{
|
|
1437
|
+
$ref: `#/components/schemas/${modelName}`
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
$ref: "#/components/schemas/_resource"
|
|
1441
|
+
}
|
|
1442
|
+
]
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
links: {
|
|
1446
|
+
allOf: [
|
|
1447
|
+
{
|
|
1448
|
+
$ref: "#/components/schemas/_pagination"
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
$ref: "#/components/schemas/_links"
|
|
1452
|
+
}
|
|
1453
|
+
]
|
|
1454
|
+
},
|
|
1455
|
+
meta: {
|
|
1456
|
+
$ref: "#/components/schemas/_meta"
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
generateSharedParams() {
|
|
1462
|
+
return {
|
|
1463
|
+
id: {
|
|
1464
|
+
name: "id",
|
|
1465
|
+
in: "path",
|
|
1466
|
+
required: true,
|
|
1467
|
+
schema: {
|
|
1468
|
+
type: "string"
|
|
1469
|
+
},
|
|
1470
|
+
description: "Resource ID"
|
|
1471
|
+
},
|
|
1472
|
+
include: {
|
|
1473
|
+
name: "include",
|
|
1474
|
+
in: "query",
|
|
1475
|
+
schema: {
|
|
1476
|
+
type: "string"
|
|
1477
|
+
},
|
|
1478
|
+
description: "Comma-separated list of relationships to include"
|
|
1479
|
+
},
|
|
1480
|
+
sort: {
|
|
1481
|
+
name: "sort",
|
|
1482
|
+
in: "query",
|
|
1483
|
+
schema: {
|
|
1484
|
+
type: "string"
|
|
1485
|
+
},
|
|
1486
|
+
description: "Comma-separated list of fields to sort by. Prefix with - for descending"
|
|
1487
|
+
},
|
|
1488
|
+
pageOffset: {
|
|
1489
|
+
name: "page[offset]",
|
|
1490
|
+
in: "query",
|
|
1491
|
+
schema: {
|
|
1492
|
+
type: "integer",
|
|
1493
|
+
minimum: 0
|
|
1494
|
+
},
|
|
1495
|
+
description: "Page offset"
|
|
1496
|
+
},
|
|
1497
|
+
pageLimit: {
|
|
1498
|
+
name: "page[limit]",
|
|
1499
|
+
in: "query",
|
|
1500
|
+
schema: {
|
|
1501
|
+
type: "integer",
|
|
1502
|
+
minimum: 1
|
|
1503
|
+
},
|
|
1504
|
+
description: "Page limit"
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
fieldToSchema(fieldDef) {
|
|
1509
|
+
const baseSchema = this.typeToSchema(fieldDef.type);
|
|
1510
|
+
if (fieldDef.array) {
|
|
1511
|
+
return {
|
|
1512
|
+
type: "array",
|
|
1513
|
+
items: baseSchema
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
if (fieldDef.optional) {
|
|
1517
|
+
return {
|
|
1518
|
+
oneOf: [
|
|
1519
|
+
baseSchema,
|
|
1520
|
+
{
|
|
1521
|
+
type: "null"
|
|
1522
|
+
}
|
|
1523
|
+
]
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return baseSchema;
|
|
1527
|
+
}
|
|
1528
|
+
typeToSchema(type) {
|
|
1529
|
+
switch (type) {
|
|
1530
|
+
case "String":
|
|
1531
|
+
return {
|
|
1532
|
+
type: "string"
|
|
1533
|
+
};
|
|
1534
|
+
case "Int":
|
|
1535
|
+
case "BigInt":
|
|
1536
|
+
return {
|
|
1537
|
+
type: "integer"
|
|
1538
|
+
};
|
|
1539
|
+
case "Float":
|
|
1540
|
+
return {
|
|
1541
|
+
type: "number"
|
|
1542
|
+
};
|
|
1543
|
+
case "Decimal":
|
|
1544
|
+
return {
|
|
1545
|
+
oneOf: [
|
|
1546
|
+
{
|
|
1547
|
+
type: "number"
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
type: "string"
|
|
1551
|
+
}
|
|
1552
|
+
]
|
|
1553
|
+
};
|
|
1554
|
+
case "Boolean":
|
|
1555
|
+
return {
|
|
1556
|
+
type: "boolean"
|
|
1557
|
+
};
|
|
1558
|
+
case "DateTime":
|
|
1559
|
+
return {
|
|
1560
|
+
type: "string",
|
|
1561
|
+
format: "date-time"
|
|
1562
|
+
};
|
|
1563
|
+
case "Bytes":
|
|
1564
|
+
return {
|
|
1565
|
+
type: "string",
|
|
1566
|
+
format: "byte"
|
|
1567
|
+
};
|
|
1568
|
+
case "Json":
|
|
1569
|
+
case "Unsupported":
|
|
1570
|
+
return {};
|
|
1571
|
+
default:
|
|
1572
|
+
return {
|
|
1573
|
+
$ref: `#/components/schemas/${type}`
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
getIdFields(modelDef) {
|
|
1578
|
+
return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f) => f !== void 0);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Checks if an operation on a model may be denied by access policies.
|
|
1582
|
+
* Returns true when `respectAccessPolicies` is enabled and the model's
|
|
1583
|
+
* policies for the given operation are NOT a constant allow (i.e., not
|
|
1584
|
+
* simply `@@allow('...', true)` with no `@@deny` rules).
|
|
1585
|
+
*/
|
|
1586
|
+
mayDenyAccess(modelDef, operation) {
|
|
1587
|
+
if (!this.specOptions?.respectAccessPolicies) return false;
|
|
1588
|
+
const policyAttrs = (modelDef.attributes ?? []).filter((attr) => attr.name === "@@allow" || attr.name === "@@deny");
|
|
1589
|
+
if (policyAttrs.length === 0) return true;
|
|
1590
|
+
const getArgByName = /* @__PURE__ */ __name((args, name) => args?.find((a) => a.name === name)?.value, "getArgByName");
|
|
1591
|
+
const matchesOperation = /* @__PURE__ */ __name((args) => {
|
|
1592
|
+
const val = getArgByName(args, "operation");
|
|
1593
|
+
if (!val || val.kind !== "literal" || typeof val.value !== "string") return false;
|
|
1594
|
+
const ops = val.value.split(",").map((s) => s.trim());
|
|
1595
|
+
return ops.includes(operation) || ops.includes("all");
|
|
1596
|
+
}, "matchesOperation");
|
|
1597
|
+
const hasEffectiveDeny = policyAttrs.some((attr) => {
|
|
1598
|
+
if (attr.name !== "@@deny" || !matchesOperation(attr.args)) return false;
|
|
1599
|
+
const condition = getArgByName(attr.args, "condition");
|
|
1600
|
+
return !(condition?.kind === "literal" && condition.value === false);
|
|
1601
|
+
});
|
|
1602
|
+
if (hasEffectiveDeny) return true;
|
|
1603
|
+
const relevantAllow = policyAttrs.filter((attr) => attr.name === "@@allow" && matchesOperation(attr.args));
|
|
1604
|
+
const hasConstantAllow = relevantAllow.some((attr) => {
|
|
1605
|
+
const condition = getArgByName(attr.args, "condition");
|
|
1606
|
+
return condition?.kind === "literal" && condition.value === true;
|
|
1607
|
+
});
|
|
1608
|
+
return !hasConstantAllow;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
197
1612
|
// src/api/rest/index.ts
|
|
198
1613
|
var InvalidValueError = class InvalidValueError2 extends Error {
|
|
199
1614
|
static {
|
|
@@ -376,7 +1791,7 @@ var RestApiHandler = class {
|
|
|
376
1791
|
const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
|
|
377
1792
|
this.modelNameMapping = options.modelNameMapping ?? {};
|
|
378
1793
|
this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
379
|
-
|
|
1794
|
+
lowerCaseFirst3(k),
|
|
380
1795
|
v
|
|
381
1796
|
]));
|
|
382
1797
|
this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
@@ -385,7 +1800,7 @@ var RestApiHandler = class {
|
|
|
385
1800
|
]));
|
|
386
1801
|
this.externalIdMapping = options.externalIdMapping ?? {};
|
|
387
1802
|
this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
|
|
388
|
-
|
|
1803
|
+
lowerCaseFirst3(k),
|
|
389
1804
|
v
|
|
390
1805
|
]));
|
|
391
1806
|
this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
|
|
@@ -404,7 +1819,8 @@ var RestApiHandler = class {
|
|
|
404
1819
|
idDivider: z2.string().min(1).optional(),
|
|
405
1820
|
urlSegmentCharset: z2.string().min(1).optional(),
|
|
406
1821
|
modelNameMapping: z2.record(z2.string(), z2.string()).optional(),
|
|
407
|
-
externalIdMapping: z2.record(z2.string(), z2.string()).optional()
|
|
1822
|
+
externalIdMapping: z2.record(z2.string(), z2.string()).optional(),
|
|
1823
|
+
queryOptions: queryOptionsSchema.optional()
|
|
408
1824
|
});
|
|
409
1825
|
const parseResult = schema.safeParse(options);
|
|
410
1826
|
if (!parseResult.success) {
|
|
@@ -562,8 +1978,9 @@ var RestApiHandler = class {
|
|
|
562
1978
|
}
|
|
563
1979
|
}
|
|
564
1980
|
handleGenericError(err) {
|
|
565
|
-
|
|
566
|
-
${err.stack
|
|
1981
|
+
const resp = this.makeError("unknownError", err instanceof Error ? `${err.message}` : "Unknown error");
|
|
1982
|
+
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
1983
|
+
return resp;
|
|
567
1984
|
}
|
|
568
1985
|
async processProcedureRequest({ client, method, proc, query, requestBody }) {
|
|
569
1986
|
if (!proc) {
|
|
@@ -621,13 +2038,13 @@ ${err.stack}` : "Unknown error");
|
|
|
621
2038
|
}
|
|
622
2039
|
makeProcBadInputErrorResponse(message) {
|
|
623
2040
|
const resp = this.makeError("invalidPayload", message, 400);
|
|
624
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2041
|
+
log(this.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}`);
|
|
625
2042
|
return resp;
|
|
626
2043
|
}
|
|
627
2044
|
makeProcGenericErrorResponse(err) {
|
|
628
2045
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
629
2046
|
const resp = this.makeError("unknownError", message, 500);
|
|
630
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2047
|
+
log(this.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
631
2048
|
return resp;
|
|
632
2049
|
}
|
|
633
2050
|
async processSingleRead(client, type, resourceId, query) {
|
|
@@ -700,7 +2117,7 @@ ${err.stack}` : "Unknown error");
|
|
|
700
2117
|
select = relationSelect;
|
|
701
2118
|
}
|
|
702
2119
|
if (!select) {
|
|
703
|
-
const { select: partialFields, error } = this.buildPartialSelect(
|
|
2120
|
+
const { select: partialFields, error } = this.buildPartialSelect(lowerCaseFirst3(relationInfo.type), query);
|
|
704
2121
|
if (error) return error;
|
|
705
2122
|
select = partialFields ? {
|
|
706
2123
|
[relationship]: {
|
|
@@ -1244,7 +2661,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1244
2661
|
}
|
|
1245
2662
|
getIdFields(model) {
|
|
1246
2663
|
const modelDef = this.requireModel(model);
|
|
1247
|
-
const modelLower =
|
|
2664
|
+
const modelLower = lowerCaseFirst3(model);
|
|
1248
2665
|
if (!(modelLower in this.externalIdMapping)) {
|
|
1249
2666
|
return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
|
|
1250
2667
|
}
|
|
@@ -1278,7 +2695,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1278
2695
|
log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
|
|
1279
2696
|
continue;
|
|
1280
2697
|
}
|
|
1281
|
-
const modelInfo = this.typeMap[
|
|
2698
|
+
const modelInfo = this.typeMap[lowerCaseFirst3(model)] = {
|
|
1282
2699
|
name: model,
|
|
1283
2700
|
idFields,
|
|
1284
2701
|
relationships: {},
|
|
@@ -1303,7 +2720,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1303
2720
|
}
|
|
1304
2721
|
}
|
|
1305
2722
|
getModelInfo(model) {
|
|
1306
|
-
return this.typeMap[
|
|
2723
|
+
return this.typeMap[lowerCaseFirst3(model)];
|
|
1307
2724
|
}
|
|
1308
2725
|
makeLinkUrl(path) {
|
|
1309
2726
|
return `${this.options.endpoint}${path}`;
|
|
@@ -1312,7 +2729,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1312
2729
|
const linkers = {};
|
|
1313
2730
|
for (const model of Object.keys(this.schema.models)) {
|
|
1314
2731
|
const ids = this.getIdFields(model);
|
|
1315
|
-
const modelLower =
|
|
2732
|
+
const modelLower = lowerCaseFirst3(model);
|
|
1316
2733
|
const mappedModel = this.mapModelName(modelLower);
|
|
1317
2734
|
if (ids.length < 1) {
|
|
1318
2735
|
continue;
|
|
@@ -1341,7 +2758,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1341
2758
|
this.serializers.set(modelLower, serializer);
|
|
1342
2759
|
}
|
|
1343
2760
|
for (const model of Object.keys(this.schema.models)) {
|
|
1344
|
-
const modelLower =
|
|
2761
|
+
const modelLower = lowerCaseFirst3(model);
|
|
1345
2762
|
const serializer = this.serializers.get(modelLower);
|
|
1346
2763
|
if (!serializer) {
|
|
1347
2764
|
continue;
|
|
@@ -1352,7 +2769,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1352
2769
|
if (!fieldDef.relation) {
|
|
1353
2770
|
continue;
|
|
1354
2771
|
}
|
|
1355
|
-
const fieldSerializer = this.serializers.get(
|
|
2772
|
+
const fieldSerializer = this.serializers.get(lowerCaseFirst3(fieldDef.type));
|
|
1356
2773
|
if (!fieldSerializer) {
|
|
1357
2774
|
continue;
|
|
1358
2775
|
}
|
|
@@ -1386,7 +2803,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1386
2803
|
}
|
|
1387
2804
|
}
|
|
1388
2805
|
async serializeItems(model, items, options) {
|
|
1389
|
-
model =
|
|
2806
|
+
model = lowerCaseFirst3(model);
|
|
1390
2807
|
const serializer = this.serializers.get(model);
|
|
1391
2808
|
if (!serializer) {
|
|
1392
2809
|
throw new Error(`serializer not found for model ${model}`);
|
|
@@ -1828,7 +3245,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1828
3245
|
error: this.makeUnsupportedModelError(relationInfo.type)
|
|
1829
3246
|
};
|
|
1830
3247
|
}
|
|
1831
|
-
const { select, error } = this.buildPartialSelect(
|
|
3248
|
+
const { select, error } = this.buildPartialSelect(lowerCaseFirst3(relationInfo.type), query);
|
|
1832
3249
|
if (error) return {
|
|
1833
3250
|
select: void 0,
|
|
1834
3251
|
error
|
|
@@ -2028,10 +3445,15 @@ ${err.stack}` : "Unknown error");
|
|
|
2028
3445
|
makeUnsupportedRelationshipError(model, relationship, status) {
|
|
2029
3446
|
return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
|
|
2030
3447
|
}
|
|
3448
|
+
//#endregion
|
|
3449
|
+
async generateSpec(options) {
|
|
3450
|
+
const generator = new RestApiSpecGenerator(this.options);
|
|
3451
|
+
return generator.generateSpec(options);
|
|
3452
|
+
}
|
|
2031
3453
|
};
|
|
2032
3454
|
|
|
2033
3455
|
// src/api/rpc/index.ts
|
|
2034
|
-
import { lowerCaseFirst as
|
|
3456
|
+
import { lowerCaseFirst as lowerCaseFirst4, safeJSONStringify as safeJSONStringify2 } from "@zenstackhq/common-helpers";
|
|
2035
3457
|
import { CoreCrudOperations, ORMError as ORMError3, ORMErrorReason as ORMErrorReason2 } from "@zenstackhq/orm";
|
|
2036
3458
|
import SuperJSON4 from "superjson";
|
|
2037
3459
|
import { match as match3 } from "ts-pattern";
|
|
@@ -2052,7 +3474,8 @@ var RPCApiHandler = class {
|
|
|
2052
3474
|
validateOptions(options) {
|
|
2053
3475
|
const schema = z3.strictObject({
|
|
2054
3476
|
schema: z3.object(),
|
|
2055
|
-
log: loggerSchema.optional()
|
|
3477
|
+
log: loggerSchema.optional(),
|
|
3478
|
+
queryOptions: queryOptionsSchema.optional()
|
|
2056
3479
|
});
|
|
2057
3480
|
const parseResult = schema.safeParse(options);
|
|
2058
3481
|
if (!parseResult.success) {
|
|
@@ -2089,7 +3512,7 @@ var RPCApiHandler = class {
|
|
|
2089
3512
|
requestBody
|
|
2090
3513
|
});
|
|
2091
3514
|
}
|
|
2092
|
-
model =
|
|
3515
|
+
model = lowerCaseFirst4(model);
|
|
2093
3516
|
method = method.toUpperCase();
|
|
2094
3517
|
let args;
|
|
2095
3518
|
let resCode = 200;
|
|
@@ -2156,7 +3579,7 @@ var RPCApiHandler = class {
|
|
|
2156
3579
|
if (!this.isValidModel(client, model)) {
|
|
2157
3580
|
return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
|
|
2158
3581
|
}
|
|
2159
|
-
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${
|
|
3582
|
+
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${safeJSONStringify2(processedArgs)}`);
|
|
2160
3583
|
const clientResult = await client[model][op](processedArgs);
|
|
2161
3584
|
let responseBody = {
|
|
2162
3585
|
data: clientResult
|
|
@@ -2176,7 +3599,7 @@ var RPCApiHandler = class {
|
|
|
2176
3599
|
status: resCode,
|
|
2177
3600
|
body: responseBody
|
|
2178
3601
|
};
|
|
2179
|
-
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${
|
|
3602
|
+
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${safeJSONStringify2(response)}`);
|
|
2180
3603
|
return response;
|
|
2181
3604
|
} catch (err) {
|
|
2182
3605
|
log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
|
|
@@ -2213,7 +3636,7 @@ var RPCApiHandler = class {
|
|
|
2213
3636
|
if (!VALID_OPS.has(itemOp)) {
|
|
2214
3637
|
return this.makeBadInputErrorResponse(`operation at index ${i} has invalid op: ${itemOp}`);
|
|
2215
3638
|
}
|
|
2216
|
-
if (!this.isValidModel(client,
|
|
3639
|
+
if (!this.isValidModel(client, lowerCaseFirst4(itemModel))) {
|
|
2217
3640
|
return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`);
|
|
2218
3641
|
}
|
|
2219
3642
|
if (itemArgs !== void 0 && itemArgs !== null && (typeof itemArgs !== "object" || Array.isArray(itemArgs))) {
|
|
@@ -2224,7 +3647,7 @@ var RPCApiHandler = class {
|
|
|
2224
3647
|
return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
|
|
2225
3648
|
}
|
|
2226
3649
|
processedOps.push({
|
|
2227
|
-
model:
|
|
3650
|
+
model: lowerCaseFirst4(itemModel),
|
|
2228
3651
|
op: itemOp,
|
|
2229
3652
|
args: processedArgs
|
|
2230
3653
|
});
|
|
@@ -2248,7 +3671,7 @@ var RPCApiHandler = class {
|
|
|
2248
3671
|
status: 200,
|
|
2249
3672
|
body: responseBody
|
|
2250
3673
|
};
|
|
2251
|
-
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${
|
|
3674
|
+
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${safeJSONStringify2(response)}`);
|
|
2252
3675
|
return response;
|
|
2253
3676
|
} catch (err) {
|
|
2254
3677
|
log(this.options.log, "error", `error occurred when handling "$transaction" request`, err);
|
|
@@ -2310,7 +3733,7 @@ var RPCApiHandler = class {
|
|
|
2310
3733
|
status: 200,
|
|
2311
3734
|
body: responseBody
|
|
2312
3735
|
};
|
|
2313
|
-
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${
|
|
3736
|
+
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${safeJSONStringify2(response)}`);
|
|
2314
3737
|
return response;
|
|
2315
3738
|
} catch (err) {
|
|
2316
3739
|
log(this.options.log, "error", `error occurred when handling "$procs.${proc}" request`, err);
|
|
@@ -2321,7 +3744,7 @@ var RPCApiHandler = class {
|
|
|
2321
3744
|
}
|
|
2322
3745
|
}
|
|
2323
3746
|
isValidModel(client, model) {
|
|
2324
|
-
return Object.keys(client.$schema.models).some((m) =>
|
|
3747
|
+
return Object.keys(client.$schema.models).some((m) => lowerCaseFirst4(m) === lowerCaseFirst4(model));
|
|
2325
3748
|
}
|
|
2326
3749
|
makeBadInputErrorResponse(message) {
|
|
2327
3750
|
const resp = {
|
|
@@ -2332,7 +3755,7 @@ var RPCApiHandler = class {
|
|
|
2332
3755
|
}
|
|
2333
3756
|
}
|
|
2334
3757
|
};
|
|
2335
|
-
log(this.options.log, "debug", () => `sending error response: ${
|
|
3758
|
+
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}`);
|
|
2336
3759
|
return resp;
|
|
2337
3760
|
}
|
|
2338
3761
|
makeGenericErrorResponse(err) {
|
|
@@ -2344,7 +3767,7 @@ var RPCApiHandler = class {
|
|
|
2344
3767
|
}
|
|
2345
3768
|
}
|
|
2346
3769
|
};
|
|
2347
|
-
log(this.options.log, "debug", () => `sending error response: ${
|
|
3770
|
+
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
2348
3771
|
return resp;
|
|
2349
3772
|
}
|
|
2350
3773
|
makeORMErrorResponse(err) {
|
|
@@ -2376,7 +3799,7 @@ var RPCApiHandler = class {
|
|
|
2376
3799
|
error
|
|
2377
3800
|
}
|
|
2378
3801
|
};
|
|
2379
|
-
log(this.options.log, "debug", () => `sending error response: ${
|
|
3802
|
+
log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}`);
|
|
2380
3803
|
return resp;
|
|
2381
3804
|
}
|
|
2382
3805
|
async processRequestPayload(args) {
|