@zenstackhq/server 3.5.0-beta.3 → 3.5.0-beta.5
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/README.md +40 -0
- package/dist/api.cjs +1915 -91
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +83 -5
- package/dist/api.d.ts +83 -5
- package/dist/api.js +1873 -49
- 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);
|
|
@@ -195,39 +215,1434 @@ function log(logger, level, message, error) {
|
|
|
195
215
|
if (!logger) {
|
|
196
216
|
return;
|
|
197
217
|
}
|
|
198
|
-
const getMessage = typeof message === "function" ? message : () => message;
|
|
199
|
-
if (typeof logger === "function") {
|
|
200
|
-
logger(level, getMessage(), error);
|
|
201
|
-
} else if (logger.includes(level)) {
|
|
202
|
-
const logFn = (0, import_ts_pattern.match)(level).with("debug", () => console.debug).with("info", () => console.info).with("warn", () => console.warn).with("error", () => console.error).exhaustive();
|
|
203
|
-
logFn(`@zenstackhq/server: [${level}] ${getMessage()}${error ? `
|
|
204
|
-
${error}` : ""}`);
|
|
218
|
+
const getMessage = typeof message === "function" ? message : () => message;
|
|
219
|
+
if (typeof logger === "function") {
|
|
220
|
+
logger(level, getMessage(), error);
|
|
221
|
+
} else if (logger.includes(level)) {
|
|
222
|
+
const logFn = (0, import_ts_pattern.match)(level).with("debug", () => console.debug).with("info", () => console.info).with("warn", () => console.warn).with("error", () => console.error).exhaustive();
|
|
223
|
+
logFn(`@zenstackhq/server: [${level}] ${getMessage()}${error ? `
|
|
224
|
+
${error}` : ""}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
__name(log, "log");
|
|
228
|
+
function registerCustomSerializers() {
|
|
229
|
+
import_superjson2.default.registerCustom({
|
|
230
|
+
isApplicable: /* @__PURE__ */ __name((v) => import_decimal.Decimal.isDecimal(v), "isApplicable"),
|
|
231
|
+
serialize: /* @__PURE__ */ __name((v) => v.toJSON(), "serialize"),
|
|
232
|
+
deserialize: /* @__PURE__ */ __name((v) => new import_decimal.Decimal(v), "deserialize")
|
|
233
|
+
}, "Decimal");
|
|
234
|
+
if (globalThis.Buffer) {
|
|
235
|
+
import_superjson2.default.registerCustom({
|
|
236
|
+
isApplicable: /* @__PURE__ */ __name((v) => Buffer.isBuffer(v), "isApplicable"),
|
|
237
|
+
serialize: /* @__PURE__ */ __name((v) => v.toString("base64"), "serialize"),
|
|
238
|
+
deserialize: /* @__PURE__ */ __name((v) => Buffer.from(v, "base64"), "deserialize")
|
|
239
|
+
}, "Bytes");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
__name(registerCustomSerializers, "registerCustomSerializers");
|
|
243
|
+
function getZodErrorMessage(error) {
|
|
244
|
+
if ("_zod" in error) {
|
|
245
|
+
return (0, import_v4.fromError)(error).toString();
|
|
246
|
+
} else {
|
|
247
|
+
return (0, import_v3.fromError)(error).toString();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
__name(getZodErrorMessage, "getZodErrorMessage");
|
|
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
|
+
}
|
|
205
1611
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
function registerCustomSerializers() {
|
|
209
|
-
import_superjson2.default.registerCustom({
|
|
210
|
-
isApplicable: /* @__PURE__ */ __name((v) => import_decimal.Decimal.isDecimal(v), "isApplicable"),
|
|
211
|
-
serialize: /* @__PURE__ */ __name((v) => v.toJSON(), "serialize"),
|
|
212
|
-
deserialize: /* @__PURE__ */ __name((v) => new import_decimal.Decimal(v), "deserialize")
|
|
213
|
-
}, "Decimal");
|
|
214
|
-
if (globalThis.Buffer) {
|
|
215
|
-
import_superjson2.default.registerCustom({
|
|
216
|
-
isApplicable: /* @__PURE__ */ __name((v) => Buffer.isBuffer(v), "isApplicable"),
|
|
217
|
-
serialize: /* @__PURE__ */ __name((v) => v.toString("base64"), "serialize"),
|
|
218
|
-
deserialize: /* @__PURE__ */ __name((v) => Buffer.from(v, "base64"), "deserialize")
|
|
219
|
-
}, "Bytes");
|
|
1612
|
+
getIdFields(modelDef) {
|
|
1613
|
+
return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f) => f !== void 0);
|
|
220
1614
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
228
1644
|
}
|
|
229
|
-
}
|
|
230
|
-
__name(getZodErrorMessage, "getZodErrorMessage");
|
|
1645
|
+
};
|
|
231
1646
|
|
|
232
1647
|
// src/api/rest/index.ts
|
|
233
1648
|
var InvalidValueError = class InvalidValueError2 extends Error {
|
|
@@ -404,6 +1819,7 @@ var RestApiHandler = class {
|
|
|
404
1819
|
modelNameMapping;
|
|
405
1820
|
reverseModelNameMapping;
|
|
406
1821
|
externalIdMapping;
|
|
1822
|
+
nestedRoutes;
|
|
407
1823
|
constructor(options) {
|
|
408
1824
|
this.options = options;
|
|
409
1825
|
this.validateOptions(options);
|
|
@@ -411,7 +1827,7 @@ var RestApiHandler = class {
|
|
|
411
1827
|
const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
|
|
412
1828
|
this.modelNameMapping = options.modelNameMapping ?? {};
|
|
413
1829
|
this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
414
|
-
(0,
|
|
1830
|
+
(0, import_common_helpers3.lowerCaseFirst)(k),
|
|
415
1831
|
v
|
|
416
1832
|
]));
|
|
417
1833
|
this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
|
|
@@ -420,9 +1836,10 @@ var RestApiHandler = class {
|
|
|
420
1836
|
]));
|
|
421
1837
|
this.externalIdMapping = options.externalIdMapping ?? {};
|
|
422
1838
|
this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
|
|
423
|
-
(0,
|
|
1839
|
+
(0, import_common_helpers3.lowerCaseFirst)(k),
|
|
424
1840
|
v
|
|
425
1841
|
]));
|
|
1842
|
+
this.nestedRoutes = options.nestedRoutes ?? false;
|
|
426
1843
|
this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
|
|
427
1844
|
this.buildTypeMap();
|
|
428
1845
|
this.buildSerializers();
|
|
@@ -439,7 +1856,9 @@ var RestApiHandler = class {
|
|
|
439
1856
|
idDivider: import_zod2.default.string().min(1).optional(),
|
|
440
1857
|
urlSegmentCharset: import_zod2.default.string().min(1).optional(),
|
|
441
1858
|
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()
|
|
1859
|
+
externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
|
|
1860
|
+
queryOptions: queryOptionsSchema.optional(),
|
|
1861
|
+
nestedRoutes: import_zod2.default.boolean().optional()
|
|
443
1862
|
});
|
|
444
1863
|
const parseResult = schema.safeParse(options);
|
|
445
1864
|
if (!parseResult.success) {
|
|
@@ -464,6 +1883,12 @@ var RestApiHandler = class {
|
|
|
464
1883
|
":type",
|
|
465
1884
|
":id"
|
|
466
1885
|
]), options),
|
|
1886
|
+
["nestedSingle"]: new import_url_pattern.default(buildPath([
|
|
1887
|
+
":type",
|
|
1888
|
+
":id",
|
|
1889
|
+
":relationship",
|
|
1890
|
+
":childId"
|
|
1891
|
+
]), options),
|
|
467
1892
|
["fetchRelationship"]: new import_url_pattern.default(buildPath([
|
|
468
1893
|
":type",
|
|
469
1894
|
":id",
|
|
@@ -483,6 +1908,81 @@ var RestApiHandler = class {
|
|
|
483
1908
|
mapModelName(modelName) {
|
|
484
1909
|
return this.modelNameMapping[modelName] ?? modelName;
|
|
485
1910
|
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Resolves child model type and reverse relation from a parent relation name.
|
|
1913
|
+
* e.g. given parentType='user', parentRelation='posts', returns { childType:'post', reverseRelation:'author' }
|
|
1914
|
+
*/
|
|
1915
|
+
resolveNestedRelation(parentType, parentRelation) {
|
|
1916
|
+
const parentInfo = this.getModelInfo(parentType);
|
|
1917
|
+
if (!parentInfo) return void 0;
|
|
1918
|
+
const field = this.schema.models[parentInfo.name]?.fields[parentRelation];
|
|
1919
|
+
if (!field?.relation) return void 0;
|
|
1920
|
+
const reverseRelation = field.relation.opposite;
|
|
1921
|
+
if (!reverseRelation) return void 0;
|
|
1922
|
+
return {
|
|
1923
|
+
childType: (0, import_common_helpers3.lowerCaseFirst)(field.type),
|
|
1924
|
+
reverseRelation,
|
|
1925
|
+
isCollection: !!field.array
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
mergeFilters(left, right) {
|
|
1929
|
+
if (!left) {
|
|
1930
|
+
return right;
|
|
1931
|
+
}
|
|
1932
|
+
if (!right) {
|
|
1933
|
+
return left;
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
AND: [
|
|
1937
|
+
left,
|
|
1938
|
+
right
|
|
1939
|
+
]
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Builds a WHERE filter for the child model that constrains results to those belonging to the given parent.
|
|
1944
|
+
* @param parentType lowercased parent model name
|
|
1945
|
+
* @param parentId parent resource ID string
|
|
1946
|
+
* @param parentRelation relation field name on the parent model (e.g. 'posts')
|
|
1947
|
+
*/
|
|
1948
|
+
buildNestedParentFilter(parentType, parentId, parentRelation) {
|
|
1949
|
+
const parentInfo = this.getModelInfo(parentType);
|
|
1950
|
+
if (!parentInfo) {
|
|
1951
|
+
return {
|
|
1952
|
+
filter: void 0,
|
|
1953
|
+
error: this.makeUnsupportedModelError(parentType)
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const resolved = this.resolveNestedRelation(parentType, parentRelation);
|
|
1957
|
+
if (!resolved) {
|
|
1958
|
+
return {
|
|
1959
|
+
filter: void 0,
|
|
1960
|
+
error: this.makeError("invalidPath", `invalid nested route: cannot resolve relation "${parentType}.${parentRelation}"`)
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
const { reverseRelation } = resolved;
|
|
1964
|
+
const childInfo = this.getModelInfo(resolved.childType);
|
|
1965
|
+
if (!childInfo) {
|
|
1966
|
+
return {
|
|
1967
|
+
filter: void 0,
|
|
1968
|
+
error: this.makeUnsupportedModelError(resolved.childType)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const reverseRelInfo = childInfo.relationships[reverseRelation];
|
|
1972
|
+
const relationFilter = reverseRelInfo?.isCollection ? {
|
|
1973
|
+
[reverseRelation]: {
|
|
1974
|
+
some: this.makeIdFilter(parentInfo.idFields, parentId, false)
|
|
1975
|
+
}
|
|
1976
|
+
} : {
|
|
1977
|
+
[reverseRelation]: {
|
|
1978
|
+
is: this.makeIdFilter(parentInfo.idFields, parentId, false)
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
return {
|
|
1982
|
+
filter: relationFilter,
|
|
1983
|
+
error: void 0
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
486
1986
|
matchUrlPattern(path, routeType) {
|
|
487
1987
|
const pattern = this.urlPatternMap[routeType];
|
|
488
1988
|
if (!pattern) {
|
|
@@ -530,6 +2030,10 @@ var RestApiHandler = class {
|
|
|
530
2030
|
if (match4) {
|
|
531
2031
|
return await this.processReadRelationship(client, match4.type, match4.id, match4.relationship, query);
|
|
532
2032
|
}
|
|
2033
|
+
match4 = this.matchUrlPattern(path, "nestedSingle");
|
|
2034
|
+
if (match4 && this.nestedRoutes && this.resolveNestedRelation(match4.type, match4.relationship)?.isCollection) {
|
|
2035
|
+
return await this.processNestedSingleRead(client, match4.type, match4.id, match4.relationship, match4.childId, query);
|
|
2036
|
+
}
|
|
533
2037
|
match4 = this.matchUrlPattern(path, "collection");
|
|
534
2038
|
if (match4) {
|
|
535
2039
|
return await this.processCollectionRead(client, match4.type, query);
|
|
@@ -540,6 +2044,10 @@ var RestApiHandler = class {
|
|
|
540
2044
|
if (!requestBody) {
|
|
541
2045
|
return this.makeError("invalidPayload");
|
|
542
2046
|
}
|
|
2047
|
+
const nestedMatch = this.matchUrlPattern(path, "fetchRelationship");
|
|
2048
|
+
if (nestedMatch && this.nestedRoutes && this.resolveNestedRelation(nestedMatch.type, nestedMatch.relationship)?.isCollection) {
|
|
2049
|
+
return await this.processNestedCreate(client, nestedMatch.type, nestedMatch.id, nestedMatch.relationship, query, requestBody);
|
|
2050
|
+
}
|
|
543
2051
|
let match4 = this.matchUrlPattern(path, "collection");
|
|
544
2052
|
if (match4) {
|
|
545
2053
|
const body = requestBody;
|
|
@@ -562,24 +2070,36 @@ var RestApiHandler = class {
|
|
|
562
2070
|
if (!requestBody) {
|
|
563
2071
|
return this.makeError("invalidPayload");
|
|
564
2072
|
}
|
|
565
|
-
let match4 = this.matchUrlPattern(path, "
|
|
2073
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
566
2074
|
if (match4) {
|
|
567
|
-
return await this.
|
|
2075
|
+
return await this.processRelationshipCRUD(client, "update", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
568
2076
|
}
|
|
569
|
-
|
|
2077
|
+
const nestedToOnePatchMatch = this.matchUrlPattern(path, "fetchRelationship");
|
|
2078
|
+
if (nestedToOnePatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship) && !this.resolveNestedRelation(nestedToOnePatchMatch.type, nestedToOnePatchMatch.relationship)?.isCollection) {
|
|
2079
|
+
return await this.processNestedUpdate(client, nestedToOnePatchMatch.type, nestedToOnePatchMatch.id, nestedToOnePatchMatch.relationship, void 0, query, requestBody);
|
|
2080
|
+
}
|
|
2081
|
+
const nestedPatchMatch = this.matchUrlPattern(path, "nestedSingle");
|
|
2082
|
+
if (nestedPatchMatch && this.nestedRoutes && this.resolveNestedRelation(nestedPatchMatch.type, nestedPatchMatch.relationship)?.isCollection) {
|
|
2083
|
+
return await this.processNestedUpdate(client, nestedPatchMatch.type, nestedPatchMatch.id, nestedPatchMatch.relationship, nestedPatchMatch.childId, query, requestBody);
|
|
2084
|
+
}
|
|
2085
|
+
match4 = this.matchUrlPattern(path, "single");
|
|
570
2086
|
if (match4) {
|
|
571
|
-
return await this.
|
|
2087
|
+
return await this.processUpdate(client, match4.type, match4.id, query, requestBody);
|
|
572
2088
|
}
|
|
573
2089
|
return this.makeError("invalidPath");
|
|
574
2090
|
}
|
|
575
2091
|
case "DELETE": {
|
|
576
|
-
let match4 = this.matchUrlPattern(path, "
|
|
2092
|
+
let match4 = this.matchUrlPattern(path, "relationship");
|
|
577
2093
|
if (match4) {
|
|
578
|
-
return await this.
|
|
2094
|
+
return await this.processRelationshipCRUD(client, "delete", match4.type, match4.id, match4.relationship, query, requestBody);
|
|
579
2095
|
}
|
|
580
|
-
|
|
2096
|
+
const nestedDeleteMatch = this.matchUrlPattern(path, "nestedSingle");
|
|
2097
|
+
if (nestedDeleteMatch && this.nestedRoutes && this.resolveNestedRelation(nestedDeleteMatch.type, nestedDeleteMatch.relationship)?.isCollection) {
|
|
2098
|
+
return await this.processNestedDelete(client, nestedDeleteMatch.type, nestedDeleteMatch.id, nestedDeleteMatch.relationship, nestedDeleteMatch.childId);
|
|
2099
|
+
}
|
|
2100
|
+
match4 = this.matchUrlPattern(path, "single");
|
|
581
2101
|
if (match4) {
|
|
582
|
-
return await this.
|
|
2102
|
+
return await this.processDelete(client, match4.type, match4.id);
|
|
583
2103
|
}
|
|
584
2104
|
return this.makeError("invalidPath");
|
|
585
2105
|
}
|
|
@@ -597,8 +2117,9 @@ var RestApiHandler = class {
|
|
|
597
2117
|
}
|
|
598
2118
|
}
|
|
599
2119
|
handleGenericError(err) {
|
|
600
|
-
|
|
601
|
-
${err.stack
|
|
2120
|
+
const resp = this.makeError("unknownError", err instanceof Error ? `${err.message}` : "Unknown error");
|
|
2121
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
2122
|
+
return resp;
|
|
602
2123
|
}
|
|
603
2124
|
async processProcedureRequest({ client, method, proc, query, requestBody }) {
|
|
604
2125
|
if (!proc) {
|
|
@@ -656,29 +2177,32 @@ ${err.stack}` : "Unknown error");
|
|
|
656
2177
|
}
|
|
657
2178
|
makeProcBadInputErrorResponse(message) {
|
|
658
2179
|
const resp = this.makeError("invalidPayload", message, 400);
|
|
659
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2180
|
+
log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}`);
|
|
660
2181
|
return resp;
|
|
661
2182
|
}
|
|
662
2183
|
makeProcGenericErrorResponse(err) {
|
|
663
2184
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
664
2185
|
const resp = this.makeError("unknownError", message, 500);
|
|
665
|
-
log(this.log, "debug", () => `sending error response: ${
|
|
2186
|
+
log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
666
2187
|
return resp;
|
|
667
2188
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
};
|
|
2189
|
+
/**
|
|
2190
|
+
* Builds the ORM `args` object (include, select) shared by single-read operations.
|
|
2191
|
+
* Returns the args to pass to findUnique/findFirst and the resolved `include` list for serialization,
|
|
2192
|
+
* or an error response if query params are invalid.
|
|
2193
|
+
*/
|
|
2194
|
+
buildSingleReadArgs(type, query) {
|
|
2195
|
+
const args = {};
|
|
676
2196
|
this.includeRelationshipIds(type, args, "include");
|
|
677
2197
|
let include;
|
|
678
2198
|
if (query?.["include"]) {
|
|
679
2199
|
const { select: select2, error: error2, allIncludes } = this.buildRelationSelect(type, query["include"], query);
|
|
680
2200
|
if (error2) {
|
|
681
|
-
return
|
|
2201
|
+
return {
|
|
2202
|
+
args,
|
|
2203
|
+
include,
|
|
2204
|
+
error: error2
|
|
2205
|
+
};
|
|
682
2206
|
}
|
|
683
2207
|
if (select2) {
|
|
684
2208
|
args.include = {
|
|
@@ -689,7 +2213,11 @@ ${err.stack}` : "Unknown error");
|
|
|
689
2213
|
include = allIncludes;
|
|
690
2214
|
}
|
|
691
2215
|
const { select, error } = this.buildPartialSelect(type, query);
|
|
692
|
-
if (error) return
|
|
2216
|
+
if (error) return {
|
|
2217
|
+
args,
|
|
2218
|
+
include,
|
|
2219
|
+
error
|
|
2220
|
+
};
|
|
693
2221
|
if (select) {
|
|
694
2222
|
args.select = {
|
|
695
2223
|
...select,
|
|
@@ -703,6 +2231,19 @@ ${err.stack}` : "Unknown error");
|
|
|
703
2231
|
args.include = void 0;
|
|
704
2232
|
}
|
|
705
2233
|
}
|
|
2234
|
+
return {
|
|
2235
|
+
args,
|
|
2236
|
+
include
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
async processSingleRead(client, type, resourceId, query) {
|
|
2240
|
+
const typeInfo = this.getModelInfo(type);
|
|
2241
|
+
if (!typeInfo) {
|
|
2242
|
+
return this.makeUnsupportedModelError(type);
|
|
2243
|
+
}
|
|
2244
|
+
const { args, include, error } = this.buildSingleReadArgs(type, query);
|
|
2245
|
+
if (error) return error;
|
|
2246
|
+
args.where = this.makeIdFilter(typeInfo.idFields, resourceId);
|
|
706
2247
|
const entity = await client[type].findUnique(args);
|
|
707
2248
|
if (entity) {
|
|
708
2249
|
return {
|
|
@@ -735,7 +2276,7 @@ ${err.stack}` : "Unknown error");
|
|
|
735
2276
|
select = relationSelect;
|
|
736
2277
|
}
|
|
737
2278
|
if (!select) {
|
|
738
|
-
const { select: partialFields, error } = this.buildPartialSelect((0,
|
|
2279
|
+
const { select: partialFields, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
|
|
739
2280
|
if (error) return error;
|
|
740
2281
|
select = partialFields ? {
|
|
741
2282
|
[relationship]: {
|
|
@@ -887,8 +2428,12 @@ ${err.stack}` : "Unknown error");
|
|
|
887
2428
|
}
|
|
888
2429
|
if (limit === Infinity) {
|
|
889
2430
|
const entities = await client[type].findMany(args);
|
|
2431
|
+
const mappedType = this.mapModelName(type);
|
|
890
2432
|
const body = await this.serializeItems(type, entities, {
|
|
891
|
-
include
|
|
2433
|
+
include,
|
|
2434
|
+
linkers: {
|
|
2435
|
+
document: new import_ts_japi.default.Linker(() => this.makeLinkUrl(`/${mappedType}`))
|
|
2436
|
+
}
|
|
892
2437
|
});
|
|
893
2438
|
const total = entities.length;
|
|
894
2439
|
body.meta = this.addTotalCountToMeta(body.meta, total);
|
|
@@ -910,6 +2455,7 @@ ${err.stack}` : "Unknown error");
|
|
|
910
2455
|
const options = {
|
|
911
2456
|
include,
|
|
912
2457
|
linkers: {
|
|
2458
|
+
document: new import_ts_japi.default.Linker(() => this.makeLinkUrl(`/${mappedType}`)),
|
|
913
2459
|
paginator: this.makePaginator(url, offset, limit, total)
|
|
914
2460
|
}
|
|
915
2461
|
};
|
|
@@ -921,6 +2467,278 @@ ${err.stack}` : "Unknown error");
|
|
|
921
2467
|
};
|
|
922
2468
|
}
|
|
923
2469
|
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Builds link URL for a nested resource using parent type, parent ID, relation name, and optional child ID.
|
|
2472
|
+
* Uses the parent model name mapping for the parent segment; the relation name is used as-is.
|
|
2473
|
+
*/
|
|
2474
|
+
makeNestedLinkUrl(parentType, parentId, parentRelation, childId) {
|
|
2475
|
+
const mappedParentType = this.mapModelName(parentType);
|
|
2476
|
+
const base = `/${mappedParentType}/${parentId}/${parentRelation}`;
|
|
2477
|
+
return childId ? `${base}/${childId}` : base;
|
|
2478
|
+
}
|
|
2479
|
+
async processNestedSingleRead(client, parentType, parentId, parentRelation, childId, query) {
|
|
2480
|
+
const resolved = this.resolveNestedRelation(parentType, parentRelation);
|
|
2481
|
+
if (!resolved) {
|
|
2482
|
+
return this.makeError("invalidPath");
|
|
2483
|
+
}
|
|
2484
|
+
const { filter: nestedFilter, error: nestedError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
|
|
2485
|
+
if (nestedError) return nestedError;
|
|
2486
|
+
const childType = resolved.childType;
|
|
2487
|
+
const typeInfo = this.getModelInfo(childType);
|
|
2488
|
+
const { args, include, error } = this.buildSingleReadArgs(childType, query);
|
|
2489
|
+
if (error) return error;
|
|
2490
|
+
args.where = this.mergeFilters(this.makeIdFilter(typeInfo.idFields, childId), nestedFilter);
|
|
2491
|
+
const entity = await client[childType].findFirst(args);
|
|
2492
|
+
if (!entity) return this.makeError("notFound");
|
|
2493
|
+
const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
|
|
2494
|
+
const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
|
|
2495
|
+
return {
|
|
2496
|
+
status: 200,
|
|
2497
|
+
body: await this.serializeItems(childType, entity, {
|
|
2498
|
+
include,
|
|
2499
|
+
linkers: {
|
|
2500
|
+
document: nestedLinker,
|
|
2501
|
+
resource: nestedLinker
|
|
2502
|
+
}
|
|
2503
|
+
})
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
async processNestedCreate(client, parentType, parentId, parentRelation, _query, requestBody) {
|
|
2507
|
+
const resolved = this.resolveNestedRelation(parentType, parentRelation);
|
|
2508
|
+
if (!resolved) {
|
|
2509
|
+
return this.makeError("invalidPath");
|
|
2510
|
+
}
|
|
2511
|
+
const parentInfo = this.getModelInfo(parentType);
|
|
2512
|
+
const childType = resolved.childType;
|
|
2513
|
+
const childInfo = this.getModelInfo(childType);
|
|
2514
|
+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
|
|
2515
|
+
if (error) return error;
|
|
2516
|
+
const createData = {
|
|
2517
|
+
...attributes
|
|
2518
|
+
};
|
|
2519
|
+
if (relationships) {
|
|
2520
|
+
for (const [key, data] of Object.entries(relationships)) {
|
|
2521
|
+
if (!data?.data) {
|
|
2522
|
+
return this.makeError("invalidRelationData");
|
|
2523
|
+
}
|
|
2524
|
+
if (key === resolved.reverseRelation) {
|
|
2525
|
+
return this.makeError("invalidPayload", `Relation "${key}" is controlled by the parent route and cannot be set in the request payload`);
|
|
2526
|
+
}
|
|
2527
|
+
const relationInfo = childInfo.relationships[key];
|
|
2528
|
+
if (!relationInfo) {
|
|
2529
|
+
return this.makeUnsupportedRelationshipError(childType, key, 400);
|
|
2530
|
+
}
|
|
2531
|
+
if (relationInfo.isCollection) {
|
|
2532
|
+
createData[key] = {
|
|
2533
|
+
connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
2534
|
+
};
|
|
2535
|
+
} else {
|
|
2536
|
+
if (typeof data.data !== "object") {
|
|
2537
|
+
return this.makeError("invalidRelationData");
|
|
2538
|
+
}
|
|
2539
|
+
createData[key] = {
|
|
2540
|
+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id)
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const parentFkFields = Object.values(childInfo.fields).filter((f) => f.foreignKeyFor?.includes(resolved.reverseRelation));
|
|
2546
|
+
if (parentFkFields.some((f) => Object.prototype.hasOwnProperty.call(createData, f.name))) {
|
|
2547
|
+
return this.makeError("invalidPayload", `Relation "${resolved.reverseRelation}" is controlled by the parent route and cannot be set in the request payload`);
|
|
2548
|
+
}
|
|
2549
|
+
await client[parentType].update({
|
|
2550
|
+
where: this.makeIdFilter(parentInfo.idFields, parentId),
|
|
2551
|
+
data: {
|
|
2552
|
+
[parentRelation]: {
|
|
2553
|
+
create: createData
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
const { filter: nestedFilter, error: filterError } = this.buildNestedParentFilter(parentType, parentId, parentRelation);
|
|
2558
|
+
if (filterError) return filterError;
|
|
2559
|
+
const fetchArgs = {
|
|
2560
|
+
where: nestedFilter
|
|
2561
|
+
};
|
|
2562
|
+
this.includeRelationshipIds(childType, fetchArgs, "include");
|
|
2563
|
+
if (childInfo.idFields[0]) {
|
|
2564
|
+
fetchArgs.orderBy = {
|
|
2565
|
+
[childInfo.idFields[0].name]: "desc"
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const entity = await client[childType].findFirst(fetchArgs);
|
|
2569
|
+
if (!entity) return this.makeError("notFound");
|
|
2570
|
+
const collectionPath = this.makeNestedLinkUrl(parentType, parentId, parentRelation);
|
|
2571
|
+
const resourceLinker = new import_ts_japi.default.Linker((item) => this.makeLinkUrl(`${collectionPath}/${this.getId(childInfo.name, item)}`));
|
|
2572
|
+
return {
|
|
2573
|
+
status: 201,
|
|
2574
|
+
body: await this.serializeItems(childType, entity, {
|
|
2575
|
+
linkers: {
|
|
2576
|
+
document: resourceLinker,
|
|
2577
|
+
resource: resourceLinker
|
|
2578
|
+
}
|
|
2579
|
+
})
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Builds the ORM `data` payload for a nested update, shared by both to-many (childId present)
|
|
2584
|
+
* and to-one (childId absent) variants. Returns either `{ updateData }` or `{ error }`.
|
|
2585
|
+
*/
|
|
2586
|
+
buildNestedUpdatePayload(childType, typeInfo, rev, requestBody) {
|
|
2587
|
+
const { attributes, relationships, error } = this.processRequestBody(requestBody);
|
|
2588
|
+
if (error) return {
|
|
2589
|
+
error
|
|
2590
|
+
};
|
|
2591
|
+
const updateData = {
|
|
2592
|
+
...attributes
|
|
2593
|
+
};
|
|
2594
|
+
if (relationships && Object.prototype.hasOwnProperty.call(relationships, rev)) {
|
|
2595
|
+
return {
|
|
2596
|
+
error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
const fkFields = Object.values(typeInfo.fields).filter((f) => f.foreignKeyFor?.includes(rev));
|
|
2600
|
+
if (fkFields.some((f) => Object.prototype.hasOwnProperty.call(updateData, f.name))) {
|
|
2601
|
+
return {
|
|
2602
|
+
error: this.makeError("invalidPayload", `Relation "${rev}" cannot be changed via a nested route`)
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
if (relationships) {
|
|
2606
|
+
for (const [key, data] of Object.entries(relationships)) {
|
|
2607
|
+
if (!data?.data) {
|
|
2608
|
+
return {
|
|
2609
|
+
error: this.makeError("invalidRelationData")
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
const relationInfo = typeInfo.relationships[key];
|
|
2613
|
+
if (!relationInfo) {
|
|
2614
|
+
return {
|
|
2615
|
+
error: this.makeUnsupportedRelationshipError(childType, key, 400)
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
if (relationInfo.isCollection) {
|
|
2619
|
+
updateData[key] = {
|
|
2620
|
+
set: (0, import_common_helpers3.enumerate)(data.data).map((item) => ({
|
|
2621
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: item.id
|
|
2622
|
+
}))
|
|
2623
|
+
};
|
|
2624
|
+
} else {
|
|
2625
|
+
if (typeof data.data !== "object") {
|
|
2626
|
+
return {
|
|
2627
|
+
error: this.makeError("invalidRelationData")
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
updateData[key] = {
|
|
2631
|
+
connect: {
|
|
2632
|
+
[this.makeDefaultIdKey(relationInfo.idFields)]: data.data.id
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
updateData
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Handles PATCH /:type/:id/:relationship/:childId (to-many) and
|
|
2644
|
+
* PATCH /:type/:id/:relationship (to-one, childId undefined).
|
|
2645
|
+
*/
|
|
2646
|
+
async processNestedUpdate(client, parentType, parentId, parentRelation, childId, _query, requestBody) {
|
|
2647
|
+
const resolved = this.resolveNestedRelation(parentType, parentRelation);
|
|
2648
|
+
if (!resolved) {
|
|
2649
|
+
return this.makeError("invalidPath");
|
|
2650
|
+
}
|
|
2651
|
+
const parentInfo = this.getModelInfo(parentType);
|
|
2652
|
+
const childType = resolved.childType;
|
|
2653
|
+
const typeInfo = this.getModelInfo(childType);
|
|
2654
|
+
const { updateData, error } = this.buildNestedUpdatePayload(childType, typeInfo, resolved.reverseRelation, requestBody);
|
|
2655
|
+
if (error) return error;
|
|
2656
|
+
if (childId) {
|
|
2657
|
+
await client[parentType].update({
|
|
2658
|
+
where: this.makeIdFilter(parentInfo.idFields, parentId),
|
|
2659
|
+
data: {
|
|
2660
|
+
[parentRelation]: {
|
|
2661
|
+
update: {
|
|
2662
|
+
where: this.makeIdFilter(typeInfo.idFields, childId),
|
|
2663
|
+
data: updateData
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
const fetchArgs = {
|
|
2669
|
+
where: this.makeIdFilter(typeInfo.idFields, childId)
|
|
2670
|
+
};
|
|
2671
|
+
this.includeRelationshipIds(childType, fetchArgs, "include");
|
|
2672
|
+
const entity = await client[childType].findUnique(fetchArgs);
|
|
2673
|
+
if (!entity) return this.makeError("notFound");
|
|
2674
|
+
const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation, childId));
|
|
2675
|
+
const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
|
|
2676
|
+
return {
|
|
2677
|
+
status: 200,
|
|
2678
|
+
body: await this.serializeItems(childType, entity, {
|
|
2679
|
+
linkers: {
|
|
2680
|
+
document: nestedLinker,
|
|
2681
|
+
resource: nestedLinker
|
|
2682
|
+
}
|
|
2683
|
+
})
|
|
2684
|
+
};
|
|
2685
|
+
} else {
|
|
2686
|
+
await client[parentType].update({
|
|
2687
|
+
where: this.makeIdFilter(parentInfo.idFields, parentId),
|
|
2688
|
+
data: {
|
|
2689
|
+
[parentRelation]: {
|
|
2690
|
+
update: updateData
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
const childIncludeArgs = {};
|
|
2695
|
+
this.includeRelationshipIds(childType, childIncludeArgs, "include");
|
|
2696
|
+
const fetchArgs = {
|
|
2697
|
+
where: this.makeIdFilter(parentInfo.idFields, parentId),
|
|
2698
|
+
select: {
|
|
2699
|
+
[parentRelation]: childIncludeArgs.include ? {
|
|
2700
|
+
include: childIncludeArgs.include
|
|
2701
|
+
} : true
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
const parent = await client[parentType].findUnique(fetchArgs);
|
|
2705
|
+
const entity = parent?.[parentRelation];
|
|
2706
|
+
if (!entity) return this.makeError("notFound");
|
|
2707
|
+
const linkUrl = this.makeLinkUrl(this.makeNestedLinkUrl(parentType, parentId, parentRelation));
|
|
2708
|
+
const nestedLinker = new import_ts_japi.default.Linker(() => linkUrl);
|
|
2709
|
+
return {
|
|
2710
|
+
status: 200,
|
|
2711
|
+
body: await this.serializeItems(childType, entity, {
|
|
2712
|
+
linkers: {
|
|
2713
|
+
document: nestedLinker,
|
|
2714
|
+
resource: nestedLinker
|
|
2715
|
+
}
|
|
2716
|
+
})
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
async processNestedDelete(client, parentType, parentId, parentRelation, childId) {
|
|
2721
|
+
const resolved = this.resolveNestedRelation(parentType, parentRelation);
|
|
2722
|
+
if (!resolved) {
|
|
2723
|
+
return this.makeError("invalidPath");
|
|
2724
|
+
}
|
|
2725
|
+
const parentInfo = this.getModelInfo(parentType);
|
|
2726
|
+
const typeInfo = this.getModelInfo(resolved.childType);
|
|
2727
|
+
await client[parentType].update({
|
|
2728
|
+
where: this.makeIdFilter(parentInfo.idFields, parentId),
|
|
2729
|
+
data: {
|
|
2730
|
+
[parentRelation]: {
|
|
2731
|
+
delete: this.makeIdFilter(typeInfo.idFields, childId)
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
return {
|
|
2736
|
+
status: 200,
|
|
2737
|
+
body: {
|
|
2738
|
+
meta: {}
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
924
2742
|
buildPartialSelect(type, query) {
|
|
925
2743
|
const selectFieldsQuery = query?.[`fields[${type}]`];
|
|
926
2744
|
if (!selectFieldsQuery) {
|
|
@@ -1030,7 +2848,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1030
2848
|
}
|
|
1031
2849
|
if (relationInfo.isCollection) {
|
|
1032
2850
|
createPayload.data[key] = {
|
|
1033
|
-
connect: (0,
|
|
2851
|
+
connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1034
2852
|
};
|
|
1035
2853
|
} else {
|
|
1036
2854
|
if (typeof data.data !== "object") {
|
|
@@ -1096,10 +2914,10 @@ ${err.stack}` : "Unknown error");
|
|
|
1096
2914
|
}
|
|
1097
2915
|
if (relationInfo.isCollection) {
|
|
1098
2916
|
upsertPayload.create[key] = {
|
|
1099
|
-
connect: (0,
|
|
2917
|
+
connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1100
2918
|
};
|
|
1101
2919
|
upsertPayload.update[key] = {
|
|
1102
|
-
set: (0,
|
|
2920
|
+
set: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
|
|
1103
2921
|
};
|
|
1104
2922
|
} else {
|
|
1105
2923
|
if (typeof data.data !== "object") {
|
|
@@ -1180,7 +2998,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1180
2998
|
const relationVerb = mode === "create" ? "connect" : mode === "delete" ? "disconnect" : "set";
|
|
1181
2999
|
updateArgs.data = {
|
|
1182
3000
|
[relationship]: {
|
|
1183
|
-
[relationVerb]: (0,
|
|
3001
|
+
[relationVerb]: (0, import_common_helpers3.enumerate)(parsed.data.data).map((item) => this.makeIdFilter(relationInfo.idFields, item.id))
|
|
1184
3002
|
}
|
|
1185
3003
|
};
|
|
1186
3004
|
}
|
|
@@ -1223,7 +3041,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1223
3041
|
}
|
|
1224
3042
|
if (relationInfo.isCollection) {
|
|
1225
3043
|
updatePayload.data[key] = {
|
|
1226
|
-
set: (0,
|
|
3044
|
+
set: (0, import_common_helpers3.enumerate)(data.data).map((item) => ({
|
|
1227
3045
|
[this.makeDefaultIdKey(relationInfo.idFields)]: item.id
|
|
1228
3046
|
}))
|
|
1229
3047
|
};
|
|
@@ -1279,7 +3097,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1279
3097
|
}
|
|
1280
3098
|
getIdFields(model) {
|
|
1281
3099
|
const modelDef = this.requireModel(model);
|
|
1282
|
-
const modelLower = (0,
|
|
3100
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1283
3101
|
if (!(modelLower in this.externalIdMapping)) {
|
|
1284
3102
|
return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
|
|
1285
3103
|
}
|
|
@@ -1313,7 +3131,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1313
3131
|
log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
|
|
1314
3132
|
continue;
|
|
1315
3133
|
}
|
|
1316
|
-
const modelInfo = this.typeMap[(0,
|
|
3134
|
+
const modelInfo = this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)] = {
|
|
1317
3135
|
name: model,
|
|
1318
3136
|
idFields,
|
|
1319
3137
|
relationships: {},
|
|
@@ -1338,7 +3156,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1338
3156
|
}
|
|
1339
3157
|
}
|
|
1340
3158
|
getModelInfo(model) {
|
|
1341
|
-
return this.typeMap[(0,
|
|
3159
|
+
return this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)];
|
|
1342
3160
|
}
|
|
1343
3161
|
makeLinkUrl(path) {
|
|
1344
3162
|
return `${this.options.endpoint}${path}`;
|
|
@@ -1347,7 +3165,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1347
3165
|
const linkers = {};
|
|
1348
3166
|
for (const model of Object.keys(this.schema.models)) {
|
|
1349
3167
|
const ids = this.getIdFields(model);
|
|
1350
|
-
const modelLower = (0,
|
|
3168
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1351
3169
|
const mappedModel = this.mapModelName(modelLower);
|
|
1352
3170
|
if (ids.length < 1) {
|
|
1353
3171
|
continue;
|
|
@@ -1376,7 +3194,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1376
3194
|
this.serializers.set(modelLower, serializer);
|
|
1377
3195
|
}
|
|
1378
3196
|
for (const model of Object.keys(this.schema.models)) {
|
|
1379
|
-
const modelLower = (0,
|
|
3197
|
+
const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1380
3198
|
const serializer = this.serializers.get(modelLower);
|
|
1381
3199
|
if (!serializer) {
|
|
1382
3200
|
continue;
|
|
@@ -1387,7 +3205,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1387
3205
|
if (!fieldDef.relation) {
|
|
1388
3206
|
continue;
|
|
1389
3207
|
}
|
|
1390
|
-
const fieldSerializer = this.serializers.get((0,
|
|
3208
|
+
const fieldSerializer = this.serializers.get((0, import_common_helpers3.lowerCaseFirst)(fieldDef.type));
|
|
1391
3209
|
if (!fieldSerializer) {
|
|
1392
3210
|
continue;
|
|
1393
3211
|
}
|
|
@@ -1421,12 +3239,12 @@ ${err.stack}` : "Unknown error");
|
|
|
1421
3239
|
}
|
|
1422
3240
|
}
|
|
1423
3241
|
async serializeItems(model, items, options) {
|
|
1424
|
-
model = (0,
|
|
3242
|
+
model = (0, import_common_helpers3.lowerCaseFirst)(model);
|
|
1425
3243
|
const serializer = this.serializers.get(model);
|
|
1426
3244
|
if (!serializer) {
|
|
1427
3245
|
throw new Error(`serializer not found for model ${model}`);
|
|
1428
3246
|
}
|
|
1429
|
-
const itemsWithId = (0,
|
|
3247
|
+
const itemsWithId = (0, import_common_helpers3.clone)(items);
|
|
1430
3248
|
this.injectCompoundId(model, itemsWithId);
|
|
1431
3249
|
const serialized = await serializer.serialize(itemsWithId, options);
|
|
1432
3250
|
const plainResult = this.toPlainObject(serialized);
|
|
@@ -1445,7 +3263,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1445
3263
|
if (!typeInfo) {
|
|
1446
3264
|
return;
|
|
1447
3265
|
}
|
|
1448
|
-
(0,
|
|
3266
|
+
(0, import_common_helpers3.enumerate)(items).forEach((item) => {
|
|
1449
3267
|
if (!item) {
|
|
1450
3268
|
return;
|
|
1451
3269
|
}
|
|
@@ -1620,7 +3438,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1620
3438
|
const url = new URL(this.makeLinkUrl(path));
|
|
1621
3439
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
1622
3440
|
if (key.startsWith("filter[") || key.startsWith("sort[") || key === "include" || key.startsWith("include[") || key.startsWith("fields[")) {
|
|
1623
|
-
for (const v of (0,
|
|
3441
|
+
for (const v of (0, import_common_helpers3.enumerate)(value)) {
|
|
1624
3442
|
url.searchParams.append(key, v);
|
|
1625
3443
|
}
|
|
1626
3444
|
}
|
|
@@ -1692,7 +3510,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1692
3510
|
const item = {};
|
|
1693
3511
|
let curr = item;
|
|
1694
3512
|
let currType = typeInfo;
|
|
1695
|
-
for (const filterValue of (0,
|
|
3513
|
+
for (const filterValue of (0, import_common_helpers3.enumerate)(value)) {
|
|
1696
3514
|
for (let i = 0; i < filterKeys.length; i++) {
|
|
1697
3515
|
let filterKey = filterKeys[i];
|
|
1698
3516
|
let filterOp;
|
|
@@ -1771,7 +3589,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1771
3589
|
};
|
|
1772
3590
|
}
|
|
1773
3591
|
const result = [];
|
|
1774
|
-
for (const sortSpec of (0,
|
|
3592
|
+
for (const sortSpec of (0, import_common_helpers3.enumerate)(query["sort"])) {
|
|
1775
3593
|
const sortFields = sortSpec.split(",").filter((i) => i);
|
|
1776
3594
|
for (const sortField of sortFields) {
|
|
1777
3595
|
const dir = sortField.startsWith("-") ? "desc" : "asc";
|
|
@@ -1840,7 +3658,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1840
3658
|
}
|
|
1841
3659
|
const result = {};
|
|
1842
3660
|
const allIncludes = [];
|
|
1843
|
-
for (const includeItem of (0,
|
|
3661
|
+
for (const includeItem of (0, import_common_helpers3.enumerate)(include)) {
|
|
1844
3662
|
const inclusions = includeItem.split(",").filter((i) => i);
|
|
1845
3663
|
for (const inclusion of inclusions) {
|
|
1846
3664
|
allIncludes.push(inclusion);
|
|
@@ -1863,7 +3681,7 @@ ${err.stack}` : "Unknown error");
|
|
|
1863
3681
|
error: this.makeUnsupportedModelError(relationInfo.type)
|
|
1864
3682
|
};
|
|
1865
3683
|
}
|
|
1866
|
-
const { select, error } = this.buildPartialSelect((0,
|
|
3684
|
+
const { select, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
|
|
1867
3685
|
if (error) return {
|
|
1868
3686
|
select: void 0,
|
|
1869
3687
|
error
|
|
@@ -2041,7 +3859,7 @@ ${err.stack}` : "Unknown error");
|
|
|
2041
3859
|
status = status ?? this.errors[code]?.status ?? 500;
|
|
2042
3860
|
const error = {
|
|
2043
3861
|
status,
|
|
2044
|
-
code: (0,
|
|
3862
|
+
code: (0, import_common_helpers3.paramCase)(code),
|
|
2045
3863
|
title: this.errors[code]?.title
|
|
2046
3864
|
};
|
|
2047
3865
|
if (detail) {
|
|
@@ -2063,10 +3881,15 @@ ${err.stack}` : "Unknown error");
|
|
|
2063
3881
|
makeUnsupportedRelationshipError(model, relationship, status) {
|
|
2064
3882
|
return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
|
|
2065
3883
|
}
|
|
3884
|
+
//#endregion
|
|
3885
|
+
async generateSpec(options) {
|
|
3886
|
+
const generator = new RestApiSpecGenerator(this.options);
|
|
3887
|
+
return generator.generateSpec(options);
|
|
3888
|
+
}
|
|
2066
3889
|
};
|
|
2067
3890
|
|
|
2068
3891
|
// src/api/rpc/index.ts
|
|
2069
|
-
var
|
|
3892
|
+
var import_common_helpers4 = require("@zenstackhq/common-helpers");
|
|
2070
3893
|
var import_orm3 = require("@zenstackhq/orm");
|
|
2071
3894
|
var import_superjson4 = __toESM(require("superjson"), 1);
|
|
2072
3895
|
var import_ts_pattern3 = require("ts-pattern");
|
|
@@ -2087,7 +3910,8 @@ var RPCApiHandler = class {
|
|
|
2087
3910
|
validateOptions(options) {
|
|
2088
3911
|
const schema = import_zod3.default.strictObject({
|
|
2089
3912
|
schema: import_zod3.default.object(),
|
|
2090
|
-
log: loggerSchema.optional()
|
|
3913
|
+
log: loggerSchema.optional(),
|
|
3914
|
+
queryOptions: queryOptionsSchema.optional()
|
|
2091
3915
|
});
|
|
2092
3916
|
const parseResult = schema.safeParse(options);
|
|
2093
3917
|
if (!parseResult.success) {
|
|
@@ -2124,7 +3948,7 @@ var RPCApiHandler = class {
|
|
|
2124
3948
|
requestBody
|
|
2125
3949
|
});
|
|
2126
3950
|
}
|
|
2127
|
-
model = (0,
|
|
3951
|
+
model = (0, import_common_helpers4.lowerCaseFirst)(model);
|
|
2128
3952
|
method = method.toUpperCase();
|
|
2129
3953
|
let args;
|
|
2130
3954
|
let resCode = 200;
|
|
@@ -2191,7 +4015,7 @@ var RPCApiHandler = class {
|
|
|
2191
4015
|
if (!this.isValidModel(client, model)) {
|
|
2192
4016
|
return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
|
|
2193
4017
|
}
|
|
2194
|
-
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0,
|
|
4018
|
+
log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0, import_common_helpers4.safeJSONStringify)(processedArgs)}`);
|
|
2195
4019
|
const clientResult = await client[model][op](processedArgs);
|
|
2196
4020
|
let responseBody = {
|
|
2197
4021
|
data: clientResult
|
|
@@ -2211,7 +4035,7 @@ var RPCApiHandler = class {
|
|
|
2211
4035
|
status: resCode,
|
|
2212
4036
|
body: responseBody
|
|
2213
4037
|
};
|
|
2214
|
-
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0,
|
|
4038
|
+
log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2215
4039
|
return response;
|
|
2216
4040
|
} catch (err) {
|
|
2217
4041
|
log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
|
|
@@ -2248,7 +4072,7 @@ var RPCApiHandler = class {
|
|
|
2248
4072
|
if (!VALID_OPS.has(itemOp)) {
|
|
2249
4073
|
return this.makeBadInputErrorResponse(`operation at index ${i} has invalid op: ${itemOp}`);
|
|
2250
4074
|
}
|
|
2251
|
-
if (!this.isValidModel(client, (0,
|
|
4075
|
+
if (!this.isValidModel(client, (0, import_common_helpers4.lowerCaseFirst)(itemModel))) {
|
|
2252
4076
|
return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`);
|
|
2253
4077
|
}
|
|
2254
4078
|
if (itemArgs !== void 0 && itemArgs !== null && (typeof itemArgs !== "object" || Array.isArray(itemArgs))) {
|
|
@@ -2259,7 +4083,7 @@ var RPCApiHandler = class {
|
|
|
2259
4083
|
return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
|
|
2260
4084
|
}
|
|
2261
4085
|
processedOps.push({
|
|
2262
|
-
model: (0,
|
|
4086
|
+
model: (0, import_common_helpers4.lowerCaseFirst)(itemModel),
|
|
2263
4087
|
op: itemOp,
|
|
2264
4088
|
args: processedArgs
|
|
2265
4089
|
});
|
|
@@ -2283,7 +4107,7 @@ var RPCApiHandler = class {
|
|
|
2283
4107
|
status: 200,
|
|
2284
4108
|
body: responseBody
|
|
2285
4109
|
};
|
|
2286
|
-
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0,
|
|
4110
|
+
log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2287
4111
|
return response;
|
|
2288
4112
|
} catch (err) {
|
|
2289
4113
|
log(this.options.log, "error", `error occurred when handling "$transaction" request`, err);
|
|
@@ -2345,7 +4169,7 @@ var RPCApiHandler = class {
|
|
|
2345
4169
|
status: 200,
|
|
2346
4170
|
body: responseBody
|
|
2347
4171
|
};
|
|
2348
|
-
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0,
|
|
4172
|
+
log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
|
|
2349
4173
|
return response;
|
|
2350
4174
|
} catch (err) {
|
|
2351
4175
|
log(this.options.log, "error", `error occurred when handling "$procs.${proc}" request`, err);
|
|
@@ -2356,7 +4180,7 @@ var RPCApiHandler = class {
|
|
|
2356
4180
|
}
|
|
2357
4181
|
}
|
|
2358
4182
|
isValidModel(client, model) {
|
|
2359
|
-
return Object.keys(client.$schema.models).some((m) => (0,
|
|
4183
|
+
return Object.keys(client.$schema.models).some((m) => (0, import_common_helpers4.lowerCaseFirst)(m) === (0, import_common_helpers4.lowerCaseFirst)(model));
|
|
2360
4184
|
}
|
|
2361
4185
|
makeBadInputErrorResponse(message) {
|
|
2362
4186
|
const resp = {
|
|
@@ -2367,7 +4191,7 @@ var RPCApiHandler = class {
|
|
|
2367
4191
|
}
|
|
2368
4192
|
}
|
|
2369
4193
|
};
|
|
2370
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
4194
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
|
|
2371
4195
|
return resp;
|
|
2372
4196
|
}
|
|
2373
4197
|
makeGenericErrorResponse(err) {
|
|
@@ -2379,7 +4203,7 @@ var RPCApiHandler = class {
|
|
|
2379
4203
|
}
|
|
2380
4204
|
}
|
|
2381
4205
|
};
|
|
2382
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
4206
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
|
|
2383
4207
|
return resp;
|
|
2384
4208
|
}
|
|
2385
4209
|
makeORMErrorResponse(err) {
|
|
@@ -2411,7 +4235,7 @@ var RPCApiHandler = class {
|
|
|
2411
4235
|
error
|
|
2412
4236
|
}
|
|
2413
4237
|
};
|
|
2414
|
-
log(this.options.log, "debug", () => `sending error response: ${(0,
|
|
4238
|
+
log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
|
|
2415
4239
|
return resp;
|
|
2416
4240
|
}
|
|
2417
4241
|
async processRequestPayload(args) {
|