@zenstackhq/server 3.5.0-beta.2 → 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.cjs
CHANGED
|
@@ -37,7 +37,7 @@ __export(api_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(api_exports);
|
|
38
38
|
|
|
39
39
|
// src/api/rest/index.ts
|
|
40
|
-
var
|
|
40
|
+
var import_common_helpers3 = require("@zenstackhq/common-helpers");
|
|
41
41
|
var import_orm2 = require("@zenstackhq/orm");
|
|
42
42
|
var import_decimal2 = require("decimal.js");
|
|
43
43
|
var import_superjson3 = __toESM(require("superjson"), 1);
|
|
@@ -127,6 +127,26 @@ var loggerSchema = import_zod.default.union([
|
|
|
127
127
|
]).array(),
|
|
128
128
|
import_zod.default.function()
|
|
129
129
|
]);
|
|
130
|
+
var fieldSlicingSchema = import_zod.default.looseObject({
|
|
131
|
+
includedFilterKinds: import_zod.default.string().array().optional(),
|
|
132
|
+
excludedFilterKinds: import_zod.default.string().array().optional()
|
|
133
|
+
});
|
|
134
|
+
var modelSlicingSchema = import_zod.default.looseObject({
|
|
135
|
+
includedOperations: import_zod.default.array(import_zod.default.string()).optional(),
|
|
136
|
+
excludedOperations: import_zod.default.array(import_zod.default.string()).optional(),
|
|
137
|
+
fields: import_zod.default.record(import_zod.default.string(), fieldSlicingSchema).optional()
|
|
138
|
+
});
|
|
139
|
+
var slicingSchema = import_zod.default.looseObject({
|
|
140
|
+
includedModels: import_zod.default.array(import_zod.default.string()).optional(),
|
|
141
|
+
excludedModels: import_zod.default.array(import_zod.default.string()).optional(),
|
|
142
|
+
models: import_zod.default.record(import_zod.default.string(), modelSlicingSchema).optional(),
|
|
143
|
+
includedProcedures: import_zod.default.array(import_zod.default.string()).optional(),
|
|
144
|
+
excludedProcedures: import_zod.default.array(import_zod.default.string()).optional()
|
|
145
|
+
});
|
|
146
|
+
var queryOptionsSchema = import_zod.default.looseObject({
|
|
147
|
+
omit: import_zod.default.record(import_zod.default.string(), import_zod.default.record(import_zod.default.string(), import_zod.default.boolean())).optional(),
|
|
148
|
+
slicing: slicingSchema.optional()
|
|
149
|
+
});
|
|
130
150
|
|
|
131
151
|
// src/api/common/utils.ts
|
|
132
152
|
var import_superjson = __toESM(require("superjson"), 1);
|
|
@@ -229,6 +249,1401 @@ function getZodErrorMessage(error) {
|
|
|
229
249
|
}
|
|
230
250
|
__name(getZodErrorMessage, "getZodErrorMessage");
|
|
231
251
|
|
|
252
|
+
// src/api/rest/openapi.ts
|
|
253
|
+
var import_common_helpers2 = require("@zenstackhq/common-helpers");
|
|
254
|
+
|
|
255
|
+
// src/api/common/spec-utils.ts
|
|
256
|
+
var import_common_helpers = require("@zenstackhq/common-helpers");
|
|
257
|
+
var import_schema = require("@zenstackhq/orm/schema");
|
|
258
|
+
function isModelIncluded(modelName, queryOptions) {
|
|
259
|
+
const slicing = queryOptions?.slicing;
|
|
260
|
+
if (!slicing) return true;
|
|
261
|
+
const excluded = slicing.excludedModels;
|
|
262
|
+
if (excluded?.includes(modelName)) return false;
|
|
263
|
+
const included = slicing.includedModels;
|
|
264
|
+
if (included && !included.includes(modelName)) return false;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
__name(isModelIncluded, "isModelIncluded");
|
|
268
|
+
function isOperationIncluded(modelName, op, queryOptions) {
|
|
269
|
+
const slicing = queryOptions?.slicing;
|
|
270
|
+
if (!slicing?.models) return true;
|
|
271
|
+
const modelKey = (0, import_common_helpers.lowerCaseFirst)(modelName);
|
|
272
|
+
const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
|
|
273
|
+
if (!modelSlicing) return true;
|
|
274
|
+
const excluded = modelSlicing.excludedOperations;
|
|
275
|
+
if (excluded?.includes(op)) return false;
|
|
276
|
+
const included = modelSlicing.includedOperations;
|
|
277
|
+
if (included && !included.includes(op)) return false;
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
__name(isOperationIncluded, "isOperationIncluded");
|
|
281
|
+
function isProcedureIncluded(procName, queryOptions) {
|
|
282
|
+
const slicing = queryOptions?.slicing;
|
|
283
|
+
if (!slicing) return true;
|
|
284
|
+
const excluded = slicing.excludedProcedures;
|
|
285
|
+
if (excluded?.includes(procName)) return false;
|
|
286
|
+
const included = slicing.includedProcedures;
|
|
287
|
+
if (included && !included.includes(procName)) return false;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
__name(isProcedureIncluded, "isProcedureIncluded");
|
|
291
|
+
function isFieldOmitted(modelName, fieldName, queryOptions) {
|
|
292
|
+
const omit = queryOptions?.omit;
|
|
293
|
+
return omit?.[modelName]?.[fieldName] === true;
|
|
294
|
+
}
|
|
295
|
+
__name(isFieldOmitted, "isFieldOmitted");
|
|
296
|
+
function getIncludedModels(schema, queryOptions) {
|
|
297
|
+
return Object.keys(schema.models).filter((name) => isModelIncluded(name, queryOptions));
|
|
298
|
+
}
|
|
299
|
+
__name(getIncludedModels, "getIncludedModels");
|
|
300
|
+
function isFilterKindIncluded(modelName, fieldName, filterKind, queryOptions) {
|
|
301
|
+
const slicing = queryOptions?.slicing;
|
|
302
|
+
if (!slicing?.models) return true;
|
|
303
|
+
const modelKey = (0, import_common_helpers.lowerCaseFirst)(modelName);
|
|
304
|
+
const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
|
|
305
|
+
if (!modelSlicing?.fields) return true;
|
|
306
|
+
const fieldSlicing = modelSlicing.fields[fieldName] ?? modelSlicing.fields.$all;
|
|
307
|
+
if (!fieldSlicing) return true;
|
|
308
|
+
const excluded = fieldSlicing.excludedFilterKinds;
|
|
309
|
+
if (excluded?.includes(filterKind)) return false;
|
|
310
|
+
const included = fieldSlicing.includedFilterKinds;
|
|
311
|
+
if (included && !included.includes(filterKind)) return false;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
__name(isFilterKindIncluded, "isFilterKindIncluded");
|
|
315
|
+
function getMetaDescription(attributes) {
|
|
316
|
+
if (!attributes) return void 0;
|
|
317
|
+
for (const attr of attributes) {
|
|
318
|
+
if (attr.name !== "@meta" && attr.name !== "@@meta") continue;
|
|
319
|
+
const nameArg = attr.args?.find((a) => a.name === "name");
|
|
320
|
+
if (!nameArg || import_schema.ExpressionUtils.getLiteralValue(nameArg.value) !== "description") continue;
|
|
321
|
+
const valueArg = attr.args?.find((a) => a.name === "value");
|
|
322
|
+
if (valueArg) {
|
|
323
|
+
return import_schema.ExpressionUtils.getLiteralValue(valueArg.value);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return void 0;
|
|
327
|
+
}
|
|
328
|
+
__name(getMetaDescription, "getMetaDescription");
|
|
329
|
+
|
|
330
|
+
// src/api/rest/openapi.ts
|
|
331
|
+
function errorResponse(description) {
|
|
332
|
+
return {
|
|
333
|
+
description,
|
|
334
|
+
content: {
|
|
335
|
+
"application/vnd.api+json": {
|
|
336
|
+
schema: {
|
|
337
|
+
$ref: "#/components/schemas/_errorResponse"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
__name(errorResponse, "errorResponse");
|
|
344
|
+
var ERROR_400 = errorResponse("Error occurred while processing the request");
|
|
345
|
+
var ERROR_403 = errorResponse("Forbidden: insufficient permissions to perform this operation");
|
|
346
|
+
var ERROR_404 = errorResponse("Resource not found");
|
|
347
|
+
var ERROR_422 = errorResponse("Operation is unprocessable due to validation errors");
|
|
348
|
+
var SCALAR_STRING_OPS = [
|
|
349
|
+
"$contains",
|
|
350
|
+
"$icontains",
|
|
351
|
+
"$search",
|
|
352
|
+
"$startsWith",
|
|
353
|
+
"$endsWith"
|
|
354
|
+
];
|
|
355
|
+
var SCALAR_COMPARABLE_OPS = [
|
|
356
|
+
"$lt",
|
|
357
|
+
"$lte",
|
|
358
|
+
"$gt",
|
|
359
|
+
"$gte"
|
|
360
|
+
];
|
|
361
|
+
var SCALAR_ARRAY_OPS = [
|
|
362
|
+
"$has",
|
|
363
|
+
"$hasEvery",
|
|
364
|
+
"$hasSome",
|
|
365
|
+
"$isEmpty"
|
|
366
|
+
];
|
|
367
|
+
var RestApiSpecGenerator = class {
|
|
368
|
+
static {
|
|
369
|
+
__name(this, "RestApiSpecGenerator");
|
|
370
|
+
}
|
|
371
|
+
handlerOptions;
|
|
372
|
+
specOptions;
|
|
373
|
+
constructor(handlerOptions) {
|
|
374
|
+
this.handlerOptions = handlerOptions;
|
|
375
|
+
}
|
|
376
|
+
get schema() {
|
|
377
|
+
return this.handlerOptions.schema;
|
|
378
|
+
}
|
|
379
|
+
get modelNameMapping() {
|
|
380
|
+
const mapping = {};
|
|
381
|
+
if (this.handlerOptions.modelNameMapping) {
|
|
382
|
+
for (const [k, v] of Object.entries(this.handlerOptions.modelNameMapping)) {
|
|
383
|
+
mapping[(0, import_common_helpers2.lowerCaseFirst)(k)] = v;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return mapping;
|
|
387
|
+
}
|
|
388
|
+
get queryOptions() {
|
|
389
|
+
return this.handlerOptions?.queryOptions;
|
|
390
|
+
}
|
|
391
|
+
generateSpec(options) {
|
|
392
|
+
this.specOptions = options;
|
|
393
|
+
return {
|
|
394
|
+
openapi: "3.1.0",
|
|
395
|
+
info: {
|
|
396
|
+
title: options?.title ?? "ZenStack Generated API",
|
|
397
|
+
version: options?.version ?? "1.0.0",
|
|
398
|
+
...options?.description && {
|
|
399
|
+
description: options.description
|
|
400
|
+
},
|
|
401
|
+
...options?.summary && {
|
|
402
|
+
summary: options.summary
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
tags: this.generateTags(),
|
|
406
|
+
paths: this.generatePaths(),
|
|
407
|
+
components: {
|
|
408
|
+
schemas: this.generateSchemas(),
|
|
409
|
+
parameters: this.generateSharedParams()
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
generateTags() {
|
|
414
|
+
return getIncludedModels(this.schema, this.queryOptions).map((modelName) => ({
|
|
415
|
+
name: (0, import_common_helpers2.lowerCaseFirst)(modelName),
|
|
416
|
+
description: `${modelName} operations`
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
getModelPath(modelName) {
|
|
420
|
+
const lower = (0, import_common_helpers2.lowerCaseFirst)(modelName);
|
|
421
|
+
return this.modelNameMapping[lower] ?? lower;
|
|
422
|
+
}
|
|
423
|
+
generatePaths() {
|
|
424
|
+
const paths = {};
|
|
425
|
+
for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
|
|
426
|
+
const modelDef = this.schema.models[modelName];
|
|
427
|
+
const idFields = this.getIdFields(modelDef);
|
|
428
|
+
if (idFields.length === 0) continue;
|
|
429
|
+
const modelPath = this.getModelPath(modelName);
|
|
430
|
+
const tag = (0, import_common_helpers2.lowerCaseFirst)(modelName);
|
|
431
|
+
const collectionPath = this.buildCollectionPath(modelName, modelDef, tag);
|
|
432
|
+
if (Object.keys(collectionPath).length > 0) {
|
|
433
|
+
paths[`/${modelPath}`] = collectionPath;
|
|
434
|
+
}
|
|
435
|
+
const singlePath = this.buildSinglePath(modelDef, tag);
|
|
436
|
+
if (Object.keys(singlePath).length > 0) {
|
|
437
|
+
paths[`/${modelPath}/{id}`] = singlePath;
|
|
438
|
+
}
|
|
439
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
440
|
+
if (!fieldDef.relation) continue;
|
|
441
|
+
if (!isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
442
|
+
const relModelDef = this.schema.models[fieldDef.type];
|
|
443
|
+
if (!relModelDef) continue;
|
|
444
|
+
const relIdFields = this.getIdFields(relModelDef);
|
|
445
|
+
if (relIdFields.length === 0) continue;
|
|
446
|
+
paths[`/${modelPath}/{id}/${fieldName}`] = this.buildFetchRelatedPath(modelName, fieldName, fieldDef, tag);
|
|
447
|
+
paths[`/${modelPath}/{id}/relationships/${fieldName}`] = this.buildRelationshipPath(modelDef, fieldName, fieldDef, tag);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (this.schema.procedures) {
|
|
451
|
+
for (const [procName, procDef] of Object.entries(this.schema.procedures)) {
|
|
452
|
+
if (!isProcedureIncluded(procName, this.queryOptions)) continue;
|
|
453
|
+
const isMutation = !!procDef.mutation;
|
|
454
|
+
if (isMutation) {
|
|
455
|
+
paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
|
|
456
|
+
post: this.buildProcedureOperation(procName, "post")
|
|
457
|
+
};
|
|
458
|
+
} else {
|
|
459
|
+
paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
|
|
460
|
+
get: this.buildProcedureOperation(procName, "get")
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return paths;
|
|
466
|
+
}
|
|
467
|
+
buildCollectionPath(modelName, modelDef, tag) {
|
|
468
|
+
const filterParams = this.buildFilterParams(modelName, modelDef);
|
|
469
|
+
const listOp = {
|
|
470
|
+
tags: [
|
|
471
|
+
tag
|
|
472
|
+
],
|
|
473
|
+
summary: `List ${modelName} resources`,
|
|
474
|
+
operationId: `list${modelName}`,
|
|
475
|
+
parameters: [
|
|
476
|
+
{
|
|
477
|
+
$ref: "#/components/parameters/include"
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
$ref: "#/components/parameters/sort"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
$ref: "#/components/parameters/pageOffset"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
$ref: "#/components/parameters/pageLimit"
|
|
487
|
+
},
|
|
488
|
+
...filterParams
|
|
489
|
+
],
|
|
490
|
+
responses: {
|
|
491
|
+
"200": {
|
|
492
|
+
description: `List of ${modelName} resources`,
|
|
493
|
+
content: {
|
|
494
|
+
"application/vnd.api+json": {
|
|
495
|
+
schema: {
|
|
496
|
+
$ref: `#/components/schemas/${modelName}ListResponse`
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
"400": ERROR_400
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const createOp = {
|
|
505
|
+
tags: [
|
|
506
|
+
tag
|
|
507
|
+
],
|
|
508
|
+
summary: `Create a ${modelName} resource`,
|
|
509
|
+
operationId: `create${modelName}`,
|
|
510
|
+
requestBody: {
|
|
511
|
+
required: true,
|
|
512
|
+
content: {
|
|
513
|
+
"application/vnd.api+json": {
|
|
514
|
+
schema: {
|
|
515
|
+
$ref: `#/components/schemas/${modelName}CreateRequest`
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
responses: {
|
|
521
|
+
"201": {
|
|
522
|
+
description: `Created ${modelName} resource`,
|
|
523
|
+
content: {
|
|
524
|
+
"application/vnd.api+json": {
|
|
525
|
+
schema: {
|
|
526
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
"400": ERROR_400,
|
|
532
|
+
...this.mayDenyAccess(modelDef, "create") && {
|
|
533
|
+
"403": ERROR_403
|
|
534
|
+
},
|
|
535
|
+
"422": ERROR_422
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const result = {};
|
|
539
|
+
if (isOperationIncluded(modelName, "findMany", this.queryOptions)) {
|
|
540
|
+
result["get"] = listOp;
|
|
541
|
+
}
|
|
542
|
+
if (isOperationIncluded(modelName, "create", this.queryOptions)) {
|
|
543
|
+
result["post"] = createOp;
|
|
544
|
+
}
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
buildSinglePath(modelDef, tag) {
|
|
548
|
+
const modelName = modelDef.name;
|
|
549
|
+
const idParam = {
|
|
550
|
+
$ref: "#/components/parameters/id"
|
|
551
|
+
};
|
|
552
|
+
const result = {};
|
|
553
|
+
if (isOperationIncluded(modelName, "findUnique", this.queryOptions)) {
|
|
554
|
+
result["get"] = {
|
|
555
|
+
tags: [
|
|
556
|
+
tag
|
|
557
|
+
],
|
|
558
|
+
summary: `Get a ${modelName} resource by ID`,
|
|
559
|
+
operationId: `get${modelName}`,
|
|
560
|
+
parameters: [
|
|
561
|
+
idParam,
|
|
562
|
+
{
|
|
563
|
+
$ref: "#/components/parameters/include"
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
responses: {
|
|
567
|
+
"200": {
|
|
568
|
+
description: `${modelName} resource`,
|
|
569
|
+
content: {
|
|
570
|
+
"application/vnd.api+json": {
|
|
571
|
+
schema: {
|
|
572
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
"404": ERROR_404
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (isOperationIncluded(modelName, "update", this.queryOptions)) {
|
|
582
|
+
result["patch"] = {
|
|
583
|
+
tags: [
|
|
584
|
+
tag
|
|
585
|
+
],
|
|
586
|
+
summary: `Update a ${modelName} resource`,
|
|
587
|
+
operationId: `update${modelName}`,
|
|
588
|
+
parameters: [
|
|
589
|
+
idParam
|
|
590
|
+
],
|
|
591
|
+
requestBody: {
|
|
592
|
+
required: true,
|
|
593
|
+
content: {
|
|
594
|
+
"application/vnd.api+json": {
|
|
595
|
+
schema: {
|
|
596
|
+
$ref: `#/components/schemas/${modelName}UpdateRequest`
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
responses: {
|
|
602
|
+
"200": {
|
|
603
|
+
description: `Updated ${modelName} resource`,
|
|
604
|
+
content: {
|
|
605
|
+
"application/vnd.api+json": {
|
|
606
|
+
schema: {
|
|
607
|
+
$ref: `#/components/schemas/${modelName}Response`
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
"400": ERROR_400,
|
|
613
|
+
...this.mayDenyAccess(modelDef, "update") && {
|
|
614
|
+
"403": ERROR_403
|
|
615
|
+
},
|
|
616
|
+
"404": ERROR_404,
|
|
617
|
+
"422": ERROR_422
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (isOperationIncluded(modelName, "delete", this.queryOptions)) {
|
|
622
|
+
result["delete"] = {
|
|
623
|
+
tags: [
|
|
624
|
+
tag
|
|
625
|
+
],
|
|
626
|
+
summary: `Delete a ${modelName} resource`,
|
|
627
|
+
operationId: `delete${modelName}`,
|
|
628
|
+
parameters: [
|
|
629
|
+
idParam
|
|
630
|
+
],
|
|
631
|
+
responses: {
|
|
632
|
+
"200": {
|
|
633
|
+
description: "Deleted successfully"
|
|
634
|
+
},
|
|
635
|
+
...this.mayDenyAccess(modelDef, "delete") && {
|
|
636
|
+
"403": ERROR_403
|
|
637
|
+
},
|
|
638
|
+
"404": ERROR_404
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
buildFetchRelatedPath(modelName, fieldName, fieldDef, tag) {
|
|
645
|
+
const isCollection = !!fieldDef.array;
|
|
646
|
+
const params = [
|
|
647
|
+
{
|
|
648
|
+
$ref: "#/components/parameters/id"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
$ref: "#/components/parameters/include"
|
|
652
|
+
}
|
|
653
|
+
];
|
|
654
|
+
if (isCollection && this.schema.models[fieldDef.type]) {
|
|
655
|
+
const relModelDef = this.schema.models[fieldDef.type];
|
|
656
|
+
params.push({
|
|
657
|
+
$ref: "#/components/parameters/sort"
|
|
658
|
+
}, {
|
|
659
|
+
$ref: "#/components/parameters/pageOffset"
|
|
660
|
+
}, {
|
|
661
|
+
$ref: "#/components/parameters/pageLimit"
|
|
662
|
+
}, ...this.buildFilterParams(fieldDef.type, relModelDef));
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
get: {
|
|
666
|
+
tags: [
|
|
667
|
+
tag
|
|
668
|
+
],
|
|
669
|
+
summary: `Fetch related ${fieldDef.type} for ${modelName}`,
|
|
670
|
+
operationId: `get${modelName}_${fieldName}`,
|
|
671
|
+
parameters: params,
|
|
672
|
+
responses: {
|
|
673
|
+
"200": {
|
|
674
|
+
description: `Related ${fieldDef.type} resource(s)`,
|
|
675
|
+
content: {
|
|
676
|
+
"application/vnd.api+json": {
|
|
677
|
+
schema: isCollection ? {
|
|
678
|
+
$ref: `#/components/schemas/${fieldDef.type}ListResponse`
|
|
679
|
+
} : {
|
|
680
|
+
$ref: `#/components/schemas/${fieldDef.type}Response`
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
"404": ERROR_404
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
buildRelationshipPath(modelDef, fieldName, fieldDef, tag) {
|
|
691
|
+
const modelName = modelDef.name;
|
|
692
|
+
const isCollection = !!fieldDef.array;
|
|
693
|
+
const idParam = {
|
|
694
|
+
$ref: "#/components/parameters/id"
|
|
695
|
+
};
|
|
696
|
+
const relSchemaRef = isCollection ? {
|
|
697
|
+
$ref: "#/components/schemas/_toManyRelationshipWithLinks"
|
|
698
|
+
} : {
|
|
699
|
+
$ref: "#/components/schemas/_toOneRelationshipWithLinks"
|
|
700
|
+
};
|
|
701
|
+
const relRequestRef = isCollection ? {
|
|
702
|
+
$ref: "#/components/schemas/_toManyRelationshipRequest"
|
|
703
|
+
} : {
|
|
704
|
+
$ref: "#/components/schemas/_toOneRelationshipRequest"
|
|
705
|
+
};
|
|
706
|
+
const mayDeny = this.mayDenyAccess(modelDef, "update");
|
|
707
|
+
const pathItem = {
|
|
708
|
+
get: {
|
|
709
|
+
tags: [
|
|
710
|
+
tag
|
|
711
|
+
],
|
|
712
|
+
summary: `Fetch ${fieldName} relationship`,
|
|
713
|
+
operationId: `get${modelName}_relationships_${fieldName}`,
|
|
714
|
+
parameters: [
|
|
715
|
+
idParam
|
|
716
|
+
],
|
|
717
|
+
responses: {
|
|
718
|
+
"200": {
|
|
719
|
+
description: `${fieldName} relationship`,
|
|
720
|
+
content: {
|
|
721
|
+
"application/vnd.api+json": {
|
|
722
|
+
schema: relSchemaRef
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
"404": ERROR_404
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
put: {
|
|
730
|
+
tags: [
|
|
731
|
+
tag
|
|
732
|
+
],
|
|
733
|
+
summary: `Replace ${fieldName} relationship`,
|
|
734
|
+
operationId: `put${modelName}_relationships_${fieldName}`,
|
|
735
|
+
parameters: [
|
|
736
|
+
idParam
|
|
737
|
+
],
|
|
738
|
+
requestBody: {
|
|
739
|
+
required: true,
|
|
740
|
+
content: {
|
|
741
|
+
"application/vnd.api+json": {
|
|
742
|
+
schema: relRequestRef
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
responses: {
|
|
747
|
+
"200": {
|
|
748
|
+
description: "Relationship updated"
|
|
749
|
+
},
|
|
750
|
+
"400": ERROR_400,
|
|
751
|
+
...mayDeny && {
|
|
752
|
+
"403": ERROR_403
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
patch: {
|
|
757
|
+
tags: [
|
|
758
|
+
tag
|
|
759
|
+
],
|
|
760
|
+
summary: `Update ${fieldName} relationship`,
|
|
761
|
+
operationId: `patch${modelName}_relationships_${fieldName}`,
|
|
762
|
+
parameters: [
|
|
763
|
+
idParam
|
|
764
|
+
],
|
|
765
|
+
requestBody: {
|
|
766
|
+
required: true,
|
|
767
|
+
content: {
|
|
768
|
+
"application/vnd.api+json": {
|
|
769
|
+
schema: relRequestRef
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
responses: {
|
|
774
|
+
"200": {
|
|
775
|
+
description: "Relationship updated"
|
|
776
|
+
},
|
|
777
|
+
"400": ERROR_400,
|
|
778
|
+
...mayDeny && {
|
|
779
|
+
"403": ERROR_403
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
if (isCollection) {
|
|
785
|
+
pathItem["post"] = {
|
|
786
|
+
tags: [
|
|
787
|
+
tag
|
|
788
|
+
],
|
|
789
|
+
summary: `Add to ${fieldName} collection relationship`,
|
|
790
|
+
operationId: `post${modelName}_relationships_${fieldName}`,
|
|
791
|
+
parameters: [
|
|
792
|
+
idParam
|
|
793
|
+
],
|
|
794
|
+
requestBody: {
|
|
795
|
+
required: true,
|
|
796
|
+
content: {
|
|
797
|
+
"application/vnd.api+json": {
|
|
798
|
+
schema: {
|
|
799
|
+
$ref: "#/components/schemas/_toManyRelationshipRequest"
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
responses: {
|
|
805
|
+
"200": {
|
|
806
|
+
description: "Added to relationship collection"
|
|
807
|
+
},
|
|
808
|
+
"400": ERROR_400,
|
|
809
|
+
...mayDeny && {
|
|
810
|
+
"403": ERROR_403
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return pathItem;
|
|
816
|
+
}
|
|
817
|
+
buildProcedureOperation(procName, method) {
|
|
818
|
+
const op = {
|
|
819
|
+
tags: [
|
|
820
|
+
"$procs"
|
|
821
|
+
],
|
|
822
|
+
summary: `Execute procedure ${procName}`,
|
|
823
|
+
operationId: `proc_${procName}`,
|
|
824
|
+
responses: {
|
|
825
|
+
"200": {
|
|
826
|
+
description: `Result of ${procName}`
|
|
827
|
+
},
|
|
828
|
+
"400": ERROR_400
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
if (method === "get") {
|
|
832
|
+
op["parameters"] = [
|
|
833
|
+
{
|
|
834
|
+
name: "q",
|
|
835
|
+
in: "query",
|
|
836
|
+
description: "Procedure arguments as JSON",
|
|
837
|
+
schema: {
|
|
838
|
+
type: "string"
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
];
|
|
842
|
+
} else {
|
|
843
|
+
op["requestBody"] = {
|
|
844
|
+
content: {
|
|
845
|
+
"application/json": {
|
|
846
|
+
schema: {
|
|
847
|
+
type: "object"
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
return op;
|
|
854
|
+
}
|
|
855
|
+
buildFilterParams(modelName, modelDef) {
|
|
856
|
+
const params = [];
|
|
857
|
+
const idFieldNames = new Set(modelDef.idFields);
|
|
858
|
+
if (isFilterKindIncluded(modelName, "id", "Equality", this.queryOptions)) {
|
|
859
|
+
params.push({
|
|
860
|
+
name: "filter[id]",
|
|
861
|
+
in: "query",
|
|
862
|
+
schema: {
|
|
863
|
+
type: "string"
|
|
864
|
+
},
|
|
865
|
+
description: `Filter by ${modelName} ID`
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
869
|
+
if (fieldDef.relation) continue;
|
|
870
|
+
if (idFieldNames.has(fieldName)) continue;
|
|
871
|
+
const type = fieldDef.type;
|
|
872
|
+
if (isFilterKindIncluded(modelName, fieldName, "Equality", this.queryOptions)) {
|
|
873
|
+
params.push({
|
|
874
|
+
name: `filter[${fieldName}]`,
|
|
875
|
+
in: "query",
|
|
876
|
+
schema: {
|
|
877
|
+
type: "string"
|
|
878
|
+
},
|
|
879
|
+
description: `Filter by ${fieldName}`
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (type === "String" && isFilterKindIncluded(modelName, fieldName, "Like", this.queryOptions)) {
|
|
883
|
+
for (const op of SCALAR_STRING_OPS) {
|
|
884
|
+
params.push({
|
|
885
|
+
name: `filter[${fieldName}][${op}]`,
|
|
886
|
+
in: "query",
|
|
887
|
+
schema: {
|
|
888
|
+
type: "string"
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
} else if ((type === "Int" || type === "Float" || type === "BigInt" || type === "Decimal" || type === "DateTime") && isFilterKindIncluded(modelName, fieldName, "Range", this.queryOptions)) {
|
|
893
|
+
for (const op of SCALAR_COMPARABLE_OPS) {
|
|
894
|
+
params.push({
|
|
895
|
+
name: `filter[${fieldName}][${op}]`,
|
|
896
|
+
in: "query",
|
|
897
|
+
schema: {
|
|
898
|
+
type: "string"
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, "List", this.queryOptions)) {
|
|
904
|
+
for (const op of SCALAR_ARRAY_OPS) {
|
|
905
|
+
params.push({
|
|
906
|
+
name: `filter[${fieldName}][${op}]`,
|
|
907
|
+
in: "query",
|
|
908
|
+
schema: {
|
|
909
|
+
type: "string"
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return params;
|
|
916
|
+
}
|
|
917
|
+
generateSchemas() {
|
|
918
|
+
const schemas = {};
|
|
919
|
+
Object.assign(schemas, this.buildSharedSchemas());
|
|
920
|
+
if (this.schema.enums) {
|
|
921
|
+
for (const [_enumName, enumDef] of Object.entries(this.schema.enums)) {
|
|
922
|
+
schemas[_enumName] = this.buildEnumSchema(enumDef);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (this.schema.typeDefs) {
|
|
926
|
+
for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) {
|
|
927
|
+
schemas[typeName] = this.buildTypeDefSchema(typeDef);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
|
|
931
|
+
const modelDef = this.schema.models[modelName];
|
|
932
|
+
const idFields = this.getIdFields(modelDef);
|
|
933
|
+
if (idFields.length === 0) continue;
|
|
934
|
+
schemas[modelName] = this.buildModelReadSchema(modelName, modelDef);
|
|
935
|
+
schemas[`${modelName}CreateRequest`] = this.buildCreateRequestSchema(modelName, modelDef);
|
|
936
|
+
schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef);
|
|
937
|
+
schemas[`${modelName}Response`] = this.buildModelResponseSchema(modelName);
|
|
938
|
+
schemas[`${modelName}ListResponse`] = this.buildModelListResponseSchema(modelName);
|
|
939
|
+
}
|
|
940
|
+
return schemas;
|
|
941
|
+
}
|
|
942
|
+
buildSharedSchemas() {
|
|
943
|
+
const nullableString = {
|
|
944
|
+
oneOf: [
|
|
945
|
+
{
|
|
946
|
+
type: "string"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
type: "null"
|
|
950
|
+
}
|
|
951
|
+
]
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
_jsonapi: {
|
|
955
|
+
type: "object",
|
|
956
|
+
properties: {
|
|
957
|
+
version: {
|
|
958
|
+
type: "string"
|
|
959
|
+
},
|
|
960
|
+
meta: {
|
|
961
|
+
type: "object"
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
_meta: {
|
|
966
|
+
type: "object",
|
|
967
|
+
additionalProperties: true
|
|
968
|
+
},
|
|
969
|
+
_links: {
|
|
970
|
+
type: "object",
|
|
971
|
+
properties: {
|
|
972
|
+
self: {
|
|
973
|
+
type: "string"
|
|
974
|
+
},
|
|
975
|
+
related: {
|
|
976
|
+
type: "string"
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
},
|
|
980
|
+
_pagination: {
|
|
981
|
+
type: "object",
|
|
982
|
+
properties: {
|
|
983
|
+
first: nullableString,
|
|
984
|
+
last: nullableString,
|
|
985
|
+
prev: nullableString,
|
|
986
|
+
next: nullableString
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
_errors: {
|
|
990
|
+
type: "array",
|
|
991
|
+
items: {
|
|
992
|
+
type: "object",
|
|
993
|
+
properties: {
|
|
994
|
+
status: {
|
|
995
|
+
type: "integer"
|
|
996
|
+
},
|
|
997
|
+
code: {
|
|
998
|
+
type: "string"
|
|
999
|
+
},
|
|
1000
|
+
title: {
|
|
1001
|
+
type: "string"
|
|
1002
|
+
},
|
|
1003
|
+
detail: {
|
|
1004
|
+
type: "string"
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
required: [
|
|
1008
|
+
"status",
|
|
1009
|
+
"title"
|
|
1010
|
+
]
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
_errorResponse: {
|
|
1014
|
+
type: "object",
|
|
1015
|
+
properties: {
|
|
1016
|
+
errors: {
|
|
1017
|
+
$ref: "#/components/schemas/_errors"
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
required: [
|
|
1021
|
+
"errors"
|
|
1022
|
+
]
|
|
1023
|
+
},
|
|
1024
|
+
_resourceIdentifier: {
|
|
1025
|
+
type: "object",
|
|
1026
|
+
properties: {
|
|
1027
|
+
type: {
|
|
1028
|
+
type: "string"
|
|
1029
|
+
},
|
|
1030
|
+
id: {
|
|
1031
|
+
type: "string"
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
required: [
|
|
1035
|
+
"type",
|
|
1036
|
+
"id"
|
|
1037
|
+
]
|
|
1038
|
+
},
|
|
1039
|
+
_resource: {
|
|
1040
|
+
type: "object",
|
|
1041
|
+
properties: {
|
|
1042
|
+
type: {
|
|
1043
|
+
type: "string"
|
|
1044
|
+
},
|
|
1045
|
+
id: {
|
|
1046
|
+
type: "string"
|
|
1047
|
+
},
|
|
1048
|
+
attributes: {
|
|
1049
|
+
type: "object"
|
|
1050
|
+
},
|
|
1051
|
+
relationships: {
|
|
1052
|
+
type: "object"
|
|
1053
|
+
},
|
|
1054
|
+
links: {
|
|
1055
|
+
$ref: "#/components/schemas/_links"
|
|
1056
|
+
},
|
|
1057
|
+
meta: {
|
|
1058
|
+
$ref: "#/components/schemas/_meta"
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
required: [
|
|
1062
|
+
"type",
|
|
1063
|
+
"id"
|
|
1064
|
+
]
|
|
1065
|
+
},
|
|
1066
|
+
_relationLinks: {
|
|
1067
|
+
type: "object",
|
|
1068
|
+
properties: {
|
|
1069
|
+
self: {
|
|
1070
|
+
type: "string"
|
|
1071
|
+
},
|
|
1072
|
+
related: {
|
|
1073
|
+
type: "string"
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
_pagedRelationLinks: {
|
|
1078
|
+
type: "object",
|
|
1079
|
+
allOf: [
|
|
1080
|
+
{
|
|
1081
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
$ref: "#/components/schemas/_pagination"
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
},
|
|
1088
|
+
_toOneRelationship: {
|
|
1089
|
+
type: "object",
|
|
1090
|
+
properties: {
|
|
1091
|
+
data: {
|
|
1092
|
+
oneOf: [
|
|
1093
|
+
{
|
|
1094
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
type: "null"
|
|
1098
|
+
}
|
|
1099
|
+
]
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
_toManyRelationship: {
|
|
1104
|
+
type: "object",
|
|
1105
|
+
properties: {
|
|
1106
|
+
data: {
|
|
1107
|
+
type: "array",
|
|
1108
|
+
items: {
|
|
1109
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
_toOneRelationshipWithLinks: {
|
|
1115
|
+
type: "object",
|
|
1116
|
+
allOf: [
|
|
1117
|
+
{
|
|
1118
|
+
$ref: "#/components/schemas/_toOneRelationship"
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
properties: {
|
|
1122
|
+
links: {
|
|
1123
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
]
|
|
1128
|
+
},
|
|
1129
|
+
_toManyRelationshipWithLinks: {
|
|
1130
|
+
type: "object",
|
|
1131
|
+
allOf: [
|
|
1132
|
+
{
|
|
1133
|
+
$ref: "#/components/schemas/_toManyRelationship"
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
properties: {
|
|
1137
|
+
links: {
|
|
1138
|
+
$ref: "#/components/schemas/_pagedRelationLinks"
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1143
|
+
},
|
|
1144
|
+
_toManyRelationshipRequest: {
|
|
1145
|
+
type: "object",
|
|
1146
|
+
properties: {
|
|
1147
|
+
data: {
|
|
1148
|
+
type: "array",
|
|
1149
|
+
items: {
|
|
1150
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
required: [
|
|
1155
|
+
"data"
|
|
1156
|
+
]
|
|
1157
|
+
},
|
|
1158
|
+
_toOneRelationshipRequest: {
|
|
1159
|
+
type: "object",
|
|
1160
|
+
properties: {
|
|
1161
|
+
data: {
|
|
1162
|
+
oneOf: [
|
|
1163
|
+
{
|
|
1164
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
type: "null"
|
|
1168
|
+
}
|
|
1169
|
+
]
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
required: [
|
|
1173
|
+
"data"
|
|
1174
|
+
]
|
|
1175
|
+
},
|
|
1176
|
+
_toManyRelationshipResponse: {
|
|
1177
|
+
type: "object",
|
|
1178
|
+
properties: {
|
|
1179
|
+
data: {
|
|
1180
|
+
type: "array",
|
|
1181
|
+
items: {
|
|
1182
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
links: {
|
|
1186
|
+
$ref: "#/components/schemas/_pagedRelationLinks"
|
|
1187
|
+
},
|
|
1188
|
+
meta: {
|
|
1189
|
+
$ref: "#/components/schemas/_meta"
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
_toOneRelationshipResponse: {
|
|
1194
|
+
type: "object",
|
|
1195
|
+
properties: {
|
|
1196
|
+
data: {
|
|
1197
|
+
oneOf: [
|
|
1198
|
+
{
|
|
1199
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
type: "null"
|
|
1203
|
+
}
|
|
1204
|
+
]
|
|
1205
|
+
},
|
|
1206
|
+
links: {
|
|
1207
|
+
$ref: "#/components/schemas/_relationLinks"
|
|
1208
|
+
},
|
|
1209
|
+
meta: {
|
|
1210
|
+
$ref: "#/components/schemas/_meta"
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
buildEnumSchema(enumDef) {
|
|
1217
|
+
return {
|
|
1218
|
+
type: "string",
|
|
1219
|
+
enum: Object.values(enumDef.values)
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
buildTypeDefSchema(typeDef) {
|
|
1223
|
+
const properties = {};
|
|
1224
|
+
const required = [];
|
|
1225
|
+
for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) {
|
|
1226
|
+
properties[fieldName] = this.fieldToSchema(fieldDef);
|
|
1227
|
+
if (!fieldDef.optional && !fieldDef.array) {
|
|
1228
|
+
required.push(fieldName);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const result = {
|
|
1232
|
+
type: "object",
|
|
1233
|
+
properties
|
|
1234
|
+
};
|
|
1235
|
+
if (required.length > 0) {
|
|
1236
|
+
result.required = required;
|
|
1237
|
+
}
|
|
1238
|
+
return result;
|
|
1239
|
+
}
|
|
1240
|
+
buildModelReadSchema(modelName, modelDef) {
|
|
1241
|
+
const attrProperties = {};
|
|
1242
|
+
const attrRequired = [];
|
|
1243
|
+
const relProperties = {};
|
|
1244
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1245
|
+
if (fieldDef.omit) continue;
|
|
1246
|
+
if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
|
|
1247
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1248
|
+
if (fieldDef.relation) {
|
|
1249
|
+
const relRef = fieldDef.array ? {
|
|
1250
|
+
$ref: "#/components/schemas/_toManyRelationshipWithLinks"
|
|
1251
|
+
} : {
|
|
1252
|
+
$ref: "#/components/schemas/_toOneRelationshipWithLinks"
|
|
1253
|
+
};
|
|
1254
|
+
relProperties[fieldName] = fieldDef.optional ? {
|
|
1255
|
+
oneOf: [
|
|
1256
|
+
{
|
|
1257
|
+
type: "null"
|
|
1258
|
+
},
|
|
1259
|
+
relRef
|
|
1260
|
+
]
|
|
1261
|
+
} : relRef;
|
|
1262
|
+
} else {
|
|
1263
|
+
const schema = this.fieldToSchema(fieldDef);
|
|
1264
|
+
const fieldDescription = getMetaDescription(fieldDef.attributes);
|
|
1265
|
+
if (fieldDescription && !("$ref" in schema)) {
|
|
1266
|
+
schema.description = fieldDescription;
|
|
1267
|
+
}
|
|
1268
|
+
attrProperties[fieldName] = schema;
|
|
1269
|
+
if (!fieldDef.optional && !fieldDef.array) {
|
|
1270
|
+
attrRequired.push(fieldName);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const properties = {};
|
|
1275
|
+
if (Object.keys(attrProperties).length > 0) {
|
|
1276
|
+
const attrSchema = {
|
|
1277
|
+
type: "object",
|
|
1278
|
+
properties: attrProperties
|
|
1279
|
+
};
|
|
1280
|
+
if (attrRequired.length > 0) attrSchema.required = attrRequired;
|
|
1281
|
+
properties["attributes"] = attrSchema;
|
|
1282
|
+
}
|
|
1283
|
+
if (Object.keys(relProperties).length > 0) {
|
|
1284
|
+
properties["relationships"] = {
|
|
1285
|
+
type: "object",
|
|
1286
|
+
properties: relProperties
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const result = {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
properties
|
|
1292
|
+
};
|
|
1293
|
+
const description = getMetaDescription(modelDef.attributes);
|
|
1294
|
+
if (description) {
|
|
1295
|
+
result.description = description;
|
|
1296
|
+
}
|
|
1297
|
+
return result;
|
|
1298
|
+
}
|
|
1299
|
+
buildCreateRequestSchema(_modelName, modelDef) {
|
|
1300
|
+
const idFieldNames = new Set(modelDef.idFields);
|
|
1301
|
+
const attributes = {};
|
|
1302
|
+
const attrRequired = [];
|
|
1303
|
+
const relationships = {};
|
|
1304
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1305
|
+
if (fieldDef.updatedAt) continue;
|
|
1306
|
+
if (fieldDef.foreignKeyFor) continue;
|
|
1307
|
+
if (idFieldNames.has(fieldName) && fieldDef.default !== void 0) continue;
|
|
1308
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1309
|
+
if (fieldDef.relation) {
|
|
1310
|
+
relationships[fieldName] = fieldDef.array ? {
|
|
1311
|
+
type: "object",
|
|
1312
|
+
properties: {
|
|
1313
|
+
data: {
|
|
1314
|
+
type: "array",
|
|
1315
|
+
items: {
|
|
1316
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
} : {
|
|
1321
|
+
type: "object",
|
|
1322
|
+
properties: {
|
|
1323
|
+
data: {
|
|
1324
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
} else {
|
|
1329
|
+
attributes[fieldName] = this.fieldToSchema(fieldDef);
|
|
1330
|
+
if (!fieldDef.optional && fieldDef.default === void 0 && !fieldDef.array) {
|
|
1331
|
+
attrRequired.push(fieldName);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const dataProperties = {
|
|
1336
|
+
type: {
|
|
1337
|
+
type: "string"
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
if (Object.keys(attributes).length > 0) {
|
|
1341
|
+
const attrSchema = {
|
|
1342
|
+
type: "object",
|
|
1343
|
+
properties: attributes
|
|
1344
|
+
};
|
|
1345
|
+
if (attrRequired.length > 0) attrSchema.required = attrRequired;
|
|
1346
|
+
dataProperties["attributes"] = attrSchema;
|
|
1347
|
+
}
|
|
1348
|
+
if (Object.keys(relationships).length > 0) {
|
|
1349
|
+
dataProperties["relationships"] = {
|
|
1350
|
+
type: "object",
|
|
1351
|
+
properties: relationships
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
properties: {
|
|
1357
|
+
data: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: dataProperties,
|
|
1360
|
+
required: [
|
|
1361
|
+
"type"
|
|
1362
|
+
]
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
required: [
|
|
1366
|
+
"data"
|
|
1367
|
+
]
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
buildUpdateRequestSchema(modelDef) {
|
|
1371
|
+
const attributes = {};
|
|
1372
|
+
const relationships = {};
|
|
1373
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
1374
|
+
if (fieldDef.updatedAt) continue;
|
|
1375
|
+
if (fieldDef.foreignKeyFor) continue;
|
|
1376
|
+
if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
|
|
1377
|
+
if (fieldDef.relation) {
|
|
1378
|
+
relationships[fieldName] = fieldDef.array ? {
|
|
1379
|
+
type: "object",
|
|
1380
|
+
properties: {
|
|
1381
|
+
data: {
|
|
1382
|
+
type: "array",
|
|
1383
|
+
items: {
|
|
1384
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
} : {
|
|
1389
|
+
type: "object",
|
|
1390
|
+
properties: {
|
|
1391
|
+
data: {
|
|
1392
|
+
$ref: "#/components/schemas/_resourceIdentifier"
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
} else {
|
|
1397
|
+
attributes[fieldName] = this.fieldToSchema(fieldDef);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const dataProperties = {
|
|
1401
|
+
type: {
|
|
1402
|
+
type: "string"
|
|
1403
|
+
},
|
|
1404
|
+
id: {
|
|
1405
|
+
type: "string"
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
if (Object.keys(attributes).length > 0) {
|
|
1409
|
+
dataProperties["attributes"] = {
|
|
1410
|
+
type: "object",
|
|
1411
|
+
properties: attributes
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
if (Object.keys(relationships).length > 0) {
|
|
1415
|
+
dataProperties["relationships"] = {
|
|
1416
|
+
type: "object",
|
|
1417
|
+
properties: relationships
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
type: "object",
|
|
1422
|
+
properties: {
|
|
1423
|
+
data: {
|
|
1424
|
+
type: "object",
|
|
1425
|
+
properties: dataProperties,
|
|
1426
|
+
required: [
|
|
1427
|
+
"type",
|
|
1428
|
+
"id"
|
|
1429
|
+
]
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
required: [
|
|
1433
|
+
"data"
|
|
1434
|
+
]
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
buildModelResponseSchema(modelName) {
|
|
1438
|
+
return {
|
|
1439
|
+
type: "object",
|
|
1440
|
+
properties: {
|
|
1441
|
+
jsonapi: {
|
|
1442
|
+
$ref: "#/components/schemas/_jsonapi"
|
|
1443
|
+
},
|
|
1444
|
+
data: {
|
|
1445
|
+
allOf: [
|
|
1446
|
+
{
|
|
1447
|
+
$ref: `#/components/schemas/${modelName}`
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
$ref: "#/components/schemas/_resource"
|
|
1451
|
+
}
|
|
1452
|
+
]
|
|
1453
|
+
},
|
|
1454
|
+
meta: {
|
|
1455
|
+
$ref: "#/components/schemas/_meta"
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
buildModelListResponseSchema(modelName) {
|
|
1461
|
+
return {
|
|
1462
|
+
type: "object",
|
|
1463
|
+
properties: {
|
|
1464
|
+
jsonapi: {
|
|
1465
|
+
$ref: "#/components/schemas/_jsonapi"
|
|
1466
|
+
},
|
|
1467
|
+
data: {
|
|
1468
|
+
type: "array",
|
|
1469
|
+
items: {
|
|
1470
|
+
allOf: [
|
|
1471
|
+
{
|
|
1472
|
+
$ref: `#/components/schemas/${modelName}`
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
$ref: "#/components/schemas/_resource"
|
|
1476
|
+
}
|
|
1477
|
+
]
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
links: {
|
|
1481
|
+
allOf: [
|
|
1482
|
+
{
|
|
1483
|
+
$ref: "#/components/schemas/_pagination"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
$ref: "#/components/schemas/_links"
|
|
1487
|
+
}
|
|
1488
|
+
]
|
|
1489
|
+
},
|
|
1490
|
+
meta: {
|
|
1491
|
+
$ref: "#/components/schemas/_meta"
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
generateSharedParams() {
|
|
1497
|
+
return {
|
|
1498
|
+
id: {
|
|
1499
|
+
name: "id",
|
|
1500
|
+
in: "path",
|
|
1501
|
+
required: true,
|
|
1502
|
+
schema: {
|
|
1503
|
+
type: "string"
|
|
1504
|
+
},
|
|
1505
|
+
description: "Resource ID"
|
|
1506
|
+
},
|
|
1507
|
+
include: {
|
|
1508
|
+
name: "include",
|
|
1509
|
+
in: "query",
|
|
1510
|
+
schema: {
|
|
1511
|
+
type: "string"
|
|
1512
|
+
},
|
|
1513
|
+
description: "Comma-separated list of relationships to include"
|
|
1514
|
+
},
|
|
1515
|
+
sort: {
|
|
1516
|
+
name: "sort",
|
|
1517
|
+
in: "query",
|
|
1518
|
+
schema: {
|
|
1519
|
+
type: "string"
|
|
1520
|
+
},
|
|
1521
|
+
description: "Comma-separated list of fields to sort by. Prefix with - for descending"
|
|
1522
|
+
},
|
|
1523
|
+
pageOffset: {
|
|
1524
|
+
name: "page[offset]",
|
|
1525
|
+
in: "query",
|
|
1526
|
+
schema: {
|
|
1527
|
+
type: "integer",
|
|
1528
|
+
minimum: 0
|
|
1529
|
+
},
|
|
1530
|
+
description: "Page offset"
|
|
1531
|
+
},
|
|
1532
|
+
pageLimit: {
|
|
1533
|
+
name: "page[limit]",
|
|
1534
|
+
in: "query",
|
|
1535
|
+
schema: {
|
|
1536
|
+
type: "integer",
|
|
1537
|
+
minimum: 1
|
|
1538
|
+
},
|
|
1539
|
+
description: "Page limit"
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
fieldToSchema(fieldDef) {
|
|
1544
|
+
const baseSchema = this.typeToSchema(fieldDef.type);
|
|
1545
|
+
if (fieldDef.array) {
|
|
1546
|
+
return {
|
|
1547
|
+
type: "array",
|
|
1548
|
+
items: baseSchema
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
if (fieldDef.optional) {
|
|
1552
|
+
return {
|
|
1553
|
+
oneOf: [
|
|
1554
|
+
baseSchema,
|
|
1555
|
+
{
|
|
1556
|
+
type: "null"
|
|
1557
|
+
}
|
|
1558
|
+
]
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
return baseSchema;
|
|
1562
|
+
}
|
|
1563
|
+
typeToSchema(type) {
|
|
1564
|
+
switch (type) {
|
|
1565
|
+
case "String":
|
|
1566
|
+
return {
|
|
1567
|
+
type: "string"
|
|
1568
|
+
};
|
|
1569
|
+
case "Int":
|
|
1570
|
+
case "BigInt":
|
|
1571
|
+
return {
|
|
1572
|
+
type: "integer"
|
|
1573
|
+
};
|
|
1574
|
+
case "Float":
|
|
1575
|
+
return {
|
|
1576
|
+
type: "number"
|
|
1577
|
+
};
|
|
1578
|
+
case "Decimal":
|
|
1579
|
+
return {
|
|
1580
|
+
oneOf: [
|
|
1581
|
+
{
|
|
1582
|
+
type: "number"
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
type: "string"
|
|
1586
|
+
}
|
|
1587
|
+
]
|
|
1588
|
+
};
|
|
1589
|
+
case "Boolean":
|
|
1590
|
+
return {
|
|
1591
|
+
type: "boolean"
|
|
1592
|
+
};
|
|
1593
|
+
case "DateTime":
|
|
1594
|
+
return {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
format: "date-time"
|
|
1597
|
+
};
|
|
1598
|
+
case "Bytes":
|
|
1599
|
+
return {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
format: "byte"
|
|
1602
|
+
};
|
|
1603
|
+
case "Json":
|
|
1604
|
+
case "Unsupported":
|
|
1605
|
+
return {};
|
|
1606
|
+
default:
|
|
1607
|
+
return {
|
|
1608
|
+
$ref: `#/components/schemas/${type}`
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
getIdFields(modelDef) {
|
|
1613
|
+
return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f) => f !== void 0);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Checks if an operation on a model may be denied by access policies.
|
|
1617
|
+
* Returns true when `respectAccessPolicies` is enabled and the model's
|
|
1618
|
+
* policies for the given operation are NOT a constant allow (i.e., not
|
|
1619
|
+
* simply `@@allow('...', true)` with no `@@deny` rules).
|
|
1620
|
+
*/
|
|
1621
|
+
mayDenyAccess(modelDef, operation) {
|
|
1622
|
+
if (!this.specOptions?.respectAccessPolicies) return false;
|
|
1623
|
+
const policyAttrs = (modelDef.attributes ?? []).filter((attr) => attr.name === "@@allow" || attr.name === "@@deny");
|
|
1624
|
+
if (policyAttrs.length === 0) return true;
|
|
1625
|
+
const getArgByName = /* @__PURE__ */ __name((args, name) => args?.find((a) => a.name === name)?.value, "getArgByName");
|
|
1626
|
+
const matchesOperation = /* @__PURE__ */ __name((args) => {
|
|
1627
|
+
const val = getArgByName(args, "operation");
|
|
1628
|
+
if (!val || val.kind !== "literal" || typeof val.value !== "string") return false;
|
|
1629
|
+
const ops = val.value.split(",").map((s) => s.trim());
|
|
1630
|
+
return ops.includes(operation) || ops.includes("all");
|
|
1631
|
+
}, "matchesOperation");
|
|
1632
|
+
const hasEffectiveDeny = policyAttrs.some((attr) => {
|
|
1633
|
+
if (attr.name !== "@@deny" || !matchesOperation(attr.args)) return false;
|
|
1634
|
+
const condition = getArgByName(attr.args, "condition");
|
|
1635
|
+
return !(condition?.kind === "literal" && condition.value === false);
|
|
1636
|
+
});
|
|
1637
|
+
if (hasEffectiveDeny) return true;
|
|
1638
|
+
const relevantAllow = policyAttrs.filter((attr) => attr.name === "@@allow" && matchesOperation(attr.args));
|
|
1639
|
+
const hasConstantAllow = relevantAllow.some((attr) => {
|
|
1640
|
+
const condition = getArgByName(attr.args, "condition");
|
|
1641
|
+
return condition?.kind === "literal" && condition.value === true;
|
|
1642
|
+
});
|
|
1643
|
+
return !hasConstantAllow;
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
232
1647
|
// src/api/rest/index.ts
|
|
233
1648
|
var InvalidValueError = class InvalidValueError2 extends Error {
|
|
234
1649
|
static {
|
|
@@ -411,7 +1826,7 @@ var RestApiHandler = class {
|
|
|
411
1826
|
const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
|
|
412
1827
|
this.modelNameMapping = options.modelNameMapping ?? {};
|
|
413
1828
|
this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
414
|
-
(0,
|
|
1829
|
+
(0, import_common_helpers3.lowerCaseFirst)(k),
|
|
415
1830
|
v
|
|
416
1831
|
]));
|
|
417
1832
|
this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
@@ -420,7 +1835,7 @@ var RestApiHandler = class {
|
|
|
420
1835
|
]));
|
|
421
1836
|
this.externalIdMapping = options.externalIdMapping ?? {};
|
|
422
1837
|
this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
|
|
423
|
-
(0,
|
|
1838
|
+
(0, import_common_helpers3.lowerCaseFirst)(k),
|
|
424
1839
|
v
|
|
425
1840
|
]));
|
|
426
1841
|
this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
|
|
@@ -439,7 +1854,8 @@ var RestApiHandler = class {
|
|
|
439
1854
|
idDivider: import_zod2.default.string().min(1).optional(),
|
|
440
1855
|
urlSegmentCharset: import_zod2.default.string().min(1).optional(),
|
|
441
1856
|
modelNameMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
|
|
442
|
-
externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional()
|
|
1857
|
+
externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
|
|
1858
|
+
queryOptions: queryOptionsSchema.optional()
|
|
443
1859
|
});
|
|
444
1860
|
const parseResult = schema.safeParse(options);
|
|
445
1861
|
if (!parseResult.success) {
|
|
@@ -597,8 +2013,9 @@ var RestApiHandler = class {
|
|
|
597
2013
|
}
|
|
598
2014
|
}
|
|
599
2015
|
handleGenericError(err) {
|
|
600
|
-
|
|
601
|
-
${err.stack
|
|
2016
|
+
const resp = this.makeError("unknownError", err instanceof Error ? `${err.message}` : "Unknown error");
|
|
2017
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
2018
|
+
return resp;
|
|
602
2019
|
}
|
|
603
2020
|
async processProcedureRequest({ client, method, proc, query, requestBody }) {
|
|
604
2021
|
if (!proc) {
|
|
@@ -656,13 +2073,13 @@ ${err.stack}` : "Unknown error");
|
|
|
656
2073
|
}
|
|
657
2074
|
makeProcBadInputErrorResponse(message) {
|
|
658
2075
|
const resp = this.makeError("invalidPayload", message, 400);
|
|
659
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2076
|
+
log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}`);
|
|
660
2077
|
return resp;
|
|
661
2078
|
}
|
|
662
2079
|
makeProcGenericErrorResponse(err) {
|
|
663
2080
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
664
2081
|
const resp = this.makeError("unknownError", message, 500);
|
|
665
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2082
|
+
log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
666
2083
|
return resp;
|
|
667
2084
|
}
|
|
668
2085
|
async processSingleRead(client, type, resourceId, query) {
|
|
@@ -735,7 +2152,7 @@ ${err.stack}` : "Unknown error");
|
|
|
735
2152
|
select = relationSelect;
|
|
736
2153
|
}
|
|
737
2154
|
if (!select) {
|
|
738
|
-
const { select: partialFields, error } = this.buildPartialSelect((0,
|
|
2155
|
+
const { select: partialFields, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
|
|
739
2156
|
if (error) return error;
|
|
740
2157
|
select = partialFields ? {
|
|
741
2158
|
[relationship]: {
|
|
@@ -1030,7 +2447,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1030
2447
|
}
|
|
1031
2448
|
if (relationInfo.isCollection) {
|
|
1032
2449
|
createPayload.data[key] = {
|
|
1033
|
-
connect: (0,
|
|
2450
|
+
connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1034
2451
|
};
|
|
1035
2452
|
} else {
|
|
1036
2453
|
if (typeof data.data !== "object") {
|
|
@@ -1096,10 +2513,10 @@ ${err.stack}` : "Unknown error");
|
|
|
1096
2513
|
}
|
|
1097
2514
|
if (relationInfo.isCollection) {
|
|
1098
2515
|
upsertPayload.create[key] = {
|
|
1099
|
-
connect: (0,
|
|
2516
|
+
connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1100
2517
|
};
|
|
1101
2518
|
upsertPayload.update[key] = {
|
|
1102
|
-
set: (0,
|
|
2519
|
+
set: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1103
2520
|
};
|
|
1104
2521
|
} else {
|
|
1105
2522
|
if (typeof data.data !== "object") {
|
|
@@ -1180,7 +2597,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1180
2597
|
const relationVerb = mode === "create" ? "connect" : mode === "delete" ? "disconnect" : "set";
|
|
1181
2598
|
updateArgs.data = {
|
|
1182
2599
|
[relationship]: {
|
|
1183
|
-
[relationVerb]: (0,
|
|
2600
|
+
[relationVerb]: (0, import_common_helpers3.enumerate)(parsed.data.data).map((item) => this.makeIdFilter(relationInfo.idFields, item.id))
|
|
1184
2601
|
}
|
|
1185
2602
|
};
|
|
1186
2603
|
}
|
|
@@ -1223,7 +2640,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1223
2640
|
}
|
|
1224
2641
|
if (relationInfo.isCollection) {
|
|
1225
2642
|
updatePayload.data[key] = {
|
|
1226
|
-
set: (0,
|
|
2643
|
+
set: (0, import_common_helpers3.enumerate)(data.data).map((item) => ({
|
|
1227
2644
|
[this.makeDefaultIdKey(relationInfo.idFields)]: item.id
|
|
1228
2645
|
}))
|
|
1229
2646
|
};
|
|
@@ -1279,7 +2696,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1279
2696
|
}
|
|
1280
2697
|
getIdFields(model) {
|
|
1281
2698
|
const modelDef = this.requireModel(model);
|
|
1282
|
-
const modelLower = (0,
|
|
2699
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1283
2700
|
if (!(modelLower in this.externalIdMapping)) {
|
|
1284
2701
|
return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
|
|
1285
2702
|
}
|
|
@@ -1313,7 +2730,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1313
2730
|
log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
|
|
1314
2731
|
continue;
|
|
1315
2732
|
}
|
|
1316
|
-
const modelInfo = this.typeMap[(0,
|
|
2733
|
+
const modelInfo = this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)] = {
|
|
1317
2734
|
name: model,
|
|
1318
2735
|
idFields,
|
|
1319
2736
|
relationships: {},
|
|
@@ -1338,7 +2755,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1338
2755
|
}
|
|
1339
2756
|
}
|
|
1340
2757
|
getModelInfo(model) {
|
|
1341
|
-
return this.typeMap[(0,
|
|
2758
|
+
return this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)];
|
|
1342
2759
|
}
|
|
1343
2760
|
makeLinkUrl(path) {
|
|
1344
2761
|
return `${this.options.endpoint}${path}`;
|
|
@@ -1347,7 +2764,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1347
2764
|
const linkers = {};
|
|
1348
2765
|
for (const model of Object.keys(this.schema.models)) {
|
|
1349
2766
|
const ids = this.getIdFields(model);
|
|
1350
|
-
const modelLower = (0,
|
|
2767
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1351
2768
|
const mappedModel = this.mapModelName(modelLower);
|
|
1352
2769
|
if (ids.length < 1) {
|
|
1353
2770
|
continue;
|
|
@@ -1376,7 +2793,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1376
2793
|
this.serializers.set(modelLower, serializer);
|
|
1377
2794
|
}
|
|
1378
2795
|
for (const model of Object.keys(this.schema.models)) {
|
|
1379
|
-
const modelLower = (0,
|
|
2796
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1380
2797
|
const serializer = this.serializers.get(modelLower);
|
|
1381
2798
|
if (!serializer) {
|
|
1382
2799
|
continue;
|
|
@@ -1387,7 +2804,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1387
2804
|
if (!fieldDef.relation) {
|
|
1388
2805
|
continue;
|
|
1389
2806
|
}
|
|
1390
|
-
const fieldSerializer = this.serializers.get((0,
|
|
2807
|
+
const fieldSerializer = this.serializers.get((0, import_common_helpers3.lowerCaseFirst)(fieldDef.type));
|
|
1391
2808
|
if (!fieldSerializer) {
|
|
1392
2809
|
continue;
|
|
1393
2810
|
}
|
|
@@ -1421,12 +2838,12 @@ ${err.stack}` : "Unknown error");
|
|
|
1421
2838
|
}
|
|
1422
2839
|
}
|
|
1423
2840
|
async serializeItems(model, items, options) {
|
|
1424
|
-
model = (0,
|
|
2841
|
+
model = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1425
2842
|
const serializer = this.serializers.get(model);
|
|
1426
2843
|
if (!serializer) {
|
|
1427
2844
|
throw new Error(`serializer not found for model ${model}`);
|
|
1428
2845
|
}
|
|
1429
|
-
const itemsWithId = (0,
|
|
2846
|
+
const itemsWithId = (0, import_common_helpers3.clone)(items);
|
|
1430
2847
|
this.injectCompoundId(model, itemsWithId);
|
|
1431
2848
|
const serialized = await serializer.serialize(itemsWithId, options);
|
|
1432
2849
|
const plainResult = this.toPlainObject(serialized);
|
|
@@ -1445,7 +2862,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1445
2862
|
if (!typeInfo) {
|
|
1446
2863
|
return;
|
|
1447
2864
|
}
|
|
1448
|
-
(0,
|
|
2865
|
+
(0, import_common_helpers3.enumerate)(items).forEach((item) => {
|
|
1449
2866
|
if (!item) {
|
|
1450
2867
|
return;
|
|
1451
2868
|
}
|
|
@@ -1620,7 +3037,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1620
3037
|
const url = new URL(this.makeLinkUrl(path));
|
|
1621
3038
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
1622
3039
|
if (key.startsWith("filter[") || key.startsWith("sort[") || key === "include" || key.startsWith("include[") || key.startsWith("fields[")) {
|
|
1623
|
-
for (const v of (0,
|
|
3040
|
+
for (const v of (0, import_common_helpers3.enumerate)(value)) {
|
|
1624
3041
|
url.searchParams.append(key, v);
|
|
1625
3042
|
}
|
|
1626
3043
|
}
|
|
@@ -1692,7 +3109,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1692
3109
|
const item = {};
|
|
1693
3110
|
let curr = item;
|
|
1694
3111
|
let currType = typeInfo;
|
|
1695
|
-
for (const filterValue of (0,
|
|
3112
|
+
for (const filterValue of (0, import_common_helpers3.enumerate)(value)) {
|
|
1696
3113
|
for (let i = 0; i < filterKeys.length; i++) {
|
|
1697
3114
|
let filterKey = filterKeys[i];
|
|
1698
3115
|
let filterOp;
|
|
@@ -1771,7 +3188,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1771
3188
|
};
|
|
1772
3189
|
}
|
|
1773
3190
|
const result = [];
|
|
1774
|
-
for (const sortSpec of (0,
|
|
3191
|
+
for (const sortSpec of (0, import_common_helpers3.enumerate)(query["sort"])) {
|
|
1775
3192
|
const sortFields = sortSpec.split(",").filter((i) => i);
|
|
1776
3193
|
for (const sortField of sortFields) {
|
|
1777
3194
|
const dir = sortField.startsWith("-") ? "desc" : "asc";
|
|
@@ -1840,7 +3257,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1840
3257
|
}
|
|
1841
3258
|
const result = {};
|
|
1842
3259
|
const allIncludes = [];
|
|
1843
|
-
for (const includeItem of (0,
|
|
3260
|
+
for (const includeItem of (0, import_common_helpers3.enumerate)(include)) {
|
|
1844
3261
|
const inclusions = includeItem.split(",").filter((i) => i);
|
|
1845
3262
|
for (const inclusion of inclusions) {
|
|
1846
3263
|
allIncludes.push(inclusion);
|
|
@@ -1863,7 +3280,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1863
3280
|
error: this.makeUnsupportedModelError(relationInfo.type)
|
|
1864
3281
|
};
|
|
1865
3282
|
}
|
|
1866
|
-
const { select, error } = this.buildPartialSelect((0,
|
|
3283
|
+
const { select, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
|
|
1867
3284
|
if (error) return {
|
|
1868
3285
|
select: void 0,
|
|
1869
3286
|
error
|
|
@@ -2041,7 +3458,7 @@ ${err.stack}` : "Unknown error");
|
|
|
2041
3458
|
status = status ?? this.errors[code]?.status ?? 500;
|
|
2042
3459
|
const error = {
|
|
2043
3460
|
status,
|
|
2044
|
-
code: (0,
|
|
3461
|
+
code: (0, import_common_helpers3.paramCase)(code),
|
|
2045
3462
|
title: this.errors[code]?.title
|
|
2046
3463
|
};
|
|
2047
3464
|
if (detail) {
|
|
@@ -2063,10 +3480,15 @@ ${err.stack}` : "Unknown error");
|
|
|
2063
3480
|
makeUnsupportedRelationshipError(model, relationship, status) {
|
|
2064
3481
|
return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
|
|
2065
3482
|
}
|
|
3483
|
+
//#endregion
|
|
3484
|
+
async generateSpec(options) {
|
|
3485
|
+
const generator = new RestApiSpecGenerator(this.options);
|
|
3486
|
+
return generator.generateSpec(options);
|
|
3487
|
+
}
|
|
2066
3488
|
};
|
|
2067
3489
|
|
|
2068
3490
|
// src/api/rpc/index.ts
|
|
2069
|
-
var
|
|
3491
|
+
var import_common_helpers4 = require("@zenstackhq/common-helpers");
|
|
2070
3492
|
var import_orm3 = require("@zenstackhq/orm");
|
|
2071
3493
|
var import_superjson4 = __toESM(require("superjson"), 1);
|
|
2072
3494
|
var import_ts_pattern3 = require("ts-pattern");
|
|
@@ -2087,7 +3509,8 @@ var RPCApiHandler = class {
|
|
|
2087
3509
|
validateOptions(options) {
|
|
2088
3510
|
const schema = import_zod3.default.strictObject({
|
|
2089
3511
|
schema: import_zod3.default.object(),
|
|
2090
|
-
log: loggerSchema.optional()
|
|
3512
|
+
log: loggerSchema.optional(),
|
|
3513
|
+
queryOptions: queryOptionsSchema.optional()
|
|
2091
3514
|
});
|
|
2092
3515
|
const parseResult = schema.safeParse(options);
|
|
2093
3516
|
if (!parseResult.success) {
|
|
@@ -2124,7 +3547,7 @@ var RPCApiHandler = class {
|
|
|
2124
3547
|
requestBody
|
|
2125
3548
|
});
|
|
2126
3549
|
}
|
|
2127
|
-
model = (0,
|
|
3550
|
+
model = (0, import_common_helpers4.lowerCaseFirst)(model);
|
|
2128
3551
|
method = method.toUpperCase();
|
|
2129
3552
|
let args;
|
|
2130
3553
|
let resCode = 200;
|
|
@@ -2191,7 +3614,7 @@ var RPCApiHandler = class {
|
|
|
2191
3614
|
if (!this.isValidModel(client, model)) {
|
|
2192
3615
|
return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
|
|
2193
3616
|
}
|
|
2194
|
-
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0,
|
|
3617
|
+
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0, import_common_helpers4.safeJSONStringify)(processedArgs)}`);
|
|
2195
3618
|
const clientResult = await client[model][op](processedArgs);
|
|
2196
3619
|
let responseBody = {
|
|
2197
3620
|
data: clientResult
|
|
@@ -2211,7 +3634,7 @@ var RPCApiHandler = class {
|
|
|
2211
3634
|
status: resCode,
|
|
2212
3635
|
body: responseBody
|
|
2213
3636
|
};
|
|
2214
|
-
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0,
|
|
3637
|
+
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2215
3638
|
return response;
|
|
2216
3639
|
} catch (err) {
|
|
2217
3640
|
log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
|
|
@@ -2248,7 +3671,7 @@ var RPCApiHandler = class {
|
|
|
2248
3671
|
if (!VALID_OPS.has(itemOp)) {
|
|
2249
3672
|
return this.makeBadInputErrorResponse(`operation at index ${i} has invalid op: ${itemOp}`);
|
|
2250
3673
|
}
|
|
2251
|
-
if (!this.isValidModel(client, (0,
|
|
3674
|
+
if (!this.isValidModel(client, (0, import_common_helpers4.lowerCaseFirst)(itemModel))) {
|
|
2252
3675
|
return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`);
|
|
2253
3676
|
}
|
|
2254
3677
|
if (itemArgs !== void 0 && itemArgs !== null && (typeof itemArgs !== "object" || Array.isArray(itemArgs))) {
|
|
@@ -2259,7 +3682,7 @@ var RPCApiHandler = class {
|
|
|
2259
3682
|
return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
|
|
2260
3683
|
}
|
|
2261
3684
|
processedOps.push({
|
|
2262
|
-
model: (0,
|
|
3685
|
+
model: (0, import_common_helpers4.lowerCaseFirst)(itemModel),
|
|
2263
3686
|
op: itemOp,
|
|
2264
3687
|
args: processedArgs
|
|
2265
3688
|
});
|
|
@@ -2283,7 +3706,7 @@ var RPCApiHandler = class {
|
|
|
2283
3706
|
status: 200,
|
|
2284
3707
|
body: responseBody
|
|
2285
3708
|
};
|
|
2286
|
-
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0,
|
|
3709
|
+
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2287
3710
|
return response;
|
|
2288
3711
|
} catch (err) {
|
|
2289
3712
|
log(this.options.log, "error", `error occurred when handling "$transaction" request`, err);
|
|
@@ -2345,7 +3768,7 @@ var RPCApiHandler = class {
|
|
|
2345
3768
|
status: 200,
|
|
2346
3769
|
body: responseBody
|
|
2347
3770
|
};
|
|
2348
|
-
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0,
|
|
3771
|
+
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2349
3772
|
return response;
|
|
2350
3773
|
} catch (err) {
|
|
2351
3774
|
log(this.options.log, "error", `error occurred when handling "$procs.${proc}" request`, err);
|
|
@@ -2356,7 +3779,7 @@ var RPCApiHandler = class {
|
|
|
2356
3779
|
}
|
|
2357
3780
|
}
|
|
2358
3781
|
isValidModel(client, model) {
|
|
2359
|
-
return Object.keys(client.$schema.models).some((m) => (0,
|
|
3782
|
+
return Object.keys(client.$schema.models).some((m) => (0, import_common_helpers4.lowerCaseFirst)(m) === (0, import_common_helpers4.lowerCaseFirst)(model));
|
|
2360
3783
|
}
|
|
2361
3784
|
makeBadInputErrorResponse(message) {
|
|
2362
3785
|
const resp = {
|
|
@@ -2367,7 +3790,7 @@ var RPCApiHandler = class {
|
|
|
2367
3790
|
}
|
|
2368
3791
|
}
|
|
2369
3792
|
};
|
|
2370
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
3793
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
|
|
2371
3794
|
return resp;
|
|
2372
3795
|
}
|
|
2373
3796
|
makeGenericErrorResponse(err) {
|
|
@@ -2379,7 +3802,7 @@ var RPCApiHandler = class {
|
|
|
2379
3802
|
}
|
|
2380
3803
|
}
|
|
2381
3804
|
};
|
|
2382
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
3805
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
2383
3806
|
return resp;
|
|
2384
3807
|
}
|
|
2385
3808
|
makeORMErrorResponse(err) {
|
|
@@ -2411,7 +3834,7 @@ var RPCApiHandler = class {
|
|
|
2411
3834
|
error
|
|
2412
3835
|
}
|
|
2413
3836
|
};
|
|
2414
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
3837
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
|
|
2415
3838
|
return resp;
|
|
2416
3839
|
}
|
|
2417
3840
|
async processRequestPayload(args) {
|