lumen-framework 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +70 -38
- package/dist/cli.cjs.map +4 -4
- package/dist/index.js +928 -259
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +919 -250
- package/dist/index.mjs.map +4 -4
- package/dist/types/errors/namespaced-serializer-missing-error.d.ts +15 -0
- package/dist/types/packages/application/utils/validate-namespaced-serializers.d.ts +15 -0
- package/dist/types/packages/controller/errors/index.d.ts +1 -0
- package/dist/types/packages/controller/errors/related-record-not-found-error.d.ts +14 -0
- package/dist/types/packages/controller/index.d.ts +65 -0
- package/dist/types/packages/controller/utils/find-many.d.ts +2 -1
- package/dist/types/packages/controller/utils/find-one.d.ts +2 -1
- package/dist/types/packages/controller/utils/params-to-query.d.ts +7 -2
- package/dist/types/packages/controller/utils/validate-relationships.d.ts +8 -0
- package/dist/types/packages/database/constants.d.ts +1 -0
- package/dist/types/packages/database/model/index.d.ts +11 -0
- package/dist/types/packages/database/model/utils/process-write-error.d.ts +9 -1
- package/dist/types/packages/database/validation/errors/validation-error.d.ts +11 -2
- package/dist/types/packages/jsonapi/interfaces.d.ts +1 -1
- package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +13 -0
- package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +14 -0
- package/dist/types/packages/router/route/params/errors/index.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +2 -0
- package/dist/types/packages/router/route/params/index.d.ts +1 -0
- package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +12 -0
- package/dist/types/packages/router/route/params/parameter/index.d.ts +8 -0
- package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +8 -0
- package/dist/types/packages/serializer/index.d.ts +111 -36
- package/dist/types/packages/serializer/utils/include-tree.d.ts +29 -0
- package/dist/types/packages/serializer/utils/load-linkage.d.ts +22 -0
- package/dist/types/packages/server/index.d.ts +2 -1
- package/dist/types/packages/server/interfaces.d.ts +5 -0
- package/dist/types/packages/server/utils/source-for.d.ts +10 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
var import_chalk4 = require("chalk");
|
|
44
44
|
|
|
45
45
|
// src/packages/database/constants.ts
|
|
46
|
+
var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
|
|
47
|
+
var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
|
|
46
48
|
var VALID_DRIVERS = [
|
|
47
49
|
"pg",
|
|
48
50
|
"sqlite3",
|
|
@@ -892,7 +894,7 @@ function underscore(source = "", upper = false) {
|
|
|
892
894
|
var normalize_model_name_default = compose(import_inflection2.singularize, import_inflection2.dasherize, underscore);
|
|
893
895
|
|
|
894
896
|
// src/packages/database/model/index.ts
|
|
895
|
-
var
|
|
897
|
+
var import_inflection10 = require("inflection");
|
|
896
898
|
|
|
897
899
|
// src/packages/database/query/index.ts
|
|
898
900
|
var import_inflection5 = require("inflection");
|
|
@@ -1090,10 +1092,16 @@ async function buildResults({
|
|
|
1090
1092
|
}
|
|
1091
1093
|
);
|
|
1092
1094
|
}
|
|
1095
|
+
const missing = new Set(
|
|
1096
|
+
entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
|
|
1097
|
+
);
|
|
1098
|
+
const absent = Array.from(missing).filter(
|
|
1099
|
+
(name) => model.relationshipFor(name)?.type === "hasOne"
|
|
1100
|
+
);
|
|
1093
1101
|
const instance = Reflect.construct(model, [
|
|
1094
1102
|
entries(record).reduce((r, entry) => {
|
|
1095
1103
|
let [key, value] = entry;
|
|
1096
|
-
if (
|
|
1104
|
+
if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
|
|
1097
1105
|
return r;
|
|
1098
1106
|
} else if (key.indexOf(".") >= 0) {
|
|
1099
1107
|
const [a, b] = key.split(".");
|
|
@@ -1113,6 +1121,7 @@ async function buildResults({
|
|
|
1113
1121
|
};
|
|
1114
1122
|
}, {})
|
|
1115
1123
|
]);
|
|
1124
|
+
absent.forEach((name) => instance.absentRelationships.add(name));
|
|
1116
1125
|
instance.currentChangeSet.persist();
|
|
1117
1126
|
return instance;
|
|
1118
1127
|
});
|
|
@@ -1124,6 +1133,17 @@ function createRunner(target, opts) {
|
|
|
1124
1133
|
const { resolve: resolve2, reject } = opts;
|
|
1125
1134
|
let didRun = false;
|
|
1126
1135
|
RUNNERS.set(target, async () => {
|
|
1136
|
+
if (didRun) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
didRun = true;
|
|
1140
|
+
try {
|
|
1141
|
+
await run();
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
reject(err);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
async function run() {
|
|
1127
1147
|
let results;
|
|
1128
1148
|
const {
|
|
1129
1149
|
model,
|
|
@@ -1133,10 +1153,6 @@ function createRunner(target, opts) {
|
|
|
1133
1153
|
shouldCount,
|
|
1134
1154
|
relationships
|
|
1135
1155
|
} = target;
|
|
1136
|
-
if (didRun) {
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
didRun = true;
|
|
1140
1156
|
if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
|
|
1141
1157
|
target.select(...target.model.attributeNames);
|
|
1142
1158
|
}
|
|
@@ -1177,7 +1193,7 @@ function createRunner(target, opts) {
|
|
|
1177
1193
|
resolve2(result);
|
|
1178
1194
|
}
|
|
1179
1195
|
}
|
|
1180
|
-
}
|
|
1196
|
+
}
|
|
1181
1197
|
}
|
|
1182
1198
|
}
|
|
1183
1199
|
function runQuery(target) {
|
|
@@ -1188,6 +1204,17 @@ function runQuery(target) {
|
|
|
1188
1204
|
}
|
|
1189
1205
|
|
|
1190
1206
|
// src/packages/database/query/index.ts
|
|
1207
|
+
var CONDITIONS = /* @__PURE__ */ new Set([
|
|
1208
|
+
"where",
|
|
1209
|
+
"whereNot",
|
|
1210
|
+
"whereIn",
|
|
1211
|
+
"whereNotIn",
|
|
1212
|
+
"whereNull",
|
|
1213
|
+
"whereNotNull",
|
|
1214
|
+
"whereBetween",
|
|
1215
|
+
"whereNotBetween",
|
|
1216
|
+
"whereRaw"
|
|
1217
|
+
]);
|
|
1191
1218
|
var Query = class extends Promise {
|
|
1192
1219
|
constructor(model) {
|
|
1193
1220
|
let resolve2;
|
|
@@ -1380,12 +1407,11 @@ var Query = class extends Promise {
|
|
|
1380
1407
|
return this;
|
|
1381
1408
|
}
|
|
1382
1409
|
count() {
|
|
1383
|
-
const validName = /^(where(((Not)?(In)?)|(Raw)|(Between)))$/g;
|
|
1384
1410
|
Object.assign(this, {
|
|
1385
1411
|
shouldCount: true,
|
|
1386
1412
|
snapshots: [
|
|
1387
1413
|
["count", "* as countAll"],
|
|
1388
|
-
...this.snapshots.filter(([name]) =>
|
|
1414
|
+
...this.snapshots.filter(([name]) => CONDITIONS.has(name))
|
|
1389
1415
|
]
|
|
1390
1416
|
});
|
|
1391
1417
|
return this;
|
|
@@ -1882,7 +1908,9 @@ async function get(owner, key) {
|
|
|
1882
1908
|
let { foreignKey } = opts;
|
|
1883
1909
|
value = owner.currentChangeSet.get(key);
|
|
1884
1910
|
foreignKey = (0, import_inflection7.camelize)(foreignKey, true);
|
|
1885
|
-
if (!value) {
|
|
1911
|
+
if (!value && owner.absentRelationships.has(key)) {
|
|
1912
|
+
value = null;
|
|
1913
|
+
} else if (!value) {
|
|
1886
1914
|
switch (type) {
|
|
1887
1915
|
case "hasOne":
|
|
1888
1916
|
value = await getHasOne(owner, {
|
|
@@ -2544,13 +2572,37 @@ async function initializeClass({
|
|
|
2544
2572
|
return model;
|
|
2545
2573
|
}
|
|
2546
2574
|
|
|
2575
|
+
// src/packages/server/utils/source-for.ts
|
|
2576
|
+
var import_inflection9 = require("inflection");
|
|
2577
|
+
function memberName(key) {
|
|
2578
|
+
return (0, import_inflection9.dasherize)(underscore(key));
|
|
2579
|
+
}
|
|
2580
|
+
function escapePointer(segment) {
|
|
2581
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2582
|
+
}
|
|
2583
|
+
function sourceFor(path) {
|
|
2584
|
+
const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
|
|
2585
|
+
if (!root) {
|
|
2586
|
+
return {};
|
|
2587
|
+
} else if (root === "data") {
|
|
2588
|
+
return {
|
|
2589
|
+
pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
return {
|
|
2593
|
+
parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2547
2597
|
// src/packages/database/validation/errors/validation-error.ts
|
|
2548
2598
|
var ValidationError = class extends Error {
|
|
2549
|
-
constructor(key
|
|
2550
|
-
super(`Validation failed for ${key}
|
|
2599
|
+
constructor(key) {
|
|
2600
|
+
super(`Validation failed for ${key}.`);
|
|
2601
|
+
this.key = key;
|
|
2602
|
+
this.source = sourceFor(`data.attributes.${key}`);
|
|
2551
2603
|
}
|
|
2552
2604
|
};
|
|
2553
|
-
var validation_error_default = ValidationError;
|
|
2605
|
+
var validation_error_default = createServerError(ValidationError, 422);
|
|
2554
2606
|
|
|
2555
2607
|
// src/packages/database/validation/index.ts
|
|
2556
2608
|
var Validation = class {
|
|
@@ -2590,12 +2642,32 @@ function validate(instance) {
|
|
|
2590
2642
|
validator: Reflect.get(instance.constructor.validates, key)
|
|
2591
2643
|
})).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
|
|
2592
2644
|
if (!validation.isValid()) {
|
|
2593
|
-
throw new validation_error_default(validation.key
|
|
2645
|
+
throw new validation_error_default(validation.key);
|
|
2594
2646
|
}
|
|
2595
2647
|
return result;
|
|
2596
2648
|
}, true);
|
|
2597
2649
|
}
|
|
2598
2650
|
|
|
2651
|
+
// src/packages/database/model/utils/process-write-error.ts
|
|
2652
|
+
function driverMessage(message) {
|
|
2653
|
+
const index = message.lastIndexOf(" - ");
|
|
2654
|
+
return index >= 0 ? message.slice(index + 3) : message;
|
|
2655
|
+
}
|
|
2656
|
+
function processWriteError(err) {
|
|
2657
|
+
if (!(err instanceof Error)) {
|
|
2658
|
+
return err;
|
|
2659
|
+
}
|
|
2660
|
+
const { code } = err;
|
|
2661
|
+
const { message } = err;
|
|
2662
|
+
if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
|
|
2663
|
+
return new unique_constraint_error_default(driverMessage(message));
|
|
2664
|
+
}
|
|
2665
|
+
return err;
|
|
2666
|
+
}
|
|
2667
|
+
function rethrowWriteError(err) {
|
|
2668
|
+
throw processWriteError(err);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2599
2671
|
// src/packages/database/model/utils/run-hooks.ts
|
|
2600
2672
|
function runHooks(record, trx, ...hooks) {
|
|
2601
2673
|
return hooks.filter((hook) => Boolean(hook)).reduce(
|
|
@@ -2635,6 +2707,12 @@ var Model = class {
|
|
|
2635
2707
|
writable: false,
|
|
2636
2708
|
enumerable: false,
|
|
2637
2709
|
configurable: false
|
|
2710
|
+
},
|
|
2711
|
+
absentRelationships: {
|
|
2712
|
+
value: /* @__PURE__ */ new Set(),
|
|
2713
|
+
writable: false,
|
|
2714
|
+
enumerable: false,
|
|
2715
|
+
configurable: false
|
|
2638
2716
|
}
|
|
2639
2717
|
});
|
|
2640
2718
|
const {
|
|
@@ -2981,9 +3059,9 @@ var Model = class {
|
|
|
2981
3059
|
return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
|
|
2982
3060
|
};
|
|
2983
3061
|
if (transaction) {
|
|
2984
|
-
return run(transaction);
|
|
3062
|
+
return run(transaction).catch(rethrowWriteError);
|
|
2985
3063
|
}
|
|
2986
|
-
return this.transaction(run);
|
|
3064
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
2987
3065
|
}
|
|
2988
3066
|
/**
|
|
2989
3067
|
* Permanently delete the instance from the database.
|
|
@@ -3103,9 +3181,9 @@ var Model = class {
|
|
|
3103
3181
|
return createTransactionResultProxy(instance, true);
|
|
3104
3182
|
};
|
|
3105
3183
|
if (transaction) {
|
|
3106
|
-
return run(transaction);
|
|
3184
|
+
return run(transaction).catch(rethrowWriteError);
|
|
3107
3185
|
}
|
|
3108
|
-
return this.transaction(run);
|
|
3186
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
3109
3187
|
}
|
|
3110
3188
|
/**
|
|
3111
3189
|
* Specify the transaction object to use for following save, update, or
|
|
@@ -3283,7 +3361,7 @@ var Model = class {
|
|
|
3283
3361
|
return Promise.resolve(this);
|
|
3284
3362
|
}
|
|
3285
3363
|
if (!this.tableName) {
|
|
3286
|
-
const getTableName = compose(
|
|
3364
|
+
const getTableName = compose(import_inflection10.pluralize, underscore);
|
|
3287
3365
|
const tableName = getTableName(this.name);
|
|
3288
3366
|
Reflect.defineProperty(this, "tableName", {
|
|
3289
3367
|
value: tableName,
|
|
@@ -3481,7 +3559,7 @@ function parseNestedObject(source) {
|
|
|
3481
3559
|
}
|
|
3482
3560
|
|
|
3483
3561
|
// src/packages/server/request/parser/utils/format.ts
|
|
3484
|
-
var
|
|
3562
|
+
var import_inflection12 = require("inflection");
|
|
3485
3563
|
|
|
3486
3564
|
// src/packages/server/request/parser/constants.ts
|
|
3487
3565
|
var INT = /^\d+$/;
|
|
@@ -3492,7 +3570,7 @@ var TRUE = /^true$/;
|
|
|
3492
3570
|
var BRACKETS = /(?:\[])/g;
|
|
3493
3571
|
|
|
3494
3572
|
// src/utils/transform-keys.ts
|
|
3495
|
-
var
|
|
3573
|
+
var import_inflection11 = require("inflection");
|
|
3496
3574
|
function transformKeys(source, transformer, deep = false) {
|
|
3497
3575
|
if (Array.isArray(source)) {
|
|
3498
3576
|
return source.slice(0);
|
|
@@ -3516,14 +3594,14 @@ function transformKeys(source, transformer, deep = false) {
|
|
|
3516
3594
|
function camelizeKeys(source, deep) {
|
|
3517
3595
|
return transformKeys(
|
|
3518
3596
|
source,
|
|
3519
|
-
(key) => (0,
|
|
3597
|
+
(key) => (0, import_inflection11.camelize)(underscore(key), true),
|
|
3520
3598
|
deep
|
|
3521
3599
|
);
|
|
3522
3600
|
}
|
|
3523
3601
|
function dasherizeKeys(source, deep) {
|
|
3524
3602
|
return transformKeys(
|
|
3525
3603
|
source,
|
|
3526
|
-
(key) => (0,
|
|
3604
|
+
(key) => (0, import_inflection11.dasherize)(underscore(key)),
|
|
3527
3605
|
deep
|
|
3528
3606
|
);
|
|
3529
3607
|
}
|
|
@@ -3538,7 +3616,7 @@ function makeArray(source) {
|
|
|
3538
3616
|
function formatString(source, method) {
|
|
3539
3617
|
if (method === "GET") {
|
|
3540
3618
|
if (source.indexOf(",") >= 0) {
|
|
3541
|
-
return source.split(",").map((str) => (0,
|
|
3619
|
+
return source.split(",").map((str) => (0, import_inflection12.camelize)(underscore(str), true));
|
|
3542
3620
|
} else if (INT.test(source)) {
|
|
3543
3621
|
return Number.parseInt(source, 10);
|
|
3544
3622
|
} else if (BOOL.test(source)) {
|
|
@@ -3565,9 +3643,9 @@ function formatObject(source, method, formatter) {
|
|
|
3565
3643
|
}
|
|
3566
3644
|
function formatSort(sort) {
|
|
3567
3645
|
if (sort.startsWith("-")) {
|
|
3568
|
-
return `-${(0,
|
|
3646
|
+
return `-${(0, import_inflection12.camelize)(underscore(sort.substr(1)), true)}`;
|
|
3569
3647
|
}
|
|
3570
|
-
return (0,
|
|
3648
|
+
return (0, import_inflection12.camelize)(underscore(sort), true);
|
|
3571
3649
|
}
|
|
3572
3650
|
function formatFields(fields) {
|
|
3573
3651
|
return entries(fields).reduce(
|
|
@@ -3832,6 +3910,10 @@ function dataFor(status, err) {
|
|
|
3832
3910
|
if (title) {
|
|
3833
3911
|
errData.title = title;
|
|
3834
3912
|
}
|
|
3913
|
+
const source = err?.source;
|
|
3914
|
+
if (source && (source.pointer || source.parameter)) {
|
|
3915
|
+
errData.source = source;
|
|
3916
|
+
}
|
|
3835
3917
|
if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
|
|
3836
3918
|
errData.detail = err.message.replace(/^\[public\]/gi, "");
|
|
3837
3919
|
}
|
|
@@ -4054,9 +4136,10 @@ function merge(dest, source) {
|
|
|
4054
4136
|
}
|
|
4055
4137
|
|
|
4056
4138
|
// src/packages/controller/utils/params-to-query.ts
|
|
4057
|
-
function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
4139
|
+
function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
|
|
4058
4140
|
const relationships = entries(model.relationships);
|
|
4059
4141
|
const includedFields = omit(fields, model.resourceName);
|
|
4142
|
+
const included = include && include.map((path) => path.split(".")[0]);
|
|
4060
4143
|
let query = {
|
|
4061
4144
|
id,
|
|
4062
4145
|
filter,
|
|
@@ -4099,9 +4182,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4099
4182
|
if (!value.includes(relationship.model.primaryKey)) {
|
|
4100
4183
|
value = [relationship.model.primaryKey, ...value];
|
|
4101
4184
|
}
|
|
4102
|
-
if (
|
|
4103
|
-
value = [...value, ...relationship.model.
|
|
4104
|
-
} else if (!
|
|
4185
|
+
if (included && value.length === 1 && included.includes(name)) {
|
|
4186
|
+
value = [...value, ...serializerFor(relationship.model).attributes];
|
|
4187
|
+
} else if (!included && value.length > 1) {
|
|
4105
4188
|
value = value.slice(0, 1);
|
|
4106
4189
|
}
|
|
4107
4190
|
return {
|
|
@@ -4116,18 +4199,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4116
4199
|
}
|
|
4117
4200
|
|
|
4118
4201
|
// src/packages/controller/utils/find-one.ts
|
|
4119
|
-
function findOne(model, req) {
|
|
4202
|
+
function findOne(model, req, serializerFor) {
|
|
4120
4203
|
const params = merge(req.defaultParams, req.params);
|
|
4121
|
-
const { id, select, include } = paramsToQuery(model, params);
|
|
4204
|
+
const { id, select, include } = paramsToQuery(model, params, serializerFor);
|
|
4122
4205
|
return model.find(id).select(...select).include(include);
|
|
4123
4206
|
}
|
|
4124
4207
|
|
|
4125
4208
|
// src/packages/controller/utils/find-many.ts
|
|
4126
|
-
function findMany(model, req) {
|
|
4209
|
+
function findMany(model, req, serializerFor) {
|
|
4127
4210
|
const params = merge(req.defaultParams, req.params);
|
|
4128
4211
|
const { sort, page, limit, select, filter, include } = paramsToQuery(
|
|
4129
4212
|
model,
|
|
4130
|
-
params
|
|
4213
|
+
params,
|
|
4214
|
+
serializerFor
|
|
4131
4215
|
);
|
|
4132
4216
|
return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
|
|
4133
4217
|
}
|
|
@@ -4157,6 +4241,53 @@ function resolveRelationships(model, relationships = {}) {
|
|
|
4157
4241
|
);
|
|
4158
4242
|
}
|
|
4159
4243
|
|
|
4244
|
+
// src/packages/controller/errors/related-record-not-found-error.ts
|
|
4245
|
+
var RelatedRecordNotFoundError = class extends Error {
|
|
4246
|
+
constructor({ name, primaryKey }, primaryKeyValue, path) {
|
|
4247
|
+
super(
|
|
4248
|
+
`Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
|
|
4249
|
+
);
|
|
4250
|
+
this.source = sourceFor(path);
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
|
|
4254
|
+
|
|
4255
|
+
// src/packages/controller/utils/validate-relationships.ts
|
|
4256
|
+
async function validateRelationships(model, relationships = {}) {
|
|
4257
|
+
await Promise.all(
|
|
4258
|
+
entries(relationships).map(async ([key, value]) => {
|
|
4259
|
+
const opts = model.relationshipFor(key);
|
|
4260
|
+
const { data = null } = value || {};
|
|
4261
|
+
if (!opts || !data) {
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
const { model: related } = opts;
|
|
4265
|
+
const isMany = Array.isArray(data);
|
|
4266
|
+
const ids = (isMany ? data : [data]).map(
|
|
4267
|
+
(item) => Reflect.get(Object(item), "id")
|
|
4268
|
+
);
|
|
4269
|
+
if (!ids.length) {
|
|
4270
|
+
return;
|
|
4271
|
+
}
|
|
4272
|
+
const column = related.columnNameFor(related.primaryKey) || "id";
|
|
4273
|
+
const rows = await related.table().whereIn(
|
|
4274
|
+
`${related.tableName}.${column}`,
|
|
4275
|
+
ids.filter((id) => id != null)
|
|
4276
|
+
).select(`${related.tableName}.${column}`);
|
|
4277
|
+
const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
|
|
4278
|
+
const missing = ids.findIndex((id) => !found.has(String(id)));
|
|
4279
|
+
if (missing >= 0) {
|
|
4280
|
+
const path = `data.relationships.${key}.data`;
|
|
4281
|
+
throw new related_record_not_found_error_default(
|
|
4282
|
+
related,
|
|
4283
|
+
ids[missing],
|
|
4284
|
+
isMany ? `${path}.${missing}` : path
|
|
4285
|
+
);
|
|
4286
|
+
}
|
|
4287
|
+
})
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4160
4291
|
// src/packages/controller/constants.ts
|
|
4161
4292
|
var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
|
|
4162
4293
|
|
|
@@ -4342,6 +4473,74 @@ var Controller = class {
|
|
|
4342
4473
|
* @public
|
|
4343
4474
|
*/
|
|
4344
4475
|
defaultPerPage = 25;
|
|
4476
|
+
/**
|
|
4477
|
+
* How many relationships deep an `?include` path may go on this
|
|
4478
|
+
* controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
|
|
4479
|
+
* only direct relationships (`comments`) can be included. Paths deeper than
|
|
4480
|
+
* this are rejected with `400 Bad Request`.
|
|
4481
|
+
*
|
|
4482
|
+
* Set it on `ApplicationController` to change it for the whole app, or on a
|
|
4483
|
+
* single controller to override it there.
|
|
4484
|
+
*
|
|
4485
|
+
* ```javascript
|
|
4486
|
+
* class ApplicationController extends Controller {
|
|
4487
|
+
* maxIncludeDepth = 2;
|
|
4488
|
+
* }
|
|
4489
|
+
* ```
|
|
4490
|
+
*
|
|
4491
|
+
* Every allowed path is enumerated up front from the serializers'
|
|
4492
|
+
* relationships, and each nested level costs its own queries per request, so
|
|
4493
|
+
* keep this small.
|
|
4494
|
+
*
|
|
4495
|
+
* @property maxIncludeDepth
|
|
4496
|
+
* @type {Number}
|
|
4497
|
+
* @default 3
|
|
4498
|
+
* @public
|
|
4499
|
+
*/
|
|
4500
|
+
maxIncludeDepth = 3;
|
|
4501
|
+
/**
|
|
4502
|
+
* Whether a namespace may fall back to the root Serializer of a type it has
|
|
4503
|
+
* no Serializer for. Read from a namespace's `ApplicationController` and
|
|
4504
|
+
* applies to the whole namespace.
|
|
4505
|
+
*
|
|
4506
|
+
* By default `app/controllers/admin/comments.js` without an
|
|
4507
|
+
* `app/serializers/admin/comments.js` — or an included type without one —
|
|
4508
|
+
* is serialized by the root Serializer, with every attribute and
|
|
4509
|
+
* relationship it declares. For a namespace that must only expose what it
|
|
4510
|
+
* declares itself, turn the fallback off:
|
|
4511
|
+
*
|
|
4512
|
+
* ```javascript
|
|
4513
|
+
* // app/controllers/admin/application.js
|
|
4514
|
+
* class AdminApplicationController extends ApplicationController {
|
|
4515
|
+
* serializerFallback = false;
|
|
4516
|
+
* }
|
|
4517
|
+
* ```
|
|
4518
|
+
*
|
|
4519
|
+
* The application then refuses to boot while any type the namespace can
|
|
4520
|
+
* serialize or `include` (down to each controller's `maxIncludeDepth`) has
|
|
4521
|
+
* no Serializer in that namespace, listing each missing one.
|
|
4522
|
+
*
|
|
4523
|
+
* @property serializerFallback
|
|
4524
|
+
* @type {Boolean}
|
|
4525
|
+
* @default true
|
|
4526
|
+
* @public
|
|
4527
|
+
*/
|
|
4528
|
+
serializerFallback = true;
|
|
4529
|
+
/**
|
|
4530
|
+
* The Serializer to serialize (and validate, and load) related resources of
|
|
4531
|
+
* this Controller's responses with: the related model's Serializer in this
|
|
4532
|
+
* Controller's namespace, falling back to the root one.
|
|
4533
|
+
*
|
|
4534
|
+
* Always this Controller's namespace — not its Serializer's, which is the
|
|
4535
|
+
* root one when the namespace has no Serializer for this resource.
|
|
4536
|
+
*
|
|
4537
|
+
* @method serializerFor
|
|
4538
|
+
* @private
|
|
4539
|
+
*/
|
|
4540
|
+
serializerFor(model) {
|
|
4541
|
+
const { serializer, namespace } = this;
|
|
4542
|
+
return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
|
|
4543
|
+
}
|
|
4345
4544
|
constructor({ model, namespace, serializer }) {
|
|
4346
4545
|
Object.assign(this, {
|
|
4347
4546
|
model,
|
|
@@ -4367,7 +4566,7 @@ var Controller = class {
|
|
|
4367
4566
|
* @public
|
|
4368
4567
|
*/
|
|
4369
4568
|
index(req) {
|
|
4370
|
-
return findMany(this.model, req);
|
|
4569
|
+
return findMany(this.model, req, (related) => this.serializerFor(related));
|
|
4371
4570
|
}
|
|
4372
4571
|
/**
|
|
4373
4572
|
* This method supports including relationships, and sparse fieldsets via
|
|
@@ -4382,7 +4581,7 @@ var Controller = class {
|
|
|
4382
4581
|
* @public
|
|
4383
4582
|
*/
|
|
4384
4583
|
show(req) {
|
|
4385
|
-
return findOne(this.model, req);
|
|
4584
|
+
return findOne(this.model, req, (related) => this.serializerFor(related));
|
|
4386
4585
|
}
|
|
4387
4586
|
/**
|
|
4388
4587
|
* Create and return a single Model instance that the Controller instance
|
|
@@ -4403,6 +4602,7 @@ var Controller = class {
|
|
|
4403
4602
|
data: { attributes, relationships }
|
|
4404
4603
|
}
|
|
4405
4604
|
} = req;
|
|
4605
|
+
await validateRelationships(model, relationships);
|
|
4406
4606
|
const record = await model.create({
|
|
4407
4607
|
...attributes,
|
|
4408
4608
|
...resolveRelationships(model, relationships)
|
|
@@ -4428,12 +4628,13 @@ var Controller = class {
|
|
|
4428
4628
|
*/
|
|
4429
4629
|
update(req) {
|
|
4430
4630
|
const { model } = this;
|
|
4431
|
-
return findOne(model, req).then((record) => {
|
|
4631
|
+
return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
|
|
4432
4632
|
const {
|
|
4433
4633
|
params: {
|
|
4434
4634
|
data: { attributes, relationships }
|
|
4435
4635
|
}
|
|
4436
4636
|
} = req;
|
|
4637
|
+
await validateRelationships(model, relationships);
|
|
4437
4638
|
return record.update({
|
|
4438
4639
|
...attributes,
|
|
4439
4640
|
...resolveRelationships(model, relationships)
|
|
@@ -4457,7 +4658,7 @@ var Controller = class {
|
|
|
4457
4658
|
* @public
|
|
4458
4659
|
*/
|
|
4459
4660
|
destroy(req) {
|
|
4460
|
-
return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
|
|
4661
|
+
return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
|
|
4461
4662
|
}
|
|
4462
4663
|
/**
|
|
4463
4664
|
* Respond to HEAD or OPTIONS requests.
|
|
@@ -4475,7 +4676,194 @@ var Controller = class {
|
|
|
4475
4676
|
var controller_default = Controller;
|
|
4476
4677
|
|
|
4477
4678
|
// src/packages/serializer/index.ts
|
|
4478
|
-
var
|
|
4679
|
+
var import_path3 = require("path");
|
|
4680
|
+
var import_inflection14 = require("inflection");
|
|
4681
|
+
|
|
4682
|
+
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
4683
|
+
var import_path2 = require("path");
|
|
4684
|
+
function closestAncestor(source, key) {
|
|
4685
|
+
const name = import_path2.posix.basename(key);
|
|
4686
|
+
let namespace = import_path2.posix.dirname(key);
|
|
4687
|
+
if (namespace === ".") {
|
|
4688
|
+
return source.get(name);
|
|
4689
|
+
}
|
|
4690
|
+
namespace = import_path2.posix.dirname(namespace);
|
|
4691
|
+
const ancestor = source.get(import_path2.posix.join(namespace, name));
|
|
4692
|
+
if (ancestor) {
|
|
4693
|
+
return ancestor;
|
|
4694
|
+
}
|
|
4695
|
+
return closestAncestor(source, import_path2.posix.join(import_path2.posix.dirname(namespace), name));
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/packages/serializer/utils/load-linkage.ts
|
|
4699
|
+
var import_inflection13 = require("inflection");
|
|
4700
|
+
var valueOf = (record, key) => Reflect.get(record, key);
|
|
4701
|
+
var toId = (value) => value == null ? null : String(value);
|
|
4702
|
+
async function loadLinkage(model, records, names) {
|
|
4703
|
+
const ids = Array.from(
|
|
4704
|
+
new Set(records.map((record) => record.getPrimaryKey()))
|
|
4705
|
+
);
|
|
4706
|
+
const linkage = /* @__PURE__ */ new Map();
|
|
4707
|
+
const belongsTo = [];
|
|
4708
|
+
const queries = [];
|
|
4709
|
+
ids.forEach((id) => {
|
|
4710
|
+
linkage.set(
|
|
4711
|
+
String(id),
|
|
4712
|
+
names.reduce((result, name) => {
|
|
4713
|
+
const opts = model.relationshipFor(name);
|
|
4714
|
+
result[name] = opts && opts.type === "hasMany" ? [] : null;
|
|
4715
|
+
return result;
|
|
4716
|
+
}, {})
|
|
4717
|
+
);
|
|
4718
|
+
});
|
|
4719
|
+
if (!ids.length) {
|
|
4720
|
+
return linkage;
|
|
4721
|
+
}
|
|
4722
|
+
const link = (ownerId, name, relatedId) => {
|
|
4723
|
+
const owner = linkage.get(String(ownerId));
|
|
4724
|
+
const id = toId(relatedId);
|
|
4725
|
+
if (!owner || id === null) {
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4728
|
+
const current = owner[name];
|
|
4729
|
+
if (Array.isArray(current)) {
|
|
4730
|
+
if (!current.includes(id)) {
|
|
4731
|
+
current.push(id);
|
|
4732
|
+
}
|
|
4733
|
+
} else if (current === null) {
|
|
4734
|
+
owner[name] = id;
|
|
4735
|
+
}
|
|
4736
|
+
};
|
|
4737
|
+
names.forEach((name) => {
|
|
4738
|
+
const opts = model.relationshipFor(name);
|
|
4739
|
+
if (!opts) {
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
const { type, through, model: related } = opts;
|
|
4743
|
+
const foreignKey = (0, import_inflection13.camelize)(opts.foreignKey, true);
|
|
4744
|
+
if (type === "belongsTo") {
|
|
4745
|
+
belongsTo.push([name, foreignKey, related]);
|
|
4746
|
+
} else if (through) {
|
|
4747
|
+
const inverse = related.relationshipFor(opts.inverse);
|
|
4748
|
+
if (!inverse) {
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
const relatedKey = (0, import_inflection13.camelize)(inverse.foreignKey, true);
|
|
4752
|
+
queries.push(
|
|
4753
|
+
(async () => {
|
|
4754
|
+
const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
|
|
4755
|
+
rows.forEach((row) => {
|
|
4756
|
+
link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
|
|
4757
|
+
});
|
|
4758
|
+
})()
|
|
4759
|
+
);
|
|
4760
|
+
} else {
|
|
4761
|
+
queries.push(
|
|
4762
|
+
(async () => {
|
|
4763
|
+
const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
|
|
4764
|
+
rows.forEach((row) => {
|
|
4765
|
+
link(valueOf(row, foreignKey), name, row.getPrimaryKey());
|
|
4766
|
+
});
|
|
4767
|
+
})()
|
|
4768
|
+
);
|
|
4769
|
+
}
|
|
4770
|
+
});
|
|
4771
|
+
if (belongsTo.length) {
|
|
4772
|
+
queries.push(
|
|
4773
|
+
(async () => {
|
|
4774
|
+
const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
|
|
4775
|
+
rows.forEach((row) => {
|
|
4776
|
+
belongsTo.forEach(([name, key]) => {
|
|
4777
|
+
link(row.getPrimaryKey(), name, valueOf(row, key));
|
|
4778
|
+
});
|
|
4779
|
+
});
|
|
4780
|
+
await dropDangling(linkage, belongsTo);
|
|
4781
|
+
})()
|
|
4782
|
+
);
|
|
4783
|
+
}
|
|
4784
|
+
await Promise.all(queries);
|
|
4785
|
+
return linkage;
|
|
4786
|
+
}
|
|
4787
|
+
async function dropDangling(linkage, belongsTo) {
|
|
4788
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
4789
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4790
|
+
const ids = wanted.get(related) || /* @__PURE__ */ new Set();
|
|
4791
|
+
linkage.forEach(({ [name]: id }) => {
|
|
4792
|
+
if (typeof id === "string") {
|
|
4793
|
+
ids.add(id);
|
|
4794
|
+
}
|
|
4795
|
+
});
|
|
4796
|
+
wanted.set(related, ids);
|
|
4797
|
+
});
|
|
4798
|
+
const existing = /* @__PURE__ */ new Map();
|
|
4799
|
+
await Promise.all(
|
|
4800
|
+
Array.from(wanted, async ([related, ids]) => {
|
|
4801
|
+
if (!ids.size) {
|
|
4802
|
+
existing.set(related, ids);
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
|
|
4806
|
+
existing.set(
|
|
4807
|
+
related,
|
|
4808
|
+
new Set(rows.map((row) => String(row.getPrimaryKey())))
|
|
4809
|
+
);
|
|
4810
|
+
})
|
|
4811
|
+
);
|
|
4812
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4813
|
+
const found = existing.get(related);
|
|
4814
|
+
linkage.forEach((owner) => {
|
|
4815
|
+
const id = owner[name];
|
|
4816
|
+
if (typeof id === "string" && !found?.has(id)) {
|
|
4817
|
+
owner[name] = null;
|
|
4818
|
+
}
|
|
4819
|
+
});
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
// src/packages/serializer/utils/include-tree.ts
|
|
4824
|
+
function createIncludeTree(paths = []) {
|
|
4825
|
+
const tree = /* @__PURE__ */ new Map();
|
|
4826
|
+
paths.forEach((path) => {
|
|
4827
|
+
path.split(".").filter(Boolean).reduce((node, name) => {
|
|
4828
|
+
let child = node.get(name);
|
|
4829
|
+
if (!child) {
|
|
4830
|
+
child = /* @__PURE__ */ new Map();
|
|
4831
|
+
node.set(name, child);
|
|
4832
|
+
}
|
|
4833
|
+
return child;
|
|
4834
|
+
}, tree);
|
|
4835
|
+
});
|
|
4836
|
+
return tree;
|
|
4837
|
+
}
|
|
4838
|
+
function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
|
|
4839
|
+
if (depth < 1) {
|
|
4840
|
+
return [];
|
|
4841
|
+
}
|
|
4842
|
+
return names.reduce((paths, name) => {
|
|
4843
|
+
const opts = model.relationshipFor(name);
|
|
4844
|
+
if (!opts) {
|
|
4845
|
+
return paths;
|
|
4846
|
+
}
|
|
4847
|
+
const { model: related } = opts;
|
|
4848
|
+
const serializer = serializerFor(related);
|
|
4849
|
+
if (!serializer) {
|
|
4850
|
+
return [...paths, name];
|
|
4851
|
+
}
|
|
4852
|
+
const { hasOne, hasMany } = serializer;
|
|
4853
|
+
return [
|
|
4854
|
+
...paths,
|
|
4855
|
+
name,
|
|
4856
|
+
...enumerateIncludePaths(
|
|
4857
|
+
related,
|
|
4858
|
+
[...hasOne, ...hasMany],
|
|
4859
|
+
depth - 1,
|
|
4860
|
+
serializerFor
|
|
4861
|
+
).map((path) => `${name}.${path}`)
|
|
4862
|
+
];
|
|
4863
|
+
}, []);
|
|
4864
|
+
}
|
|
4865
|
+
|
|
4866
|
+
// src/packages/serializer/index.ts
|
|
4479
4867
|
var Serializer = class {
|
|
4480
4868
|
/**
|
|
4481
4869
|
* An Array of the `hasOne` or `belongsTo` relationships on a Serializer
|
|
@@ -4562,9 +4950,17 @@ var Serializer = class {
|
|
|
4562
4950
|
* the resource and relationship objects in the returned [JSON API](
|
|
4563
4951
|
* http://jsonapi.org) document object.
|
|
4564
4952
|
*
|
|
4565
|
-
* @param {Array} options.include - An array of
|
|
4566
|
-
*
|
|
4567
|
-
* the returned [JSON API](http://jsonapi.org)
|
|
4953
|
+
* @param {Array} options.include - An array of relationship paths (e.g.
|
|
4954
|
+
* `'comments'` or `'comments.user'`) whose resources should be added to the
|
|
4955
|
+
* top level included object of the returned [JSON API](http://jsonapi.org)
|
|
4956
|
+
* document object. Intermediate resources of a nested path are included too.
|
|
4957
|
+
*
|
|
4958
|
+
* @param {String} options.namespace - The namespace of the request, i.e. of
|
|
4959
|
+
* the Controller handling it. Every link in the document is built in it, and
|
|
4960
|
+
* included resources are serialized by their Serializer in it (falling back
|
|
4961
|
+
* to the root). Defaults to this Serializer's namespace — which is the root
|
|
4962
|
+
* one when a namespaced Controller has no Serializer of its own, so the
|
|
4963
|
+
* Controller passes its namespace explicitly.
|
|
4568
4964
|
*
|
|
4569
4965
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
|
|
4570
4966
|
* object.
|
|
@@ -4575,38 +4971,61 @@ var Serializer = class {
|
|
|
4575
4971
|
data,
|
|
4576
4972
|
links,
|
|
4577
4973
|
domain,
|
|
4578
|
-
include
|
|
4974
|
+
include,
|
|
4975
|
+
namespace = this.namespace
|
|
4579
4976
|
}) {
|
|
4580
|
-
|
|
4581
|
-
const
|
|
4977
|
+
const tree = createIncludeTree(include);
|
|
4978
|
+
const related = /* @__PURE__ */ new Map();
|
|
4979
|
+
const included = /* @__PURE__ */ new Map();
|
|
4980
|
+
const names = Array.from(tree.keys());
|
|
4981
|
+
let primary;
|
|
4982
|
+
let serialized;
|
|
4582
4983
|
if (Array.isArray(data)) {
|
|
4583
|
-
|
|
4584
|
-
data
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
)
|
|
4984
|
+
primary = await Promise.all(
|
|
4985
|
+
data.map(
|
|
4986
|
+
(item) => this.formatOne({
|
|
4987
|
+
item,
|
|
4988
|
+
domain,
|
|
4989
|
+
related,
|
|
4990
|
+
namespace,
|
|
4991
|
+
include: names
|
|
4992
|
+
})
|
|
4593
4993
|
)
|
|
4594
|
-
|
|
4994
|
+
);
|
|
4995
|
+
serialized = { data: primary };
|
|
4595
4996
|
} else {
|
|
4596
|
-
|
|
4597
|
-
|
|
4997
|
+
const resource2 = await this.formatOne({
|
|
4998
|
+
domain,
|
|
4999
|
+
related,
|
|
5000
|
+
namespace,
|
|
5001
|
+
item: data,
|
|
5002
|
+
include: names,
|
|
5003
|
+
links: false
|
|
5004
|
+
});
|
|
5005
|
+
primary = [resource2];
|
|
5006
|
+
serialized = { data: resource2 };
|
|
5007
|
+
}
|
|
5008
|
+
for (const [name, children] of tree) {
|
|
5009
|
+
const opts = this.model.relationshipFor(name);
|
|
5010
|
+
const records = related.get(name);
|
|
5011
|
+
if (opts && records) {
|
|
5012
|
+
await this.addIncluded({
|
|
4598
5013
|
domain,
|
|
4599
|
-
|
|
5014
|
+
records,
|
|
4600
5015
|
included,
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
5016
|
+
namespace,
|
|
5017
|
+
model: opts.model,
|
|
5018
|
+
tree: children
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
4605
5021
|
}
|
|
4606
|
-
|
|
5022
|
+
primary.forEach((resource2) => {
|
|
5023
|
+
included.delete(resourceKey(resource2));
|
|
5024
|
+
});
|
|
5025
|
+
if (included.size) {
|
|
4607
5026
|
serialized = {
|
|
4608
5027
|
...serialized,
|
|
4609
|
-
included:
|
|
5028
|
+
included: Array.from(included.values())
|
|
4610
5029
|
};
|
|
4611
5030
|
}
|
|
4612
5031
|
return {
|
|
@@ -4621,6 +5040,12 @@ var Serializer = class {
|
|
|
4621
5040
|
* Transform a single Model instance into a [JSON API](http://jsonapi.org)
|
|
4622
5041
|
* resource object.
|
|
4623
5042
|
*
|
|
5043
|
+
* Relationships are serialized in one of two ways. By default each one is
|
|
5044
|
+
* read from the Model instance (for primary data the query has already
|
|
5045
|
+
* loaded them). When `linkage` is given — as it is for included resources,
|
|
5046
|
+
* whose relationships are batch-loaded by `addIncluded()` — the resource
|
|
5047
|
+
* linkage is built from it instead, without touching the database.
|
|
5048
|
+
*
|
|
4624
5049
|
* @method formatOne
|
|
4625
5050
|
*
|
|
4626
5051
|
* @param {Object} options - An options object used for building the returned
|
|
@@ -4637,17 +5062,19 @@ var Serializer = class {
|
|
|
4637
5062
|
* the top level links object or relationship links objects in the returned
|
|
4638
5063
|
* [JSON API](http://jsonapi.org) resource object.
|
|
4639
5064
|
*
|
|
4640
|
-
* @param {Array} options.include - An array of
|
|
4641
|
-
*
|
|
4642
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
5065
|
+
* @param {Array} options.include - An array of the relationship keys whose
|
|
5066
|
+
* related records should be collected into `options.related`.
|
|
4643
5067
|
*
|
|
4644
|
-
* @param {
|
|
4645
|
-
*
|
|
4646
|
-
* included
|
|
5068
|
+
* @param {Map} options.related - Collects, per relationship key in
|
|
5069
|
+
* `options.include`, the related Model instances that belong in the top
|
|
5070
|
+
* level included object of a [JSON API](http://jsonapi.org) document object.
|
|
4647
5071
|
*
|
|
4648
|
-
* @param {
|
|
4649
|
-
*
|
|
4650
|
-
*
|
|
5072
|
+
* @param {Object} options.linkage - Pre-loaded resource linkage (related
|
|
5073
|
+
* primary keys per relationship key) to serialize relationships from.
|
|
5074
|
+
*
|
|
5075
|
+
* @param {String} options.namespace - The namespace to build links in.
|
|
5076
|
+
* Defaults to this Serializer's; included resources pass the namespace of
|
|
5077
|
+
* the request, so every link in a document points into the same namespace.
|
|
4651
5078
|
*
|
|
4652
5079
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
|
|
4653
5080
|
* object.
|
|
@@ -4658,13 +5085,15 @@ var Serializer = class {
|
|
|
4658
5085
|
item,
|
|
4659
5086
|
links,
|
|
4660
5087
|
domain,
|
|
4661
|
-
include,
|
|
4662
|
-
|
|
4663
|
-
|
|
5088
|
+
include = [],
|
|
5089
|
+
related,
|
|
5090
|
+
linkage,
|
|
5091
|
+
namespace = this.namespace
|
|
4664
5092
|
}) {
|
|
4665
5093
|
const { resourceName: type } = item;
|
|
4666
5094
|
const id = String(item.getPrimaryKey());
|
|
4667
|
-
|
|
5095
|
+
const names = [...this.hasOne, ...this.hasMany];
|
|
5096
|
+
let relationships;
|
|
4668
5097
|
const attributes = dasherizeKeys(
|
|
4669
5098
|
item.getAttributes(
|
|
4670
5099
|
...Object.keys(item.rawColumnData).filter(
|
|
@@ -4677,34 +5106,41 @@ var Serializer = class {
|
|
|
4677
5106
|
type,
|
|
4678
5107
|
attributes
|
|
4679
5108
|
};
|
|
4680
|
-
if (
|
|
5109
|
+
if (linkage) {
|
|
5110
|
+
relationships = names.reduce(
|
|
5111
|
+
(hash, name) => ({
|
|
5112
|
+
...hash,
|
|
5113
|
+
[(0, import_inflection14.dasherize)(underscore(name))]: this.formatLinkage(
|
|
5114
|
+
domain,
|
|
5115
|
+
this.model.relationshipFor(name)?.model.resourceName,
|
|
5116
|
+
linkage[name],
|
|
5117
|
+
namespace
|
|
5118
|
+
)
|
|
5119
|
+
}),
|
|
5120
|
+
{}
|
|
5121
|
+
);
|
|
5122
|
+
} else {
|
|
5123
|
+
const collect = (name, records) => {
|
|
5124
|
+
if (related && include.includes(name)) {
|
|
5125
|
+
related.set(name, [...related.get(name) || [], ...records]);
|
|
5126
|
+
}
|
|
5127
|
+
};
|
|
4681
5128
|
relationships = await promiseHash(
|
|
4682
|
-
|
|
5129
|
+
names.reduce(
|
|
4683
5130
|
(hash, name) => ({
|
|
4684
5131
|
...hash,
|
|
4685
|
-
[(0,
|
|
4686
|
-
const
|
|
4687
|
-
if (Array.isArray(
|
|
5132
|
+
[(0, import_inflection14.dasherize)(underscore(name))]: (async () => {
|
|
5133
|
+
const value = await Reflect.get(item, name);
|
|
5134
|
+
if (Array.isArray(value)) {
|
|
5135
|
+
collect(name, value);
|
|
4688
5136
|
return {
|
|
4689
|
-
data:
|
|
4690
|
-
|
|
4691
|
-
const { data: relatedData } = await this.formatRelationship({
|
|
4692
|
-
domain,
|
|
4693
|
-
included,
|
|
4694
|
-
item: relatedItem,
|
|
4695
|
-
include: include.includes(name)
|
|
4696
|
-
});
|
|
4697
|
-
return relatedData;
|
|
4698
|
-
})
|
|
5137
|
+
data: value.map(
|
|
5138
|
+
(relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
|
|
4699
5139
|
)
|
|
4700
5140
|
};
|
|
4701
|
-
} else if (
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
included,
|
|
4705
|
-
item: related,
|
|
4706
|
-
include: include.includes(name)
|
|
4707
|
-
});
|
|
5141
|
+
} else if (value && value.id != null) {
|
|
5142
|
+
collect(name, [value]);
|
|
5143
|
+
return this.formatRelationship(value, domain, namespace);
|
|
4708
5144
|
}
|
|
4709
5145
|
return {
|
|
4710
5146
|
data: null
|
|
@@ -4719,16 +5155,9 @@ var Serializer = class {
|
|
|
4719
5155
|
serialized.relationships = relationships;
|
|
4720
5156
|
}
|
|
4721
5157
|
if (links || typeof links !== "boolean") {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4726
|
-
};
|
|
4727
|
-
} else {
|
|
4728
|
-
serialized.links = {
|
|
4729
|
-
self: `${domain}/${type}/${id}`
|
|
4730
|
-
};
|
|
4731
|
-
}
|
|
5158
|
+
serialized.links = {
|
|
5159
|
+
self: this.linkFor(domain, type, id, namespace)
|
|
5160
|
+
};
|
|
4732
5161
|
}
|
|
4733
5162
|
return serialized;
|
|
4734
5163
|
}
|
|
@@ -4738,70 +5167,167 @@ var Serializer = class {
|
|
|
4738
5167
|
*
|
|
4739
5168
|
* @method formatRelationship
|
|
4740
5169
|
*
|
|
4741
|
-
* @param {
|
|
5170
|
+
* @param {Model} item - The Model instance to transform into the returned
|
|
4742
5171
|
* [JSON API](http://jsonapi.org) relationship object.
|
|
4743
5172
|
*
|
|
4744
|
-
* @param {
|
|
5173
|
+
* @param {String} domain - A string used to build links included in the
|
|
4745
5174
|
* returned [JSON API](http://jsonapi.org) relationship object.
|
|
4746
5175
|
*
|
|
4747
|
-
* @
|
|
4748
|
-
* the returned [JSON API](http://jsonapi.org) relationship object.
|
|
4749
|
-
*
|
|
4750
|
-
* @param {Array} options.include - An array of strings containing the
|
|
4751
|
-
* relationship keys that should be added to the top level included object of
|
|
4752
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
4753
|
-
*
|
|
4754
|
-
* @param {Array} options.included - An array of [JSON API](
|
|
4755
|
-
* http://jsonapi.org) resource objects that will be added to the top level
|
|
4756
|
-
* included array of a [JSON API](http://jsonapi.org) document object.
|
|
5176
|
+
* @return {Object} A [JSON API](http://jsonapi.org) relationship object.
|
|
4757
5177
|
*
|
|
4758
|
-
* @
|
|
4759
|
-
|
|
5178
|
+
* @private
|
|
5179
|
+
*/
|
|
5180
|
+
formatRelationship(item, domain, namespace = this.namespace) {
|
|
5181
|
+
return this.formatLinkage(
|
|
5182
|
+
domain,
|
|
5183
|
+
item.resourceName,
|
|
5184
|
+
String(item.getPrimaryKey()),
|
|
5185
|
+
namespace
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
5188
|
+
/**
|
|
5189
|
+
* Build a [JSON API](http://jsonapi.org) relationship object from resource
|
|
5190
|
+
* linkage, in the same shape `formatRelationship()` produces from Model
|
|
5191
|
+
* instances: to-one relationships carry a `links` object, to-many ones only
|
|
5192
|
+
* `data`, and a missing to-one relationship is `{ data: null }`.
|
|
4760
5193
|
*
|
|
5194
|
+
* @method formatLinkage
|
|
4761
5195
|
* @private
|
|
4762
5196
|
*/
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
included
|
|
4768
|
-
}) {
|
|
4769
|
-
const { namespace } = this;
|
|
4770
|
-
const {
|
|
4771
|
-
resourceName: type,
|
|
4772
|
-
constructor: { serializer }
|
|
4773
|
-
} = item;
|
|
4774
|
-
const id = String(item.getPrimaryKey());
|
|
4775
|
-
let links;
|
|
4776
|
-
if (namespace) {
|
|
4777
|
-
links = {
|
|
4778
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4779
|
-
};
|
|
4780
|
-
} else {
|
|
4781
|
-
links = {
|
|
4782
|
-
self: `${domain}/${type}/${id}`
|
|
5197
|
+
formatLinkage(domain, type, linkage, namespace = this.namespace) {
|
|
5198
|
+
if (Array.isArray(linkage)) {
|
|
5199
|
+
return {
|
|
5200
|
+
data: type ? linkage.map((id) => ({ id, type })) : []
|
|
4783
5201
|
};
|
|
4784
5202
|
}
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
domain,
|
|
4790
|
-
include: [],
|
|
4791
|
-
included: [],
|
|
4792
|
-
formatRelationships: false
|
|
4793
|
-
})
|
|
4794
|
-
);
|
|
5203
|
+
if (linkage == null || !type) {
|
|
5204
|
+
return {
|
|
5205
|
+
data: null
|
|
5206
|
+
};
|
|
4795
5207
|
}
|
|
4796
5208
|
return {
|
|
4797
5209
|
data: {
|
|
4798
|
-
id,
|
|
5210
|
+
id: linkage,
|
|
4799
5211
|
type
|
|
4800
5212
|
},
|
|
4801
|
-
links
|
|
5213
|
+
links: {
|
|
5214
|
+
self: this.linkFor(domain, type, linkage, namespace)
|
|
5215
|
+
}
|
|
4802
5216
|
};
|
|
4803
5217
|
}
|
|
5218
|
+
/**
|
|
5219
|
+
* Add `records` (instances of `model`) to `included` as resource objects,
|
|
5220
|
+
* then recurse into the relationships named in `tree`. Each is serialized by
|
|
5221
|
+
* `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
|
|
5222
|
+
* `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
|
|
5223
|
+
* is one and `CommentsSerializer` otherwise. The relationships of every level
|
|
5224
|
+
* are batch-loaded with one query per relationship, not one per record.
|
|
5225
|
+
*
|
|
5226
|
+
* @method addIncluded
|
|
5227
|
+
* @private
|
|
5228
|
+
*/
|
|
5229
|
+
async addIncluded({
|
|
5230
|
+
model,
|
|
5231
|
+
records,
|
|
5232
|
+
tree,
|
|
5233
|
+
domain,
|
|
5234
|
+
included,
|
|
5235
|
+
namespace
|
|
5236
|
+
}) {
|
|
5237
|
+
const serializer = this.serializerFor(model, namespace);
|
|
5238
|
+
const unique = Array.from(
|
|
5239
|
+
new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
|
|
5240
|
+
);
|
|
5241
|
+
if (!unique.length) {
|
|
5242
|
+
return;
|
|
5243
|
+
}
|
|
5244
|
+
const names = [...serializer.hasOne, ...serializer.hasMany];
|
|
5245
|
+
const linkage = await loadLinkage(model, unique, names);
|
|
5246
|
+
for (const item of unique) {
|
|
5247
|
+
const id = String(item.getPrimaryKey());
|
|
5248
|
+
const key = resourceKey({ id, type: item.resourceName });
|
|
5249
|
+
if (!included.has(key)) {
|
|
5250
|
+
included.set(
|
|
5251
|
+
key,
|
|
5252
|
+
await serializer.formatOne({
|
|
5253
|
+
item,
|
|
5254
|
+
domain,
|
|
5255
|
+
linkage: linkage.get(id),
|
|
5256
|
+
namespace
|
|
5257
|
+
})
|
|
5258
|
+
);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
for (const [name, children] of tree) {
|
|
5262
|
+
const opts = model.relationshipFor(name);
|
|
5263
|
+
if (!opts || !names.includes(name)) {
|
|
5264
|
+
continue;
|
|
5265
|
+
}
|
|
5266
|
+
const { model: next } = opts;
|
|
5267
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5268
|
+
linkage.forEach(({ [name]: value }) => {
|
|
5269
|
+
(Array.isArray(value) ? value : [value]).forEach((relatedId) => {
|
|
5270
|
+
if (relatedId != null) {
|
|
5271
|
+
ids.add(relatedId);
|
|
5272
|
+
}
|
|
5273
|
+
});
|
|
5274
|
+
});
|
|
5275
|
+
if (ids.size) {
|
|
5276
|
+
const attributes = this.serializerFor(
|
|
5277
|
+
next,
|
|
5278
|
+
namespace
|
|
5279
|
+
).attributes.filter((attr) => next.attributeNames.includes(attr));
|
|
5280
|
+
const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
|
|
5281
|
+
await this.addIncluded({
|
|
5282
|
+
domain,
|
|
5283
|
+
included,
|
|
5284
|
+
namespace,
|
|
5285
|
+
model: next,
|
|
5286
|
+
records: nextRecords,
|
|
5287
|
+
tree: children
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
/**
|
|
5293
|
+
* Resolve the Serializer for `model` in `namespace` (this Serializer's by
|
|
5294
|
+
* default), the way a namespaced Controller resolves its own:
|
|
5295
|
+
* `admin/comments` if it exists, otherwise the closest ancestor namespace's,
|
|
5296
|
+
* down to the root `comments` Serializer. Falls back to `model.serializer`
|
|
5297
|
+
* when this Serializer was not created by an application (e.g. in
|
|
5298
|
+
* isolation).
|
|
5299
|
+
*
|
|
5300
|
+
* Pass the request's namespace when there is one: a Serializer's own
|
|
5301
|
+
* namespace is the root one whenever it is a namespaced Controller's
|
|
5302
|
+
* fallback.
|
|
5303
|
+
*
|
|
5304
|
+
* @method serializerFor
|
|
5305
|
+
* @private
|
|
5306
|
+
*/
|
|
5307
|
+
serializerFor(model, namespace = this.namespace) {
|
|
5308
|
+
const { serializers } = this;
|
|
5309
|
+
if (serializers) {
|
|
5310
|
+
const key = import_path3.posix.join(namespace || ".", model.resourceName);
|
|
5311
|
+
const serializer = serializers.get(key) || closestAncestor(serializers, key);
|
|
5312
|
+
if (serializer) {
|
|
5313
|
+
return serializer;
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
return model.serializer;
|
|
5317
|
+
}
|
|
5318
|
+
/**
|
|
5319
|
+
* @private
|
|
5320
|
+
*/
|
|
5321
|
+
linkFor(domain, type, id, namespace = this.namespace) {
|
|
5322
|
+
if (namespace) {
|
|
5323
|
+
return `${domain}/${namespace}/${type}/${id}`;
|
|
5324
|
+
}
|
|
5325
|
+
return `${domain}/${type}/${id}`;
|
|
5326
|
+
}
|
|
4804
5327
|
};
|
|
5328
|
+
function resourceKey({ id, type }) {
|
|
5329
|
+
return `${type}:${id}`;
|
|
5330
|
+
}
|
|
4805
5331
|
var serializer_default = Serializer;
|
|
4806
5332
|
|
|
4807
5333
|
// src/packages/config/index.ts
|
|
@@ -5000,7 +5526,10 @@ function resource(action) {
|
|
|
5000
5526
|
data,
|
|
5001
5527
|
links,
|
|
5002
5528
|
domain,
|
|
5003
|
-
include
|
|
5529
|
+
include,
|
|
5530
|
+
// The request's namespace, not the serializer's: a namespaced
|
|
5531
|
+
// controller without its own serializer is given the root one.
|
|
5532
|
+
namespace
|
|
5004
5533
|
});
|
|
5005
5534
|
}
|
|
5006
5535
|
return data;
|
|
@@ -5081,6 +5610,7 @@ var ParameterTypeError = class extends TypeError {
|
|
|
5081
5610
|
Expected type '${type || "undefined"}' for parameter '${path}' but got
|
|
5082
5611
|
'${actual}'.
|
|
5083
5612
|
`);
|
|
5613
|
+
this.source = sourceFor(path);
|
|
5084
5614
|
}
|
|
5085
5615
|
};
|
|
5086
5616
|
var parameter_type_error_default = createServerError(ParameterTypeError, 400);
|
|
@@ -5093,6 +5623,7 @@ var ParameterValueError = class extends TypeError {
|
|
|
5093
5623
|
[${param.size ? Array.from(param.values()).join(", ") : ""}] but got
|
|
5094
5624
|
${String(actual)}.
|
|
5095
5625
|
`);
|
|
5626
|
+
this.source = sourceFor(param.path);
|
|
5096
5627
|
}
|
|
5097
5628
|
};
|
|
5098
5629
|
var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
@@ -5101,6 +5632,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
|
5101
5632
|
var InvalidParameterError = class extends TypeError {
|
|
5102
5633
|
constructor(path) {
|
|
5103
5634
|
super(`'${path}' is not a valid parameter for this resource.`);
|
|
5635
|
+
this.source = sourceFor(path);
|
|
5104
5636
|
}
|
|
5105
5637
|
};
|
|
5106
5638
|
var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
|
|
@@ -5116,6 +5648,7 @@ var ResourceMismatchError = class extends TypeError {
|
|
|
5116
5648
|
Expected '${String(expected)}' for parameter '${path}' but got
|
|
5117
5649
|
${String(normalized)}.
|
|
5118
5650
|
`);
|
|
5651
|
+
this.source = sourceFor(path);
|
|
5119
5652
|
}
|
|
5120
5653
|
};
|
|
5121
5654
|
var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
|
|
@@ -5124,14 +5657,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
|
|
|
5124
5657
|
var ParameterRequiredError = class extends TypeError {
|
|
5125
5658
|
constructor(path) {
|
|
5126
5659
|
super(`Missing required parameter '${path}'.`);
|
|
5660
|
+
this.source = sourceFor(path);
|
|
5127
5661
|
}
|
|
5128
5662
|
};
|
|
5129
5663
|
var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
|
|
5130
5664
|
|
|
5665
|
+
// src/packages/router/route/params/errors/client-generated-id-error.ts
|
|
5666
|
+
var ClientGeneratedIdError = class extends TypeError {
|
|
5667
|
+
constructor() {
|
|
5668
|
+
super("Client-generated IDs are not supported for this resource.");
|
|
5669
|
+
this.source = sourceFor("data.id");
|
|
5670
|
+
}
|
|
5671
|
+
};
|
|
5672
|
+
var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
|
|
5673
|
+
|
|
5674
|
+
// src/packages/router/route/params/errors/forbidden-parameter-error.ts
|
|
5675
|
+
var ForbiddenParameterError = class extends TypeError {
|
|
5676
|
+
constructor(path) {
|
|
5677
|
+
super(`Setting '${path}' is not supported for this resource.`);
|
|
5678
|
+
this.source = sourceFor(path);
|
|
5679
|
+
}
|
|
5680
|
+
};
|
|
5681
|
+
var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
|
|
5682
|
+
|
|
5131
5683
|
// src/packages/router/route/params/errors/parameter-not-nullable-error.ts
|
|
5132
5684
|
var ParameterNotNullableError = class extends TypeError {
|
|
5133
5685
|
constructor({ path }) {
|
|
5134
5686
|
super(`Parameter '${path}' is not nullable.`);
|
|
5687
|
+
this.source = sourceFor(path);
|
|
5135
5688
|
}
|
|
5136
5689
|
};
|
|
5137
5690
|
var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
|
|
@@ -5260,13 +5813,14 @@ var Parameter = class extends set_default {
|
|
|
5260
5813
|
path,
|
|
5261
5814
|
type,
|
|
5262
5815
|
required: Boolean(required),
|
|
5263
|
-
sanitize: Boolean(sanitize)
|
|
5816
|
+
sanitize: Boolean(sanitize),
|
|
5817
|
+
restricted: values !== void 0
|
|
5264
5818
|
});
|
|
5265
5819
|
this.freeze();
|
|
5266
5820
|
}
|
|
5267
5821
|
validate(value) {
|
|
5268
5822
|
validateType2(this, value);
|
|
5269
|
-
if (this.
|
|
5823
|
+
if (this.restricted) {
|
|
5270
5824
|
return validateValue(this, value);
|
|
5271
5825
|
}
|
|
5272
5826
|
return value;
|
|
@@ -5285,6 +5839,17 @@ function getURLParams(dynamicSegments) {
|
|
|
5285
5839
|
]);
|
|
5286
5840
|
}
|
|
5287
5841
|
|
|
5842
|
+
// src/packages/router/route/params/parameter/forbidden-parameter.ts
|
|
5843
|
+
var ForbiddenParameter = class extends parameter_default {
|
|
5844
|
+
constructor(path) {
|
|
5845
|
+
super({ path });
|
|
5846
|
+
}
|
|
5847
|
+
validate() {
|
|
5848
|
+
throw new forbidden_parameter_error_default(this.path);
|
|
5849
|
+
}
|
|
5850
|
+
};
|
|
5851
|
+
var forbidden_parameter_default = ForbiddenParameter;
|
|
5852
|
+
|
|
5288
5853
|
// src/packages/router/route/params/utils/get-data-params.ts
|
|
5289
5854
|
function getIDParam({ model }) {
|
|
5290
5855
|
const primaryKeyColumn = model.columnFor(model.primaryKey);
|
|
@@ -5337,16 +5902,48 @@ function getRelationshipsParam({
|
|
|
5337
5902
|
model,
|
|
5338
5903
|
params
|
|
5339
5904
|
}) {
|
|
5905
|
+
const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
|
|
5906
|
+
key,
|
|
5907
|
+
new forbidden_parameter_default(`data.relationships.${key}`)
|
|
5908
|
+
]);
|
|
5340
5909
|
return [
|
|
5341
5910
|
"relationships",
|
|
5342
5911
|
new parameter_group_default(
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5912
|
+
[
|
|
5913
|
+
...params.reduce((group, param) => {
|
|
5914
|
+
const path = `data.relationships.${param}`;
|
|
5915
|
+
const opts = model.relationshipFor(param);
|
|
5916
|
+
if (!opts) {
|
|
5917
|
+
return group;
|
|
5918
|
+
}
|
|
5919
|
+
if (opts.type === "hasMany") {
|
|
5920
|
+
return [
|
|
5921
|
+
...group,
|
|
5922
|
+
[
|
|
5923
|
+
param,
|
|
5924
|
+
new parameter_group_default(
|
|
5925
|
+
[
|
|
5926
|
+
[
|
|
5927
|
+
"data",
|
|
5928
|
+
new parameter_default({
|
|
5929
|
+
type: "array",
|
|
5930
|
+
path: `${path}.data`,
|
|
5931
|
+
required: true
|
|
5932
|
+
})
|
|
5933
|
+
]
|
|
5934
|
+
],
|
|
5935
|
+
{
|
|
5936
|
+
path
|
|
5937
|
+
}
|
|
5938
|
+
)
|
|
5939
|
+
]
|
|
5940
|
+
];
|
|
5941
|
+
}
|
|
5942
|
+
const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
|
|
5943
|
+
let primaryKeyType = "number";
|
|
5944
|
+
if (primaryKeyColumn) {
|
|
5945
|
+
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5946
|
+
}
|
|
5350
5947
|
return [
|
|
5351
5948
|
...group,
|
|
5352
5949
|
[
|
|
@@ -5355,11 +5952,32 @@ function getRelationshipsParam({
|
|
|
5355
5952
|
[
|
|
5356
5953
|
[
|
|
5357
5954
|
"data",
|
|
5358
|
-
new
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5955
|
+
new parameter_group_default(
|
|
5956
|
+
[
|
|
5957
|
+
[
|
|
5958
|
+
"id",
|
|
5959
|
+
new parameter_default({
|
|
5960
|
+
type: primaryKeyType,
|
|
5961
|
+
path: `${path}.data.id`,
|
|
5962
|
+
required: true
|
|
5963
|
+
})
|
|
5964
|
+
],
|
|
5965
|
+
[
|
|
5966
|
+
"type",
|
|
5967
|
+
new parameter_default({
|
|
5968
|
+
type: "string",
|
|
5969
|
+
path: `${path}.data.type`,
|
|
5970
|
+
values: [opts.model.resourceName],
|
|
5971
|
+
required: true
|
|
5972
|
+
})
|
|
5973
|
+
]
|
|
5974
|
+
],
|
|
5975
|
+
{
|
|
5976
|
+
type: "array",
|
|
5977
|
+
path: `${path}.data`,
|
|
5978
|
+
required: true
|
|
5979
|
+
}
|
|
5980
|
+
)
|
|
5363
5981
|
]
|
|
5364
5982
|
],
|
|
5365
5983
|
{
|
|
@@ -5368,55 +5986,9 @@ function getRelationshipsParam({
|
|
|
5368
5986
|
)
|
|
5369
5987
|
]
|
|
5370
5988
|
];
|
|
5371
|
-
}
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
if (primaryKeyColumn) {
|
|
5375
|
-
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5376
|
-
}
|
|
5377
|
-
return [
|
|
5378
|
-
...group,
|
|
5379
|
-
[
|
|
5380
|
-
param,
|
|
5381
|
-
new parameter_group_default(
|
|
5382
|
-
[
|
|
5383
|
-
[
|
|
5384
|
-
"data",
|
|
5385
|
-
new parameter_group_default(
|
|
5386
|
-
[
|
|
5387
|
-
[
|
|
5388
|
-
"id",
|
|
5389
|
-
new parameter_default({
|
|
5390
|
-
type: primaryKeyType,
|
|
5391
|
-
path: `${path}.data.id`,
|
|
5392
|
-
required: true
|
|
5393
|
-
})
|
|
5394
|
-
],
|
|
5395
|
-
[
|
|
5396
|
-
"type",
|
|
5397
|
-
new parameter_default({
|
|
5398
|
-
type: "string",
|
|
5399
|
-
path: `${path}.data.type`,
|
|
5400
|
-
values: [opts.model.resourceName],
|
|
5401
|
-
required: true
|
|
5402
|
-
})
|
|
5403
|
-
]
|
|
5404
|
-
],
|
|
5405
|
-
{
|
|
5406
|
-
type: "array",
|
|
5407
|
-
path: `${path}.data`,
|
|
5408
|
-
required: true
|
|
5409
|
-
}
|
|
5410
|
-
)
|
|
5411
|
-
]
|
|
5412
|
-
],
|
|
5413
|
-
{
|
|
5414
|
-
path
|
|
5415
|
-
}
|
|
5416
|
-
)
|
|
5417
|
-
]
|
|
5418
|
-
];
|
|
5419
|
-
}, []),
|
|
5989
|
+
}, []),
|
|
5990
|
+
...forbidden
|
|
5991
|
+
],
|
|
5420
5992
|
{
|
|
5421
5993
|
path: "data.relationships"
|
|
5422
5994
|
}
|
|
@@ -5524,10 +6096,9 @@ function getFilterParam({ filter }) {
|
|
|
5524
6096
|
)
|
|
5525
6097
|
];
|
|
5526
6098
|
}
|
|
5527
|
-
function getFieldsParam({
|
|
5528
|
-
model,
|
|
5529
|
-
|
|
5530
|
-
}) {
|
|
6099
|
+
function getFieldsParam(controller) {
|
|
6100
|
+
const { model, serializer } = controller;
|
|
6101
|
+
const { hasOne, hasMany, attributes } = serializer;
|
|
5531
6102
|
const relationships = [...hasOne, ...hasMany];
|
|
5532
6103
|
return [
|
|
5533
6104
|
"fields",
|
|
@@ -5556,7 +6127,7 @@ function getFieldsParam({
|
|
|
5556
6127
|
sanitize: true,
|
|
5557
6128
|
values: [
|
|
5558
6129
|
opts.model.primaryKey,
|
|
5559
|
-
...opts.model.
|
|
6130
|
+
...controller.serializerFor(opts.model).attributes
|
|
5560
6131
|
]
|
|
5561
6132
|
})
|
|
5562
6133
|
]
|
|
@@ -5574,16 +6145,30 @@ function getFieldsParam({
|
|
|
5574
6145
|
)
|
|
5575
6146
|
];
|
|
5576
6147
|
}
|
|
5577
|
-
function getIncludeParam({
|
|
5578
|
-
|
|
5579
|
-
}
|
|
6148
|
+
function getIncludeParam(controller) {
|
|
6149
|
+
const { model, maxIncludeDepth, serializer } = controller;
|
|
6150
|
+
const { hasOne, hasMany } = serializer;
|
|
5580
6151
|
const relationships = [...hasOne, ...hasMany];
|
|
5581
6152
|
return [
|
|
5582
6153
|
"include",
|
|
5583
6154
|
new parameter_default({
|
|
5584
6155
|
path: "include",
|
|
5585
6156
|
type: "array",
|
|
5586
|
-
|
|
6157
|
+
// Every top level name stays allowed (as before), plus the nested paths
|
|
6158
|
+
// (`comments.user`) reachable through each related serializer in the
|
|
6159
|
+
// controller's namespace (at every level, even below a root fallback
|
|
6160
|
+
// serializer), down to the controller's `maxIncludeDepth`.
|
|
6161
|
+
values: Array.from(
|
|
6162
|
+
/* @__PURE__ */ new Set([
|
|
6163
|
+
...relationships,
|
|
6164
|
+
...enumerateIncludePaths(
|
|
6165
|
+
model,
|
|
6166
|
+
relationships,
|
|
6167
|
+
maxIncludeDepth,
|
|
6168
|
+
(related) => controller.serializerFor(related)
|
|
6169
|
+
)
|
|
6170
|
+
])
|
|
6171
|
+
)
|
|
5587
6172
|
})
|
|
5588
6173
|
];
|
|
5589
6174
|
}
|
|
@@ -5621,6 +6206,15 @@ function getCollectionQueryParams(controller) {
|
|
|
5621
6206
|
return getCustomParams(controller);
|
|
5622
6207
|
}
|
|
5623
6208
|
|
|
6209
|
+
// src/packages/router/route/params/utils/validate-client-id.ts
|
|
6210
|
+
function validateClientId(params) {
|
|
6211
|
+
const { data } = params;
|
|
6212
|
+
if (data && typeof data === "object" && Reflect.has(data, "id")) {
|
|
6213
|
+
throw new client_generated_id_error_default();
|
|
6214
|
+
}
|
|
6215
|
+
return true;
|
|
6216
|
+
}
|
|
6217
|
+
|
|
5624
6218
|
// src/packages/router/route/params/utils/validate-resource-id.ts
|
|
5625
6219
|
function validateResourceId({
|
|
5626
6220
|
params: {
|
|
@@ -5786,6 +6380,9 @@ var Route = class extends set_default {
|
|
|
5786
6380
|
...req.params,
|
|
5787
6381
|
...this.parseParams(req.url.params)
|
|
5788
6382
|
};
|
|
6383
|
+
if (this.action === "create" && req.method === "POST") {
|
|
6384
|
+
validateClientId(params);
|
|
6385
|
+
}
|
|
5789
6386
|
if (req.method !== "OPTIONS") {
|
|
5790
6387
|
params = this.params.validate(params);
|
|
5791
6388
|
}
|
|
@@ -6098,10 +6695,10 @@ var Router = class extends map_default {
|
|
|
6098
6695
|
var router_default = Router;
|
|
6099
6696
|
|
|
6100
6697
|
// src/packages/loader/utils/bundle-for.ts
|
|
6101
|
-
var
|
|
6698
|
+
var import_path4 = require("path");
|
|
6102
6699
|
|
|
6103
6700
|
// src/packages/loader/utils/format-key.ts
|
|
6104
|
-
var
|
|
6701
|
+
var import_inflection15 = require("inflection");
|
|
6105
6702
|
var NAMESPACE_DELIMITER = /\$-/g;
|
|
6106
6703
|
function formatKey(key, formatter) {
|
|
6107
6704
|
return chain(key).pipe((str) => {
|
|
@@ -6109,7 +6706,7 @@ function formatKey(key, formatter) {
|
|
|
6109
6706
|
return formatter(str);
|
|
6110
6707
|
}
|
|
6111
6708
|
return str;
|
|
6112
|
-
}).pipe(underscore).pipe(
|
|
6709
|
+
}).pipe(underscore).pipe(import_inflection15.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
|
|
6113
6710
|
}
|
|
6114
6711
|
|
|
6115
6712
|
// src/packages/loader/utils/bundle-for.ts
|
|
@@ -6176,31 +6773,15 @@ function normalize2(manifest) {
|
|
|
6176
6773
|
}
|
|
6177
6774
|
function bundleFor(path) {
|
|
6178
6775
|
const manifest = Reflect.apply(require, null, [
|
|
6179
|
-
(0,
|
|
6776
|
+
(0, import_path4.join)(path, "dist", "bundle")
|
|
6180
6777
|
]);
|
|
6181
6778
|
return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
|
|
6182
6779
|
}
|
|
6183
6780
|
|
|
6184
|
-
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
6185
|
-
var import_path3 = require("path");
|
|
6186
|
-
function closestAncestor(source, key) {
|
|
6187
|
-
const name = import_path3.posix.basename(key);
|
|
6188
|
-
let namespace = import_path3.posix.dirname(key);
|
|
6189
|
-
if (namespace === ".") {
|
|
6190
|
-
return source.get(name);
|
|
6191
|
-
}
|
|
6192
|
-
namespace = import_path3.posix.dirname(namespace);
|
|
6193
|
-
const ancestor = source.get(import_path3.posix.join(namespace, name));
|
|
6194
|
-
if (ancestor) {
|
|
6195
|
-
return ancestor;
|
|
6196
|
-
}
|
|
6197
|
-
return closestAncestor(source, import_path3.posix.join(import_path3.posix.dirname(namespace), name));
|
|
6198
|
-
}
|
|
6199
|
-
|
|
6200
6781
|
// src/packages/loader/resolver/utils/closest-child.ts
|
|
6201
|
-
var
|
|
6782
|
+
var import_path5 = require("path");
|
|
6202
6783
|
function closestChild(source, key) {
|
|
6203
|
-
const [[, result] = []] = Array.from(source).map(([path, value]) => [
|
|
6784
|
+
const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path5.posix.basename(path), value]).filter(([resource2]) => key === resource2);
|
|
6204
6785
|
return result;
|
|
6205
6786
|
}
|
|
6206
6787
|
|
|
@@ -6223,7 +6804,7 @@ function resolve(group) {
|
|
|
6223
6804
|
}
|
|
6224
6805
|
|
|
6225
6806
|
// src/packages/loader/builder/utils/create-parent-builder.ts
|
|
6226
|
-
var
|
|
6807
|
+
var import_path6 = require("path");
|
|
6227
6808
|
|
|
6228
6809
|
// src/packages/loader/builder/utils/sort-by-namespace.ts
|
|
6229
6810
|
function sortByNamespace([a], [b]) {
|
|
@@ -6244,7 +6825,7 @@ function createParentBuilder(construct) {
|
|
|
6244
6825
|
let grandparent = null;
|
|
6245
6826
|
if (key !== "root") {
|
|
6246
6827
|
const found = result.find((namespace) => {
|
|
6247
|
-
const dirname =
|
|
6828
|
+
const dirname = import_path6.posix.dirname(key);
|
|
6248
6829
|
if (namespace.key === "root") {
|
|
6249
6830
|
return dirname === ".";
|
|
6250
6831
|
}
|
|
@@ -6314,13 +6895,13 @@ function normalizePort(port) {
|
|
|
6314
6895
|
}
|
|
6315
6896
|
|
|
6316
6897
|
// src/packages/application/utils/create-controller.ts
|
|
6317
|
-
var
|
|
6898
|
+
var import_path7 = require("path");
|
|
6318
6899
|
function createController(constructor, opts) {
|
|
6319
6900
|
const { key, store, serializers } = opts;
|
|
6320
|
-
const namespace =
|
|
6901
|
+
const namespace = import_path7.posix.dirname(key).replace(".", "");
|
|
6321
6902
|
let { parent } = opts;
|
|
6322
6903
|
let model = tryCatchSync(
|
|
6323
|
-
() => store.modelFor(
|
|
6904
|
+
() => store.modelFor(import_path7.posix.basename(key))
|
|
6324
6905
|
);
|
|
6325
6906
|
let serializer = serializers.get(key);
|
|
6326
6907
|
if (!model) {
|
|
@@ -6376,13 +6957,13 @@ function createController(constructor, opts) {
|
|
|
6376
6957
|
}
|
|
6377
6958
|
|
|
6378
6959
|
// src/packages/application/utils/create-serializer.ts
|
|
6379
|
-
var
|
|
6960
|
+
var import_path8 = require("path");
|
|
6380
6961
|
function createSerializer(constructor, opts) {
|
|
6381
6962
|
const { key, store } = opts;
|
|
6382
|
-
const namespace =
|
|
6963
|
+
const namespace = import_path8.posix.dirname(key).replace(".", "");
|
|
6383
6964
|
let { parent } = opts;
|
|
6384
6965
|
let model = tryCatchSync(
|
|
6385
|
-
() => store.modelFor(
|
|
6966
|
+
() => store.modelFor(import_path8.posix.basename(key))
|
|
6386
6967
|
);
|
|
6387
6968
|
if (!model) {
|
|
6388
6969
|
model = null;
|
|
@@ -6406,6 +6987,85 @@ function createSerializer(constructor, opts) {
|
|
|
6406
6987
|
return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
|
|
6407
6988
|
}
|
|
6408
6989
|
|
|
6990
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6991
|
+
var import_path9 = require("path");
|
|
6992
|
+
|
|
6993
|
+
// src/errors/namespaced-serializer-missing-error.ts
|
|
6994
|
+
var NamespacedSerializerMissingError = class extends ReferenceError {
|
|
6995
|
+
/**
|
|
6996
|
+
* @param missing - Each missing serializer key (`admin/comments`) with an
|
|
6997
|
+
* example of how it is reached (`admin/posts?include=comments`).
|
|
6998
|
+
*/
|
|
6999
|
+
constructor(missing) {
|
|
7000
|
+
const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
|
|
7001
|
+
super(
|
|
7002
|
+
[
|
|
7003
|
+
"Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
|
|
7004
|
+
...lines
|
|
7005
|
+
].join("\n")
|
|
7006
|
+
);
|
|
7007
|
+
}
|
|
7008
|
+
};
|
|
7009
|
+
var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
|
|
7010
|
+
|
|
7011
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
7012
|
+
var distance = (via) => {
|
|
7013
|
+
const [, include] = via.split("?include=");
|
|
7014
|
+
return include ? include.split(".").length : 0;
|
|
7015
|
+
};
|
|
7016
|
+
function validateNamespacedSerializers(controllers, serializers) {
|
|
7017
|
+
const missing = /* @__PURE__ */ new Map();
|
|
7018
|
+
controllers.forEach((controller, key) => {
|
|
7019
|
+
const { namespace, model } = controller;
|
|
7020
|
+
if (!namespace || !model) {
|
|
7021
|
+
return;
|
|
7022
|
+
}
|
|
7023
|
+
const application = controllers.get(import_path9.posix.join(namespace, "application"));
|
|
7024
|
+
if (!application || application.serializerFallback !== false) {
|
|
7025
|
+
return;
|
|
7026
|
+
}
|
|
7027
|
+
const require2 = (type, via) => {
|
|
7028
|
+
const serializerKey = import_path9.posix.join(namespace, type);
|
|
7029
|
+
const serializer = serializers.get(serializerKey);
|
|
7030
|
+
if (!serializer) {
|
|
7031
|
+
const known = missing.get(serializerKey);
|
|
7032
|
+
if (known === void 0 || distance(via) < distance(known)) {
|
|
7033
|
+
missing.set(serializerKey, via);
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
return serializer;
|
|
7037
|
+
};
|
|
7038
|
+
const own = require2(model.resourceName, key);
|
|
7039
|
+
if (!own) {
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
7042
|
+
const depth = Math.max(controller.maxIncludeDepth, 1);
|
|
7043
|
+
let level = [
|
|
7044
|
+
[model, own, []]
|
|
7045
|
+
];
|
|
7046
|
+
for (let current = 1; current <= depth && level.length; current += 1) {
|
|
7047
|
+
const next = [];
|
|
7048
|
+
level.forEach(([parent, serializer, path]) => {
|
|
7049
|
+
[...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
|
|
7050
|
+
const opts = parent.relationshipFor(name);
|
|
7051
|
+
if (!opts) {
|
|
7052
|
+
return;
|
|
7053
|
+
}
|
|
7054
|
+
const nextPath = [...path, name];
|
|
7055
|
+
const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
|
|
7056
|
+
if (related) {
|
|
7057
|
+
next.push([opts.model, related, nextPath]);
|
|
7058
|
+
}
|
|
7059
|
+
});
|
|
7060
|
+
});
|
|
7061
|
+
level = next;
|
|
7062
|
+
}
|
|
7063
|
+
});
|
|
7064
|
+
if (missing.size) {
|
|
7065
|
+
throw new namespaced_serializer_missing_error_default(missing);
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
|
|
6409
7069
|
// src/packages/application/initialize.ts
|
|
6410
7070
|
async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
|
|
6411
7071
|
const load = createLoader(path);
|
|
@@ -6428,6 +7088,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6428
7088
|
parent
|
|
6429
7089
|
})
|
|
6430
7090
|
);
|
|
7091
|
+
serializers.forEach((serializer) => {
|
|
7092
|
+
Reflect.defineProperty(serializer, "serializers", {
|
|
7093
|
+
value: serializers,
|
|
7094
|
+
writable: false,
|
|
7095
|
+
enumerable: false,
|
|
7096
|
+
configurable: false
|
|
7097
|
+
});
|
|
7098
|
+
});
|
|
6431
7099
|
models.forEach((model) => {
|
|
6432
7100
|
Reflect.defineProperty(model, "serializer", {
|
|
6433
7101
|
value: closestChild(serializers, model.resourceName),
|
|
@@ -6453,6 +7121,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6453
7121
|
configurable: false
|
|
6454
7122
|
});
|
|
6455
7123
|
});
|
|
7124
|
+
validateNamespacedSerializers(controllers, serializers);
|
|
6456
7125
|
const ApplicationController = controllers.get("application");
|
|
6457
7126
|
if (!ApplicationController) {
|
|
6458
7127
|
throw new controller_missing_error_default("application");
|