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.mjs
CHANGED
|
@@ -9,6 +9,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
import { green, yellow as yellow3 } from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/packages/database/constants.ts
|
|
12
|
+
var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
|
|
13
|
+
var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
|
|
12
14
|
var VALID_DRIVERS = [
|
|
13
15
|
"pg",
|
|
14
16
|
"sqlite3",
|
|
@@ -1056,10 +1058,16 @@ async function buildResults({
|
|
|
1056
1058
|
}
|
|
1057
1059
|
);
|
|
1058
1060
|
}
|
|
1061
|
+
const missing = new Set(
|
|
1062
|
+
entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
|
|
1063
|
+
);
|
|
1064
|
+
const absent = Array.from(missing).filter(
|
|
1065
|
+
(name) => model.relationshipFor(name)?.type === "hasOne"
|
|
1066
|
+
);
|
|
1059
1067
|
const instance = Reflect.construct(model, [
|
|
1060
1068
|
entries(record).reduce((r, entry) => {
|
|
1061
1069
|
let [key, value] = entry;
|
|
1062
|
-
if (
|
|
1070
|
+
if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
|
|
1063
1071
|
return r;
|
|
1064
1072
|
} else if (key.indexOf(".") >= 0) {
|
|
1065
1073
|
const [a, b] = key.split(".");
|
|
@@ -1079,6 +1087,7 @@ async function buildResults({
|
|
|
1079
1087
|
};
|
|
1080
1088
|
}, {})
|
|
1081
1089
|
]);
|
|
1090
|
+
absent.forEach((name) => instance.absentRelationships.add(name));
|
|
1082
1091
|
instance.currentChangeSet.persist();
|
|
1083
1092
|
return instance;
|
|
1084
1093
|
});
|
|
@@ -1090,6 +1099,17 @@ function createRunner(target, opts) {
|
|
|
1090
1099
|
const { resolve: resolve2, reject } = opts;
|
|
1091
1100
|
let didRun = false;
|
|
1092
1101
|
RUNNERS.set(target, async () => {
|
|
1102
|
+
if (didRun) {
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
didRun = true;
|
|
1106
|
+
try {
|
|
1107
|
+
await run();
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
reject(err);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
async function run() {
|
|
1093
1113
|
let results;
|
|
1094
1114
|
const {
|
|
1095
1115
|
model,
|
|
@@ -1099,10 +1119,6 @@ function createRunner(target, opts) {
|
|
|
1099
1119
|
shouldCount,
|
|
1100
1120
|
relationships
|
|
1101
1121
|
} = target;
|
|
1102
|
-
if (didRun) {
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
didRun = true;
|
|
1106
1122
|
if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
|
|
1107
1123
|
target.select(...target.model.attributeNames);
|
|
1108
1124
|
}
|
|
@@ -1143,7 +1159,7 @@ function createRunner(target, opts) {
|
|
|
1143
1159
|
resolve2(result);
|
|
1144
1160
|
}
|
|
1145
1161
|
}
|
|
1146
|
-
}
|
|
1162
|
+
}
|
|
1147
1163
|
}
|
|
1148
1164
|
}
|
|
1149
1165
|
function runQuery(target) {
|
|
@@ -1154,6 +1170,17 @@ function runQuery(target) {
|
|
|
1154
1170
|
}
|
|
1155
1171
|
|
|
1156
1172
|
// src/packages/database/query/index.ts
|
|
1173
|
+
var CONDITIONS = /* @__PURE__ */ new Set([
|
|
1174
|
+
"where",
|
|
1175
|
+
"whereNot",
|
|
1176
|
+
"whereIn",
|
|
1177
|
+
"whereNotIn",
|
|
1178
|
+
"whereNull",
|
|
1179
|
+
"whereNotNull",
|
|
1180
|
+
"whereBetween",
|
|
1181
|
+
"whereNotBetween",
|
|
1182
|
+
"whereRaw"
|
|
1183
|
+
]);
|
|
1157
1184
|
var Query = class extends Promise {
|
|
1158
1185
|
constructor(model) {
|
|
1159
1186
|
let resolve2;
|
|
@@ -1346,12 +1373,11 @@ var Query = class extends Promise {
|
|
|
1346
1373
|
return this;
|
|
1347
1374
|
}
|
|
1348
1375
|
count() {
|
|
1349
|
-
const validName = /^(where(((Not)?(In)?)|(Raw)|(Between)))$/g;
|
|
1350
1376
|
Object.assign(this, {
|
|
1351
1377
|
shouldCount: true,
|
|
1352
1378
|
snapshots: [
|
|
1353
1379
|
["count", "* as countAll"],
|
|
1354
|
-
...this.snapshots.filter(([name]) =>
|
|
1380
|
+
...this.snapshots.filter(([name]) => CONDITIONS.has(name))
|
|
1355
1381
|
]
|
|
1356
1382
|
});
|
|
1357
1383
|
return this;
|
|
@@ -1848,7 +1874,9 @@ async function get(owner, key) {
|
|
|
1848
1874
|
let { foreignKey } = opts;
|
|
1849
1875
|
value = owner.currentChangeSet.get(key);
|
|
1850
1876
|
foreignKey = camelize5(foreignKey, true);
|
|
1851
|
-
if (!value) {
|
|
1877
|
+
if (!value && owner.absentRelationships.has(key)) {
|
|
1878
|
+
value = null;
|
|
1879
|
+
} else if (!value) {
|
|
1852
1880
|
switch (type) {
|
|
1853
1881
|
case "hasOne":
|
|
1854
1882
|
value = await getHasOne(owner, {
|
|
@@ -2510,13 +2538,37 @@ async function initializeClass({
|
|
|
2510
2538
|
return model;
|
|
2511
2539
|
}
|
|
2512
2540
|
|
|
2541
|
+
// src/packages/server/utils/source-for.ts
|
|
2542
|
+
import { dasherize as dasherize3 } from "inflection";
|
|
2543
|
+
function memberName(key) {
|
|
2544
|
+
return dasherize3(underscore(key));
|
|
2545
|
+
}
|
|
2546
|
+
function escapePointer(segment) {
|
|
2547
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2548
|
+
}
|
|
2549
|
+
function sourceFor(path) {
|
|
2550
|
+
const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
|
|
2551
|
+
if (!root) {
|
|
2552
|
+
return {};
|
|
2553
|
+
} else if (root === "data") {
|
|
2554
|
+
return {
|
|
2555
|
+
pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2513
2563
|
// src/packages/database/validation/errors/validation-error.ts
|
|
2514
2564
|
var ValidationError = class extends Error {
|
|
2515
|
-
constructor(key
|
|
2516
|
-
super(`Validation failed for ${key}
|
|
2565
|
+
constructor(key) {
|
|
2566
|
+
super(`Validation failed for ${key}.`);
|
|
2567
|
+
this.key = key;
|
|
2568
|
+
this.source = sourceFor(`data.attributes.${key}`);
|
|
2517
2569
|
}
|
|
2518
2570
|
};
|
|
2519
|
-
var validation_error_default = ValidationError;
|
|
2571
|
+
var validation_error_default = createServerError(ValidationError, 422);
|
|
2520
2572
|
|
|
2521
2573
|
// src/packages/database/validation/index.ts
|
|
2522
2574
|
var Validation = class {
|
|
@@ -2556,12 +2608,32 @@ function validate(instance) {
|
|
|
2556
2608
|
validator: Reflect.get(instance.constructor.validates, key)
|
|
2557
2609
|
})).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
|
|
2558
2610
|
if (!validation.isValid()) {
|
|
2559
|
-
throw new validation_error_default(validation.key
|
|
2611
|
+
throw new validation_error_default(validation.key);
|
|
2560
2612
|
}
|
|
2561
2613
|
return result;
|
|
2562
2614
|
}, true);
|
|
2563
2615
|
}
|
|
2564
2616
|
|
|
2617
|
+
// src/packages/database/model/utils/process-write-error.ts
|
|
2618
|
+
function driverMessage(message) {
|
|
2619
|
+
const index = message.lastIndexOf(" - ");
|
|
2620
|
+
return index >= 0 ? message.slice(index + 3) : message;
|
|
2621
|
+
}
|
|
2622
|
+
function processWriteError(err) {
|
|
2623
|
+
if (!(err instanceof Error)) {
|
|
2624
|
+
return err;
|
|
2625
|
+
}
|
|
2626
|
+
const { code } = err;
|
|
2627
|
+
const { message } = err;
|
|
2628
|
+
if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
|
|
2629
|
+
return new unique_constraint_error_default(driverMessage(message));
|
|
2630
|
+
}
|
|
2631
|
+
return err;
|
|
2632
|
+
}
|
|
2633
|
+
function rethrowWriteError(err) {
|
|
2634
|
+
throw processWriteError(err);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2565
2637
|
// src/packages/database/model/utils/run-hooks.ts
|
|
2566
2638
|
function runHooks(record, trx, ...hooks) {
|
|
2567
2639
|
return hooks.filter((hook) => Boolean(hook)).reduce(
|
|
@@ -2601,6 +2673,12 @@ var Model = class {
|
|
|
2601
2673
|
writable: false,
|
|
2602
2674
|
enumerable: false,
|
|
2603
2675
|
configurable: false
|
|
2676
|
+
},
|
|
2677
|
+
absentRelationships: {
|
|
2678
|
+
value: /* @__PURE__ */ new Set(),
|
|
2679
|
+
writable: false,
|
|
2680
|
+
enumerable: false,
|
|
2681
|
+
configurable: false
|
|
2604
2682
|
}
|
|
2605
2683
|
});
|
|
2606
2684
|
const {
|
|
@@ -2947,9 +3025,9 @@ var Model = class {
|
|
|
2947
3025
|
return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
|
|
2948
3026
|
};
|
|
2949
3027
|
if (transaction) {
|
|
2950
|
-
return run(transaction);
|
|
3028
|
+
return run(transaction).catch(rethrowWriteError);
|
|
2951
3029
|
}
|
|
2952
|
-
return this.transaction(run);
|
|
3030
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
2953
3031
|
}
|
|
2954
3032
|
/**
|
|
2955
3033
|
* Permanently delete the instance from the database.
|
|
@@ -3069,9 +3147,9 @@ var Model = class {
|
|
|
3069
3147
|
return createTransactionResultProxy(instance, true);
|
|
3070
3148
|
};
|
|
3071
3149
|
if (transaction) {
|
|
3072
|
-
return run(transaction);
|
|
3150
|
+
return run(transaction).catch(rethrowWriteError);
|
|
3073
3151
|
}
|
|
3074
|
-
return this.transaction(run);
|
|
3152
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
3075
3153
|
}
|
|
3076
3154
|
/**
|
|
3077
3155
|
* Specify the transaction object to use for following save, update, or
|
|
@@ -3458,7 +3536,7 @@ var TRUE = /^true$/;
|
|
|
3458
3536
|
var BRACKETS = /(?:\[])/g;
|
|
3459
3537
|
|
|
3460
3538
|
// src/utils/transform-keys.ts
|
|
3461
|
-
import { camelize as camelize7, dasherize as
|
|
3539
|
+
import { camelize as camelize7, dasherize as dasherize4 } from "inflection";
|
|
3462
3540
|
function transformKeys(source, transformer, deep = false) {
|
|
3463
3541
|
if (Array.isArray(source)) {
|
|
3464
3542
|
return source.slice(0);
|
|
@@ -3489,7 +3567,7 @@ function camelizeKeys(source, deep) {
|
|
|
3489
3567
|
function dasherizeKeys(source, deep) {
|
|
3490
3568
|
return transformKeys(
|
|
3491
3569
|
source,
|
|
3492
|
-
(key) =>
|
|
3570
|
+
(key) => dasherize4(underscore(key)),
|
|
3493
3571
|
deep
|
|
3494
3572
|
);
|
|
3495
3573
|
}
|
|
@@ -3798,6 +3876,10 @@ function dataFor(status, err) {
|
|
|
3798
3876
|
if (title) {
|
|
3799
3877
|
errData.title = title;
|
|
3800
3878
|
}
|
|
3879
|
+
const source = err?.source;
|
|
3880
|
+
if (source && (source.pointer || source.parameter)) {
|
|
3881
|
+
errData.source = source;
|
|
3882
|
+
}
|
|
3801
3883
|
if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
|
|
3802
3884
|
errData.detail = err.message.replace(/^\[public\]/gi, "");
|
|
3803
3885
|
}
|
|
@@ -4020,9 +4102,10 @@ function merge(dest, source) {
|
|
|
4020
4102
|
}
|
|
4021
4103
|
|
|
4022
4104
|
// src/packages/controller/utils/params-to-query.ts
|
|
4023
|
-
function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
4105
|
+
function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
|
|
4024
4106
|
const relationships = entries(model.relationships);
|
|
4025
4107
|
const includedFields = omit(fields, model.resourceName);
|
|
4108
|
+
const included = include && include.map((path) => path.split(".")[0]);
|
|
4026
4109
|
let query = {
|
|
4027
4110
|
id,
|
|
4028
4111
|
filter,
|
|
@@ -4065,9 +4148,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4065
4148
|
if (!value.includes(relationship.model.primaryKey)) {
|
|
4066
4149
|
value = [relationship.model.primaryKey, ...value];
|
|
4067
4150
|
}
|
|
4068
|
-
if (
|
|
4069
|
-
value = [...value, ...relationship.model.
|
|
4070
|
-
} else if (!
|
|
4151
|
+
if (included && value.length === 1 && included.includes(name)) {
|
|
4152
|
+
value = [...value, ...serializerFor(relationship.model).attributes];
|
|
4153
|
+
} else if (!included && value.length > 1) {
|
|
4071
4154
|
value = value.slice(0, 1);
|
|
4072
4155
|
}
|
|
4073
4156
|
return {
|
|
@@ -4082,18 +4165,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4082
4165
|
}
|
|
4083
4166
|
|
|
4084
4167
|
// src/packages/controller/utils/find-one.ts
|
|
4085
|
-
function findOne(model, req) {
|
|
4168
|
+
function findOne(model, req, serializerFor) {
|
|
4086
4169
|
const params = merge(req.defaultParams, req.params);
|
|
4087
|
-
const { id, select, include } = paramsToQuery(model, params);
|
|
4170
|
+
const { id, select, include } = paramsToQuery(model, params, serializerFor);
|
|
4088
4171
|
return model.find(id).select(...select).include(include);
|
|
4089
4172
|
}
|
|
4090
4173
|
|
|
4091
4174
|
// src/packages/controller/utils/find-many.ts
|
|
4092
|
-
function findMany(model, req) {
|
|
4175
|
+
function findMany(model, req, serializerFor) {
|
|
4093
4176
|
const params = merge(req.defaultParams, req.params);
|
|
4094
4177
|
const { sort, page, limit, select, filter, include } = paramsToQuery(
|
|
4095
4178
|
model,
|
|
4096
|
-
params
|
|
4179
|
+
params,
|
|
4180
|
+
serializerFor
|
|
4097
4181
|
);
|
|
4098
4182
|
return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
|
|
4099
4183
|
}
|
|
@@ -4123,6 +4207,53 @@ function resolveRelationships(model, relationships = {}) {
|
|
|
4123
4207
|
);
|
|
4124
4208
|
}
|
|
4125
4209
|
|
|
4210
|
+
// src/packages/controller/errors/related-record-not-found-error.ts
|
|
4211
|
+
var RelatedRecordNotFoundError = class extends Error {
|
|
4212
|
+
constructor({ name, primaryKey }, primaryKeyValue, path) {
|
|
4213
|
+
super(
|
|
4214
|
+
`Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
|
|
4215
|
+
);
|
|
4216
|
+
this.source = sourceFor(path);
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
|
|
4220
|
+
|
|
4221
|
+
// src/packages/controller/utils/validate-relationships.ts
|
|
4222
|
+
async function validateRelationships(model, relationships = {}) {
|
|
4223
|
+
await Promise.all(
|
|
4224
|
+
entries(relationships).map(async ([key, value]) => {
|
|
4225
|
+
const opts = model.relationshipFor(key);
|
|
4226
|
+
const { data = null } = value || {};
|
|
4227
|
+
if (!opts || !data) {
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
const { model: related } = opts;
|
|
4231
|
+
const isMany = Array.isArray(data);
|
|
4232
|
+
const ids = (isMany ? data : [data]).map(
|
|
4233
|
+
(item) => Reflect.get(Object(item), "id")
|
|
4234
|
+
);
|
|
4235
|
+
if (!ids.length) {
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
const column = related.columnNameFor(related.primaryKey) || "id";
|
|
4239
|
+
const rows = await related.table().whereIn(
|
|
4240
|
+
`${related.tableName}.${column}`,
|
|
4241
|
+
ids.filter((id) => id != null)
|
|
4242
|
+
).select(`${related.tableName}.${column}`);
|
|
4243
|
+
const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
|
|
4244
|
+
const missing = ids.findIndex((id) => !found.has(String(id)));
|
|
4245
|
+
if (missing >= 0) {
|
|
4246
|
+
const path = `data.relationships.${key}.data`;
|
|
4247
|
+
throw new related_record_not_found_error_default(
|
|
4248
|
+
related,
|
|
4249
|
+
ids[missing],
|
|
4250
|
+
isMany ? `${path}.${missing}` : path
|
|
4251
|
+
);
|
|
4252
|
+
}
|
|
4253
|
+
})
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4126
4257
|
// src/packages/controller/constants.ts
|
|
4127
4258
|
var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
|
|
4128
4259
|
|
|
@@ -4308,6 +4439,74 @@ var Controller = class {
|
|
|
4308
4439
|
* @public
|
|
4309
4440
|
*/
|
|
4310
4441
|
defaultPerPage = 25;
|
|
4442
|
+
/**
|
|
4443
|
+
* How many relationships deep an `?include` path may go on this
|
|
4444
|
+
* controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
|
|
4445
|
+
* only direct relationships (`comments`) can be included. Paths deeper than
|
|
4446
|
+
* this are rejected with `400 Bad Request`.
|
|
4447
|
+
*
|
|
4448
|
+
* Set it on `ApplicationController` to change it for the whole app, or on a
|
|
4449
|
+
* single controller to override it there.
|
|
4450
|
+
*
|
|
4451
|
+
* ```javascript
|
|
4452
|
+
* class ApplicationController extends Controller {
|
|
4453
|
+
* maxIncludeDepth = 2;
|
|
4454
|
+
* }
|
|
4455
|
+
* ```
|
|
4456
|
+
*
|
|
4457
|
+
* Every allowed path is enumerated up front from the serializers'
|
|
4458
|
+
* relationships, and each nested level costs its own queries per request, so
|
|
4459
|
+
* keep this small.
|
|
4460
|
+
*
|
|
4461
|
+
* @property maxIncludeDepth
|
|
4462
|
+
* @type {Number}
|
|
4463
|
+
* @default 3
|
|
4464
|
+
* @public
|
|
4465
|
+
*/
|
|
4466
|
+
maxIncludeDepth = 3;
|
|
4467
|
+
/**
|
|
4468
|
+
* Whether a namespace may fall back to the root Serializer of a type it has
|
|
4469
|
+
* no Serializer for. Read from a namespace's `ApplicationController` and
|
|
4470
|
+
* applies to the whole namespace.
|
|
4471
|
+
*
|
|
4472
|
+
* By default `app/controllers/admin/comments.js` without an
|
|
4473
|
+
* `app/serializers/admin/comments.js` — or an included type without one —
|
|
4474
|
+
* is serialized by the root Serializer, with every attribute and
|
|
4475
|
+
* relationship it declares. For a namespace that must only expose what it
|
|
4476
|
+
* declares itself, turn the fallback off:
|
|
4477
|
+
*
|
|
4478
|
+
* ```javascript
|
|
4479
|
+
* // app/controllers/admin/application.js
|
|
4480
|
+
* class AdminApplicationController extends ApplicationController {
|
|
4481
|
+
* serializerFallback = false;
|
|
4482
|
+
* }
|
|
4483
|
+
* ```
|
|
4484
|
+
*
|
|
4485
|
+
* The application then refuses to boot while any type the namespace can
|
|
4486
|
+
* serialize or `include` (down to each controller's `maxIncludeDepth`) has
|
|
4487
|
+
* no Serializer in that namespace, listing each missing one.
|
|
4488
|
+
*
|
|
4489
|
+
* @property serializerFallback
|
|
4490
|
+
* @type {Boolean}
|
|
4491
|
+
* @default true
|
|
4492
|
+
* @public
|
|
4493
|
+
*/
|
|
4494
|
+
serializerFallback = true;
|
|
4495
|
+
/**
|
|
4496
|
+
* The Serializer to serialize (and validate, and load) related resources of
|
|
4497
|
+
* this Controller's responses with: the related model's Serializer in this
|
|
4498
|
+
* Controller's namespace, falling back to the root one.
|
|
4499
|
+
*
|
|
4500
|
+
* Always this Controller's namespace — not its Serializer's, which is the
|
|
4501
|
+
* root one when the namespace has no Serializer for this resource.
|
|
4502
|
+
*
|
|
4503
|
+
* @method serializerFor
|
|
4504
|
+
* @private
|
|
4505
|
+
*/
|
|
4506
|
+
serializerFor(model) {
|
|
4507
|
+
const { serializer, namespace } = this;
|
|
4508
|
+
return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
|
|
4509
|
+
}
|
|
4311
4510
|
constructor({ model, namespace, serializer }) {
|
|
4312
4511
|
Object.assign(this, {
|
|
4313
4512
|
model,
|
|
@@ -4333,7 +4532,7 @@ var Controller = class {
|
|
|
4333
4532
|
* @public
|
|
4334
4533
|
*/
|
|
4335
4534
|
index(req) {
|
|
4336
|
-
return findMany(this.model, req);
|
|
4535
|
+
return findMany(this.model, req, (related) => this.serializerFor(related));
|
|
4337
4536
|
}
|
|
4338
4537
|
/**
|
|
4339
4538
|
* This method supports including relationships, and sparse fieldsets via
|
|
@@ -4348,7 +4547,7 @@ var Controller = class {
|
|
|
4348
4547
|
* @public
|
|
4349
4548
|
*/
|
|
4350
4549
|
show(req) {
|
|
4351
|
-
return findOne(this.model, req);
|
|
4550
|
+
return findOne(this.model, req, (related) => this.serializerFor(related));
|
|
4352
4551
|
}
|
|
4353
4552
|
/**
|
|
4354
4553
|
* Create and return a single Model instance that the Controller instance
|
|
@@ -4369,6 +4568,7 @@ var Controller = class {
|
|
|
4369
4568
|
data: { attributes, relationships }
|
|
4370
4569
|
}
|
|
4371
4570
|
} = req;
|
|
4571
|
+
await validateRelationships(model, relationships);
|
|
4372
4572
|
const record = await model.create({
|
|
4373
4573
|
...attributes,
|
|
4374
4574
|
...resolveRelationships(model, relationships)
|
|
@@ -4394,12 +4594,13 @@ var Controller = class {
|
|
|
4394
4594
|
*/
|
|
4395
4595
|
update(req) {
|
|
4396
4596
|
const { model } = this;
|
|
4397
|
-
return findOne(model, req).then((record) => {
|
|
4597
|
+
return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
|
|
4398
4598
|
const {
|
|
4399
4599
|
params: {
|
|
4400
4600
|
data: { attributes, relationships }
|
|
4401
4601
|
}
|
|
4402
4602
|
} = req;
|
|
4603
|
+
await validateRelationships(model, relationships);
|
|
4403
4604
|
return record.update({
|
|
4404
4605
|
...attributes,
|
|
4405
4606
|
...resolveRelationships(model, relationships)
|
|
@@ -4423,7 +4624,7 @@ var Controller = class {
|
|
|
4423
4624
|
* @public
|
|
4424
4625
|
*/
|
|
4425
4626
|
destroy(req) {
|
|
4426
|
-
return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
|
|
4627
|
+
return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
|
|
4427
4628
|
}
|
|
4428
4629
|
/**
|
|
4429
4630
|
* Respond to HEAD or OPTIONS requests.
|
|
@@ -4441,7 +4642,194 @@ var Controller = class {
|
|
|
4441
4642
|
var controller_default = Controller;
|
|
4442
4643
|
|
|
4443
4644
|
// src/packages/serializer/index.ts
|
|
4444
|
-
import {
|
|
4645
|
+
import { posix as posix2 } from "path";
|
|
4646
|
+
import { dasherize as dasherize5 } from "inflection";
|
|
4647
|
+
|
|
4648
|
+
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
4649
|
+
import { posix } from "path";
|
|
4650
|
+
function closestAncestor(source, key) {
|
|
4651
|
+
const name = posix.basename(key);
|
|
4652
|
+
let namespace = posix.dirname(key);
|
|
4653
|
+
if (namespace === ".") {
|
|
4654
|
+
return source.get(name);
|
|
4655
|
+
}
|
|
4656
|
+
namespace = posix.dirname(namespace);
|
|
4657
|
+
const ancestor = source.get(posix.join(namespace, name));
|
|
4658
|
+
if (ancestor) {
|
|
4659
|
+
return ancestor;
|
|
4660
|
+
}
|
|
4661
|
+
return closestAncestor(source, posix.join(posix.dirname(namespace), name));
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
// src/packages/serializer/utils/load-linkage.ts
|
|
4665
|
+
import { camelize as camelize9 } from "inflection";
|
|
4666
|
+
var valueOf = (record, key) => Reflect.get(record, key);
|
|
4667
|
+
var toId = (value) => value == null ? null : String(value);
|
|
4668
|
+
async function loadLinkage(model, records, names) {
|
|
4669
|
+
const ids = Array.from(
|
|
4670
|
+
new Set(records.map((record) => record.getPrimaryKey()))
|
|
4671
|
+
);
|
|
4672
|
+
const linkage = /* @__PURE__ */ new Map();
|
|
4673
|
+
const belongsTo = [];
|
|
4674
|
+
const queries = [];
|
|
4675
|
+
ids.forEach((id) => {
|
|
4676
|
+
linkage.set(
|
|
4677
|
+
String(id),
|
|
4678
|
+
names.reduce((result, name) => {
|
|
4679
|
+
const opts = model.relationshipFor(name);
|
|
4680
|
+
result[name] = opts && opts.type === "hasMany" ? [] : null;
|
|
4681
|
+
return result;
|
|
4682
|
+
}, {})
|
|
4683
|
+
);
|
|
4684
|
+
});
|
|
4685
|
+
if (!ids.length) {
|
|
4686
|
+
return linkage;
|
|
4687
|
+
}
|
|
4688
|
+
const link = (ownerId, name, relatedId) => {
|
|
4689
|
+
const owner = linkage.get(String(ownerId));
|
|
4690
|
+
const id = toId(relatedId);
|
|
4691
|
+
if (!owner || id === null) {
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
const current = owner[name];
|
|
4695
|
+
if (Array.isArray(current)) {
|
|
4696
|
+
if (!current.includes(id)) {
|
|
4697
|
+
current.push(id);
|
|
4698
|
+
}
|
|
4699
|
+
} else if (current === null) {
|
|
4700
|
+
owner[name] = id;
|
|
4701
|
+
}
|
|
4702
|
+
};
|
|
4703
|
+
names.forEach((name) => {
|
|
4704
|
+
const opts = model.relationshipFor(name);
|
|
4705
|
+
if (!opts) {
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const { type, through, model: related } = opts;
|
|
4709
|
+
const foreignKey = camelize9(opts.foreignKey, true);
|
|
4710
|
+
if (type === "belongsTo") {
|
|
4711
|
+
belongsTo.push([name, foreignKey, related]);
|
|
4712
|
+
} else if (through) {
|
|
4713
|
+
const inverse = related.relationshipFor(opts.inverse);
|
|
4714
|
+
if (!inverse) {
|
|
4715
|
+
return;
|
|
4716
|
+
}
|
|
4717
|
+
const relatedKey = camelize9(inverse.foreignKey, true);
|
|
4718
|
+
queries.push(
|
|
4719
|
+
(async () => {
|
|
4720
|
+
const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
|
|
4721
|
+
rows.forEach((row) => {
|
|
4722
|
+
link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
|
|
4723
|
+
});
|
|
4724
|
+
})()
|
|
4725
|
+
);
|
|
4726
|
+
} else {
|
|
4727
|
+
queries.push(
|
|
4728
|
+
(async () => {
|
|
4729
|
+
const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
|
|
4730
|
+
rows.forEach((row) => {
|
|
4731
|
+
link(valueOf(row, foreignKey), name, row.getPrimaryKey());
|
|
4732
|
+
});
|
|
4733
|
+
})()
|
|
4734
|
+
);
|
|
4735
|
+
}
|
|
4736
|
+
});
|
|
4737
|
+
if (belongsTo.length) {
|
|
4738
|
+
queries.push(
|
|
4739
|
+
(async () => {
|
|
4740
|
+
const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
|
|
4741
|
+
rows.forEach((row) => {
|
|
4742
|
+
belongsTo.forEach(([name, key]) => {
|
|
4743
|
+
link(row.getPrimaryKey(), name, valueOf(row, key));
|
|
4744
|
+
});
|
|
4745
|
+
});
|
|
4746
|
+
await dropDangling(linkage, belongsTo);
|
|
4747
|
+
})()
|
|
4748
|
+
);
|
|
4749
|
+
}
|
|
4750
|
+
await Promise.all(queries);
|
|
4751
|
+
return linkage;
|
|
4752
|
+
}
|
|
4753
|
+
async function dropDangling(linkage, belongsTo) {
|
|
4754
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
4755
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4756
|
+
const ids = wanted.get(related) || /* @__PURE__ */ new Set();
|
|
4757
|
+
linkage.forEach(({ [name]: id }) => {
|
|
4758
|
+
if (typeof id === "string") {
|
|
4759
|
+
ids.add(id);
|
|
4760
|
+
}
|
|
4761
|
+
});
|
|
4762
|
+
wanted.set(related, ids);
|
|
4763
|
+
});
|
|
4764
|
+
const existing = /* @__PURE__ */ new Map();
|
|
4765
|
+
await Promise.all(
|
|
4766
|
+
Array.from(wanted, async ([related, ids]) => {
|
|
4767
|
+
if (!ids.size) {
|
|
4768
|
+
existing.set(related, ids);
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4771
|
+
const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
|
|
4772
|
+
existing.set(
|
|
4773
|
+
related,
|
|
4774
|
+
new Set(rows.map((row) => String(row.getPrimaryKey())))
|
|
4775
|
+
);
|
|
4776
|
+
})
|
|
4777
|
+
);
|
|
4778
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4779
|
+
const found = existing.get(related);
|
|
4780
|
+
linkage.forEach((owner) => {
|
|
4781
|
+
const id = owner[name];
|
|
4782
|
+
if (typeof id === "string" && !found?.has(id)) {
|
|
4783
|
+
owner[name] = null;
|
|
4784
|
+
}
|
|
4785
|
+
});
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
// src/packages/serializer/utils/include-tree.ts
|
|
4790
|
+
function createIncludeTree(paths = []) {
|
|
4791
|
+
const tree = /* @__PURE__ */ new Map();
|
|
4792
|
+
paths.forEach((path) => {
|
|
4793
|
+
path.split(".").filter(Boolean).reduce((node, name) => {
|
|
4794
|
+
let child = node.get(name);
|
|
4795
|
+
if (!child) {
|
|
4796
|
+
child = /* @__PURE__ */ new Map();
|
|
4797
|
+
node.set(name, child);
|
|
4798
|
+
}
|
|
4799
|
+
return child;
|
|
4800
|
+
}, tree);
|
|
4801
|
+
});
|
|
4802
|
+
return tree;
|
|
4803
|
+
}
|
|
4804
|
+
function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
|
|
4805
|
+
if (depth < 1) {
|
|
4806
|
+
return [];
|
|
4807
|
+
}
|
|
4808
|
+
return names.reduce((paths, name) => {
|
|
4809
|
+
const opts = model.relationshipFor(name);
|
|
4810
|
+
if (!opts) {
|
|
4811
|
+
return paths;
|
|
4812
|
+
}
|
|
4813
|
+
const { model: related } = opts;
|
|
4814
|
+
const serializer = serializerFor(related);
|
|
4815
|
+
if (!serializer) {
|
|
4816
|
+
return [...paths, name];
|
|
4817
|
+
}
|
|
4818
|
+
const { hasOne, hasMany } = serializer;
|
|
4819
|
+
return [
|
|
4820
|
+
...paths,
|
|
4821
|
+
name,
|
|
4822
|
+
...enumerateIncludePaths(
|
|
4823
|
+
related,
|
|
4824
|
+
[...hasOne, ...hasMany],
|
|
4825
|
+
depth - 1,
|
|
4826
|
+
serializerFor
|
|
4827
|
+
).map((path) => `${name}.${path}`)
|
|
4828
|
+
];
|
|
4829
|
+
}, []);
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
// src/packages/serializer/index.ts
|
|
4445
4833
|
var Serializer = class {
|
|
4446
4834
|
/**
|
|
4447
4835
|
* An Array of the `hasOne` or `belongsTo` relationships on a Serializer
|
|
@@ -4528,9 +4916,17 @@ var Serializer = class {
|
|
|
4528
4916
|
* the resource and relationship objects in the returned [JSON API](
|
|
4529
4917
|
* http://jsonapi.org) document object.
|
|
4530
4918
|
*
|
|
4531
|
-
* @param {Array} options.include - An array of
|
|
4532
|
-
*
|
|
4533
|
-
* the returned [JSON API](http://jsonapi.org)
|
|
4919
|
+
* @param {Array} options.include - An array of relationship paths (e.g.
|
|
4920
|
+
* `'comments'` or `'comments.user'`) whose resources should be added to the
|
|
4921
|
+
* top level included object of the returned [JSON API](http://jsonapi.org)
|
|
4922
|
+
* document object. Intermediate resources of a nested path are included too.
|
|
4923
|
+
*
|
|
4924
|
+
* @param {String} options.namespace - The namespace of the request, i.e. of
|
|
4925
|
+
* the Controller handling it. Every link in the document is built in it, and
|
|
4926
|
+
* included resources are serialized by their Serializer in it (falling back
|
|
4927
|
+
* to the root). Defaults to this Serializer's namespace — which is the root
|
|
4928
|
+
* one when a namespaced Controller has no Serializer of its own, so the
|
|
4929
|
+
* Controller passes its namespace explicitly.
|
|
4534
4930
|
*
|
|
4535
4931
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
|
|
4536
4932
|
* object.
|
|
@@ -4541,38 +4937,61 @@ var Serializer = class {
|
|
|
4541
4937
|
data,
|
|
4542
4938
|
links,
|
|
4543
4939
|
domain,
|
|
4544
|
-
include
|
|
4940
|
+
include,
|
|
4941
|
+
namespace = this.namespace
|
|
4545
4942
|
}) {
|
|
4546
|
-
|
|
4547
|
-
const
|
|
4943
|
+
const tree = createIncludeTree(include);
|
|
4944
|
+
const related = /* @__PURE__ */ new Map();
|
|
4945
|
+
const included = /* @__PURE__ */ new Map();
|
|
4946
|
+
const names = Array.from(tree.keys());
|
|
4947
|
+
let primary;
|
|
4948
|
+
let serialized;
|
|
4548
4949
|
if (Array.isArray(data)) {
|
|
4549
|
-
|
|
4550
|
-
data
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
)
|
|
4950
|
+
primary = await Promise.all(
|
|
4951
|
+
data.map(
|
|
4952
|
+
(item) => this.formatOne({
|
|
4953
|
+
item,
|
|
4954
|
+
domain,
|
|
4955
|
+
related,
|
|
4956
|
+
namespace,
|
|
4957
|
+
include: names
|
|
4958
|
+
})
|
|
4559
4959
|
)
|
|
4560
|
-
|
|
4960
|
+
);
|
|
4961
|
+
serialized = { data: primary };
|
|
4561
4962
|
} else {
|
|
4562
|
-
|
|
4563
|
-
|
|
4963
|
+
const resource2 = await this.formatOne({
|
|
4964
|
+
domain,
|
|
4965
|
+
related,
|
|
4966
|
+
namespace,
|
|
4967
|
+
item: data,
|
|
4968
|
+
include: names,
|
|
4969
|
+
links: false
|
|
4970
|
+
});
|
|
4971
|
+
primary = [resource2];
|
|
4972
|
+
serialized = { data: resource2 };
|
|
4973
|
+
}
|
|
4974
|
+
for (const [name, children] of tree) {
|
|
4975
|
+
const opts = this.model.relationshipFor(name);
|
|
4976
|
+
const records = related.get(name);
|
|
4977
|
+
if (opts && records) {
|
|
4978
|
+
await this.addIncluded({
|
|
4564
4979
|
domain,
|
|
4565
|
-
|
|
4980
|
+
records,
|
|
4566
4981
|
included,
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4982
|
+
namespace,
|
|
4983
|
+
model: opts.model,
|
|
4984
|
+
tree: children
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4571
4987
|
}
|
|
4572
|
-
|
|
4988
|
+
primary.forEach((resource2) => {
|
|
4989
|
+
included.delete(resourceKey(resource2));
|
|
4990
|
+
});
|
|
4991
|
+
if (included.size) {
|
|
4573
4992
|
serialized = {
|
|
4574
4993
|
...serialized,
|
|
4575
|
-
included:
|
|
4994
|
+
included: Array.from(included.values())
|
|
4576
4995
|
};
|
|
4577
4996
|
}
|
|
4578
4997
|
return {
|
|
@@ -4587,6 +5006,12 @@ var Serializer = class {
|
|
|
4587
5006
|
* Transform a single Model instance into a [JSON API](http://jsonapi.org)
|
|
4588
5007
|
* resource object.
|
|
4589
5008
|
*
|
|
5009
|
+
* Relationships are serialized in one of two ways. By default each one is
|
|
5010
|
+
* read from the Model instance (for primary data the query has already
|
|
5011
|
+
* loaded them). When `linkage` is given — as it is for included resources,
|
|
5012
|
+
* whose relationships are batch-loaded by `addIncluded()` — the resource
|
|
5013
|
+
* linkage is built from it instead, without touching the database.
|
|
5014
|
+
*
|
|
4590
5015
|
* @method formatOne
|
|
4591
5016
|
*
|
|
4592
5017
|
* @param {Object} options - An options object used for building the returned
|
|
@@ -4603,17 +5028,19 @@ var Serializer = class {
|
|
|
4603
5028
|
* the top level links object or relationship links objects in the returned
|
|
4604
5029
|
* [JSON API](http://jsonapi.org) resource object.
|
|
4605
5030
|
*
|
|
4606
|
-
* @param {Array} options.include - An array of
|
|
4607
|
-
*
|
|
4608
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
5031
|
+
* @param {Array} options.include - An array of the relationship keys whose
|
|
5032
|
+
* related records should be collected into `options.related`.
|
|
4609
5033
|
*
|
|
4610
|
-
* @param {
|
|
4611
|
-
*
|
|
4612
|
-
* included
|
|
5034
|
+
* @param {Map} options.related - Collects, per relationship key in
|
|
5035
|
+
* `options.include`, the related Model instances that belong in the top
|
|
5036
|
+
* level included object of a [JSON API](http://jsonapi.org) document object.
|
|
4613
5037
|
*
|
|
4614
|
-
* @param {
|
|
4615
|
-
*
|
|
4616
|
-
*
|
|
5038
|
+
* @param {Object} options.linkage - Pre-loaded resource linkage (related
|
|
5039
|
+
* primary keys per relationship key) to serialize relationships from.
|
|
5040
|
+
*
|
|
5041
|
+
* @param {String} options.namespace - The namespace to build links in.
|
|
5042
|
+
* Defaults to this Serializer's; included resources pass the namespace of
|
|
5043
|
+
* the request, so every link in a document points into the same namespace.
|
|
4617
5044
|
*
|
|
4618
5045
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
|
|
4619
5046
|
* object.
|
|
@@ -4624,13 +5051,15 @@ var Serializer = class {
|
|
|
4624
5051
|
item,
|
|
4625
5052
|
links,
|
|
4626
5053
|
domain,
|
|
4627
|
-
include,
|
|
4628
|
-
|
|
4629
|
-
|
|
5054
|
+
include = [],
|
|
5055
|
+
related,
|
|
5056
|
+
linkage,
|
|
5057
|
+
namespace = this.namespace
|
|
4630
5058
|
}) {
|
|
4631
5059
|
const { resourceName: type } = item;
|
|
4632
5060
|
const id = String(item.getPrimaryKey());
|
|
4633
|
-
|
|
5061
|
+
const names = [...this.hasOne, ...this.hasMany];
|
|
5062
|
+
let relationships;
|
|
4634
5063
|
const attributes = dasherizeKeys(
|
|
4635
5064
|
item.getAttributes(
|
|
4636
5065
|
...Object.keys(item.rawColumnData).filter(
|
|
@@ -4643,34 +5072,41 @@ var Serializer = class {
|
|
|
4643
5072
|
type,
|
|
4644
5073
|
attributes
|
|
4645
5074
|
};
|
|
4646
|
-
if (
|
|
5075
|
+
if (linkage) {
|
|
5076
|
+
relationships = names.reduce(
|
|
5077
|
+
(hash, name) => ({
|
|
5078
|
+
...hash,
|
|
5079
|
+
[dasherize5(underscore(name))]: this.formatLinkage(
|
|
5080
|
+
domain,
|
|
5081
|
+
this.model.relationshipFor(name)?.model.resourceName,
|
|
5082
|
+
linkage[name],
|
|
5083
|
+
namespace
|
|
5084
|
+
)
|
|
5085
|
+
}),
|
|
5086
|
+
{}
|
|
5087
|
+
);
|
|
5088
|
+
} else {
|
|
5089
|
+
const collect = (name, records) => {
|
|
5090
|
+
if (related && include.includes(name)) {
|
|
5091
|
+
related.set(name, [...related.get(name) || [], ...records]);
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
4647
5094
|
relationships = await promiseHash(
|
|
4648
|
-
|
|
5095
|
+
names.reduce(
|
|
4649
5096
|
(hash, name) => ({
|
|
4650
5097
|
...hash,
|
|
4651
|
-
[
|
|
4652
|
-
const
|
|
4653
|
-
if (Array.isArray(
|
|
5098
|
+
[dasherize5(underscore(name))]: (async () => {
|
|
5099
|
+
const value = await Reflect.get(item, name);
|
|
5100
|
+
if (Array.isArray(value)) {
|
|
5101
|
+
collect(name, value);
|
|
4654
5102
|
return {
|
|
4655
|
-
data:
|
|
4656
|
-
|
|
4657
|
-
const { data: relatedData } = await this.formatRelationship({
|
|
4658
|
-
domain,
|
|
4659
|
-
included,
|
|
4660
|
-
item: relatedItem,
|
|
4661
|
-
include: include.includes(name)
|
|
4662
|
-
});
|
|
4663
|
-
return relatedData;
|
|
4664
|
-
})
|
|
5103
|
+
data: value.map(
|
|
5104
|
+
(relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
|
|
4665
5105
|
)
|
|
4666
5106
|
};
|
|
4667
|
-
} else if (
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
included,
|
|
4671
|
-
item: related,
|
|
4672
|
-
include: include.includes(name)
|
|
4673
|
-
});
|
|
5107
|
+
} else if (value && value.id != null) {
|
|
5108
|
+
collect(name, [value]);
|
|
5109
|
+
return this.formatRelationship(value, domain, namespace);
|
|
4674
5110
|
}
|
|
4675
5111
|
return {
|
|
4676
5112
|
data: null
|
|
@@ -4685,16 +5121,9 @@ var Serializer = class {
|
|
|
4685
5121
|
serialized.relationships = relationships;
|
|
4686
5122
|
}
|
|
4687
5123
|
if (links || typeof links !== "boolean") {
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4692
|
-
};
|
|
4693
|
-
} else {
|
|
4694
|
-
serialized.links = {
|
|
4695
|
-
self: `${domain}/${type}/${id}`
|
|
4696
|
-
};
|
|
4697
|
-
}
|
|
5124
|
+
serialized.links = {
|
|
5125
|
+
self: this.linkFor(domain, type, id, namespace)
|
|
5126
|
+
};
|
|
4698
5127
|
}
|
|
4699
5128
|
return serialized;
|
|
4700
5129
|
}
|
|
@@ -4704,70 +5133,167 @@ var Serializer = class {
|
|
|
4704
5133
|
*
|
|
4705
5134
|
* @method formatRelationship
|
|
4706
5135
|
*
|
|
4707
|
-
* @param {
|
|
5136
|
+
* @param {Model} item - The Model instance to transform into the returned
|
|
4708
5137
|
* [JSON API](http://jsonapi.org) relationship object.
|
|
4709
5138
|
*
|
|
4710
|
-
* @param {
|
|
5139
|
+
* @param {String} domain - A string used to build links included in the
|
|
4711
5140
|
* returned [JSON API](http://jsonapi.org) relationship object.
|
|
4712
5141
|
*
|
|
4713
|
-
* @
|
|
4714
|
-
* the returned [JSON API](http://jsonapi.org) relationship object.
|
|
4715
|
-
*
|
|
4716
|
-
* @param {Array} options.include - An array of strings containing the
|
|
4717
|
-
* relationship keys that should be added to the top level included object of
|
|
4718
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
4719
|
-
*
|
|
4720
|
-
* @param {Array} options.included - An array of [JSON API](
|
|
4721
|
-
* http://jsonapi.org) resource objects that will be added to the top level
|
|
4722
|
-
* included array of a [JSON API](http://jsonapi.org) document object.
|
|
5142
|
+
* @return {Object} A [JSON API](http://jsonapi.org) relationship object.
|
|
4723
5143
|
*
|
|
4724
|
-
* @
|
|
4725
|
-
|
|
5144
|
+
* @private
|
|
5145
|
+
*/
|
|
5146
|
+
formatRelationship(item, domain, namespace = this.namespace) {
|
|
5147
|
+
return this.formatLinkage(
|
|
5148
|
+
domain,
|
|
5149
|
+
item.resourceName,
|
|
5150
|
+
String(item.getPrimaryKey()),
|
|
5151
|
+
namespace
|
|
5152
|
+
);
|
|
5153
|
+
}
|
|
5154
|
+
/**
|
|
5155
|
+
* Build a [JSON API](http://jsonapi.org) relationship object from resource
|
|
5156
|
+
* linkage, in the same shape `formatRelationship()` produces from Model
|
|
5157
|
+
* instances: to-one relationships carry a `links` object, to-many ones only
|
|
5158
|
+
* `data`, and a missing to-one relationship is `{ data: null }`.
|
|
4726
5159
|
*
|
|
5160
|
+
* @method formatLinkage
|
|
4727
5161
|
* @private
|
|
4728
5162
|
*/
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
included
|
|
4734
|
-
}) {
|
|
4735
|
-
const { namespace } = this;
|
|
4736
|
-
const {
|
|
4737
|
-
resourceName: type,
|
|
4738
|
-
constructor: { serializer }
|
|
4739
|
-
} = item;
|
|
4740
|
-
const id = String(item.getPrimaryKey());
|
|
4741
|
-
let links;
|
|
4742
|
-
if (namespace) {
|
|
4743
|
-
links = {
|
|
4744
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4745
|
-
};
|
|
4746
|
-
} else {
|
|
4747
|
-
links = {
|
|
4748
|
-
self: `${domain}/${type}/${id}`
|
|
5163
|
+
formatLinkage(domain, type, linkage, namespace = this.namespace) {
|
|
5164
|
+
if (Array.isArray(linkage)) {
|
|
5165
|
+
return {
|
|
5166
|
+
data: type ? linkage.map((id) => ({ id, type })) : []
|
|
4749
5167
|
};
|
|
4750
5168
|
}
|
|
4751
|
-
if (
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
domain,
|
|
4756
|
-
include: [],
|
|
4757
|
-
included: [],
|
|
4758
|
-
formatRelationships: false
|
|
4759
|
-
})
|
|
4760
|
-
);
|
|
5169
|
+
if (linkage == null || !type) {
|
|
5170
|
+
return {
|
|
5171
|
+
data: null
|
|
5172
|
+
};
|
|
4761
5173
|
}
|
|
4762
5174
|
return {
|
|
4763
5175
|
data: {
|
|
4764
|
-
id,
|
|
5176
|
+
id: linkage,
|
|
4765
5177
|
type
|
|
4766
5178
|
},
|
|
4767
|
-
links
|
|
5179
|
+
links: {
|
|
5180
|
+
self: this.linkFor(domain, type, linkage, namespace)
|
|
5181
|
+
}
|
|
4768
5182
|
};
|
|
4769
5183
|
}
|
|
5184
|
+
/**
|
|
5185
|
+
* Add `records` (instances of `model`) to `included` as resource objects,
|
|
5186
|
+
* then recurse into the relationships named in `tree`. Each is serialized by
|
|
5187
|
+
* `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
|
|
5188
|
+
* `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
|
|
5189
|
+
* is one and `CommentsSerializer` otherwise. The relationships of every level
|
|
5190
|
+
* are batch-loaded with one query per relationship, not one per record.
|
|
5191
|
+
*
|
|
5192
|
+
* @method addIncluded
|
|
5193
|
+
* @private
|
|
5194
|
+
*/
|
|
5195
|
+
async addIncluded({
|
|
5196
|
+
model,
|
|
5197
|
+
records,
|
|
5198
|
+
tree,
|
|
5199
|
+
domain,
|
|
5200
|
+
included,
|
|
5201
|
+
namespace
|
|
5202
|
+
}) {
|
|
5203
|
+
const serializer = this.serializerFor(model, namespace);
|
|
5204
|
+
const unique = Array.from(
|
|
5205
|
+
new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
|
|
5206
|
+
);
|
|
5207
|
+
if (!unique.length) {
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
const names = [...serializer.hasOne, ...serializer.hasMany];
|
|
5211
|
+
const linkage = await loadLinkage(model, unique, names);
|
|
5212
|
+
for (const item of unique) {
|
|
5213
|
+
const id = String(item.getPrimaryKey());
|
|
5214
|
+
const key = resourceKey({ id, type: item.resourceName });
|
|
5215
|
+
if (!included.has(key)) {
|
|
5216
|
+
included.set(
|
|
5217
|
+
key,
|
|
5218
|
+
await serializer.formatOne({
|
|
5219
|
+
item,
|
|
5220
|
+
domain,
|
|
5221
|
+
linkage: linkage.get(id),
|
|
5222
|
+
namespace
|
|
5223
|
+
})
|
|
5224
|
+
);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
for (const [name, children] of tree) {
|
|
5228
|
+
const opts = model.relationshipFor(name);
|
|
5229
|
+
if (!opts || !names.includes(name)) {
|
|
5230
|
+
continue;
|
|
5231
|
+
}
|
|
5232
|
+
const { model: next } = opts;
|
|
5233
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5234
|
+
linkage.forEach(({ [name]: value }) => {
|
|
5235
|
+
(Array.isArray(value) ? value : [value]).forEach((relatedId) => {
|
|
5236
|
+
if (relatedId != null) {
|
|
5237
|
+
ids.add(relatedId);
|
|
5238
|
+
}
|
|
5239
|
+
});
|
|
5240
|
+
});
|
|
5241
|
+
if (ids.size) {
|
|
5242
|
+
const attributes = this.serializerFor(
|
|
5243
|
+
next,
|
|
5244
|
+
namespace
|
|
5245
|
+
).attributes.filter((attr) => next.attributeNames.includes(attr));
|
|
5246
|
+
const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
|
|
5247
|
+
await this.addIncluded({
|
|
5248
|
+
domain,
|
|
5249
|
+
included,
|
|
5250
|
+
namespace,
|
|
5251
|
+
model: next,
|
|
5252
|
+
records: nextRecords,
|
|
5253
|
+
tree: children
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
/**
|
|
5259
|
+
* Resolve the Serializer for `model` in `namespace` (this Serializer's by
|
|
5260
|
+
* default), the way a namespaced Controller resolves its own:
|
|
5261
|
+
* `admin/comments` if it exists, otherwise the closest ancestor namespace's,
|
|
5262
|
+
* down to the root `comments` Serializer. Falls back to `model.serializer`
|
|
5263
|
+
* when this Serializer was not created by an application (e.g. in
|
|
5264
|
+
* isolation).
|
|
5265
|
+
*
|
|
5266
|
+
* Pass the request's namespace when there is one: a Serializer's own
|
|
5267
|
+
* namespace is the root one whenever it is a namespaced Controller's
|
|
5268
|
+
* fallback.
|
|
5269
|
+
*
|
|
5270
|
+
* @method serializerFor
|
|
5271
|
+
* @private
|
|
5272
|
+
*/
|
|
5273
|
+
serializerFor(model, namespace = this.namespace) {
|
|
5274
|
+
const { serializers } = this;
|
|
5275
|
+
if (serializers) {
|
|
5276
|
+
const key = posix2.join(namespace || ".", model.resourceName);
|
|
5277
|
+
const serializer = serializers.get(key) || closestAncestor(serializers, key);
|
|
5278
|
+
if (serializer) {
|
|
5279
|
+
return serializer;
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
return model.serializer;
|
|
5283
|
+
}
|
|
5284
|
+
/**
|
|
5285
|
+
* @private
|
|
5286
|
+
*/
|
|
5287
|
+
linkFor(domain, type, id, namespace = this.namespace) {
|
|
5288
|
+
if (namespace) {
|
|
5289
|
+
return `${domain}/${namespace}/${type}/${id}`;
|
|
5290
|
+
}
|
|
5291
|
+
return `${domain}/${type}/${id}`;
|
|
5292
|
+
}
|
|
4770
5293
|
};
|
|
5294
|
+
function resourceKey({ id, type }) {
|
|
5295
|
+
return `${type}:${id}`;
|
|
5296
|
+
}
|
|
4771
5297
|
var serializer_default = Serializer;
|
|
4772
5298
|
|
|
4773
5299
|
// src/packages/config/index.ts
|
|
@@ -4966,7 +5492,10 @@ function resource(action) {
|
|
|
4966
5492
|
data,
|
|
4967
5493
|
links,
|
|
4968
5494
|
domain,
|
|
4969
|
-
include
|
|
5495
|
+
include,
|
|
5496
|
+
// The request's namespace, not the serializer's: a namespaced
|
|
5497
|
+
// controller without its own serializer is given the root one.
|
|
5498
|
+
namespace
|
|
4970
5499
|
});
|
|
4971
5500
|
}
|
|
4972
5501
|
return data;
|
|
@@ -5047,6 +5576,7 @@ var ParameterTypeError = class extends TypeError {
|
|
|
5047
5576
|
Expected type '${type || "undefined"}' for parameter '${path}' but got
|
|
5048
5577
|
'${actual}'.
|
|
5049
5578
|
`);
|
|
5579
|
+
this.source = sourceFor(path);
|
|
5050
5580
|
}
|
|
5051
5581
|
};
|
|
5052
5582
|
var parameter_type_error_default = createServerError(ParameterTypeError, 400);
|
|
@@ -5059,6 +5589,7 @@ var ParameterValueError = class extends TypeError {
|
|
|
5059
5589
|
[${param.size ? Array.from(param.values()).join(", ") : ""}] but got
|
|
5060
5590
|
${String(actual)}.
|
|
5061
5591
|
`);
|
|
5592
|
+
this.source = sourceFor(param.path);
|
|
5062
5593
|
}
|
|
5063
5594
|
};
|
|
5064
5595
|
var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
@@ -5067,6 +5598,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
|
5067
5598
|
var InvalidParameterError = class extends TypeError {
|
|
5068
5599
|
constructor(path) {
|
|
5069
5600
|
super(`'${path}' is not a valid parameter for this resource.`);
|
|
5601
|
+
this.source = sourceFor(path);
|
|
5070
5602
|
}
|
|
5071
5603
|
};
|
|
5072
5604
|
var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
|
|
@@ -5082,6 +5614,7 @@ var ResourceMismatchError = class extends TypeError {
|
|
|
5082
5614
|
Expected '${String(expected)}' for parameter '${path}' but got
|
|
5083
5615
|
${String(normalized)}.
|
|
5084
5616
|
`);
|
|
5617
|
+
this.source = sourceFor(path);
|
|
5085
5618
|
}
|
|
5086
5619
|
};
|
|
5087
5620
|
var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
|
|
@@ -5090,14 +5623,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
|
|
|
5090
5623
|
var ParameterRequiredError = class extends TypeError {
|
|
5091
5624
|
constructor(path) {
|
|
5092
5625
|
super(`Missing required parameter '${path}'.`);
|
|
5626
|
+
this.source = sourceFor(path);
|
|
5093
5627
|
}
|
|
5094
5628
|
};
|
|
5095
5629
|
var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
|
|
5096
5630
|
|
|
5631
|
+
// src/packages/router/route/params/errors/client-generated-id-error.ts
|
|
5632
|
+
var ClientGeneratedIdError = class extends TypeError {
|
|
5633
|
+
constructor() {
|
|
5634
|
+
super("Client-generated IDs are not supported for this resource.");
|
|
5635
|
+
this.source = sourceFor("data.id");
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
|
|
5639
|
+
|
|
5640
|
+
// src/packages/router/route/params/errors/forbidden-parameter-error.ts
|
|
5641
|
+
var ForbiddenParameterError = class extends TypeError {
|
|
5642
|
+
constructor(path) {
|
|
5643
|
+
super(`Setting '${path}' is not supported for this resource.`);
|
|
5644
|
+
this.source = sourceFor(path);
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5647
|
+
var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
|
|
5648
|
+
|
|
5097
5649
|
// src/packages/router/route/params/errors/parameter-not-nullable-error.ts
|
|
5098
5650
|
var ParameterNotNullableError = class extends TypeError {
|
|
5099
5651
|
constructor({ path }) {
|
|
5100
5652
|
super(`Parameter '${path}' is not nullable.`);
|
|
5653
|
+
this.source = sourceFor(path);
|
|
5101
5654
|
}
|
|
5102
5655
|
};
|
|
5103
5656
|
var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
|
|
@@ -5226,13 +5779,14 @@ var Parameter = class extends set_default {
|
|
|
5226
5779
|
path,
|
|
5227
5780
|
type,
|
|
5228
5781
|
required: Boolean(required),
|
|
5229
|
-
sanitize: Boolean(sanitize)
|
|
5782
|
+
sanitize: Boolean(sanitize),
|
|
5783
|
+
restricted: values !== void 0
|
|
5230
5784
|
});
|
|
5231
5785
|
this.freeze();
|
|
5232
5786
|
}
|
|
5233
5787
|
validate(value) {
|
|
5234
5788
|
validateType2(this, value);
|
|
5235
|
-
if (this.
|
|
5789
|
+
if (this.restricted) {
|
|
5236
5790
|
return validateValue(this, value);
|
|
5237
5791
|
}
|
|
5238
5792
|
return value;
|
|
@@ -5251,6 +5805,17 @@ function getURLParams(dynamicSegments) {
|
|
|
5251
5805
|
]);
|
|
5252
5806
|
}
|
|
5253
5807
|
|
|
5808
|
+
// src/packages/router/route/params/parameter/forbidden-parameter.ts
|
|
5809
|
+
var ForbiddenParameter = class extends parameter_default {
|
|
5810
|
+
constructor(path) {
|
|
5811
|
+
super({ path });
|
|
5812
|
+
}
|
|
5813
|
+
validate() {
|
|
5814
|
+
throw new forbidden_parameter_error_default(this.path);
|
|
5815
|
+
}
|
|
5816
|
+
};
|
|
5817
|
+
var forbidden_parameter_default = ForbiddenParameter;
|
|
5818
|
+
|
|
5254
5819
|
// src/packages/router/route/params/utils/get-data-params.ts
|
|
5255
5820
|
function getIDParam({ model }) {
|
|
5256
5821
|
const primaryKeyColumn = model.columnFor(model.primaryKey);
|
|
@@ -5303,16 +5868,48 @@ function getRelationshipsParam({
|
|
|
5303
5868
|
model,
|
|
5304
5869
|
params
|
|
5305
5870
|
}) {
|
|
5871
|
+
const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
|
|
5872
|
+
key,
|
|
5873
|
+
new forbidden_parameter_default(`data.relationships.${key}`)
|
|
5874
|
+
]);
|
|
5306
5875
|
return [
|
|
5307
5876
|
"relationships",
|
|
5308
5877
|
new parameter_group_default(
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5878
|
+
[
|
|
5879
|
+
...params.reduce((group, param) => {
|
|
5880
|
+
const path = `data.relationships.${param}`;
|
|
5881
|
+
const opts = model.relationshipFor(param);
|
|
5882
|
+
if (!opts) {
|
|
5883
|
+
return group;
|
|
5884
|
+
}
|
|
5885
|
+
if (opts.type === "hasMany") {
|
|
5886
|
+
return [
|
|
5887
|
+
...group,
|
|
5888
|
+
[
|
|
5889
|
+
param,
|
|
5890
|
+
new parameter_group_default(
|
|
5891
|
+
[
|
|
5892
|
+
[
|
|
5893
|
+
"data",
|
|
5894
|
+
new parameter_default({
|
|
5895
|
+
type: "array",
|
|
5896
|
+
path: `${path}.data`,
|
|
5897
|
+
required: true
|
|
5898
|
+
})
|
|
5899
|
+
]
|
|
5900
|
+
],
|
|
5901
|
+
{
|
|
5902
|
+
path
|
|
5903
|
+
}
|
|
5904
|
+
)
|
|
5905
|
+
]
|
|
5906
|
+
];
|
|
5907
|
+
}
|
|
5908
|
+
const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
|
|
5909
|
+
let primaryKeyType = "number";
|
|
5910
|
+
if (primaryKeyColumn) {
|
|
5911
|
+
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5912
|
+
}
|
|
5316
5913
|
return [
|
|
5317
5914
|
...group,
|
|
5318
5915
|
[
|
|
@@ -5321,11 +5918,32 @@ function getRelationshipsParam({
|
|
|
5321
5918
|
[
|
|
5322
5919
|
[
|
|
5323
5920
|
"data",
|
|
5324
|
-
new
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5921
|
+
new parameter_group_default(
|
|
5922
|
+
[
|
|
5923
|
+
[
|
|
5924
|
+
"id",
|
|
5925
|
+
new parameter_default({
|
|
5926
|
+
type: primaryKeyType,
|
|
5927
|
+
path: `${path}.data.id`,
|
|
5928
|
+
required: true
|
|
5929
|
+
})
|
|
5930
|
+
],
|
|
5931
|
+
[
|
|
5932
|
+
"type",
|
|
5933
|
+
new parameter_default({
|
|
5934
|
+
type: "string",
|
|
5935
|
+
path: `${path}.data.type`,
|
|
5936
|
+
values: [opts.model.resourceName],
|
|
5937
|
+
required: true
|
|
5938
|
+
})
|
|
5939
|
+
]
|
|
5940
|
+
],
|
|
5941
|
+
{
|
|
5942
|
+
type: "array",
|
|
5943
|
+
path: `${path}.data`,
|
|
5944
|
+
required: true
|
|
5945
|
+
}
|
|
5946
|
+
)
|
|
5329
5947
|
]
|
|
5330
5948
|
],
|
|
5331
5949
|
{
|
|
@@ -5334,55 +5952,9 @@ function getRelationshipsParam({
|
|
|
5334
5952
|
)
|
|
5335
5953
|
]
|
|
5336
5954
|
];
|
|
5337
|
-
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
if (primaryKeyColumn) {
|
|
5341
|
-
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5342
|
-
}
|
|
5343
|
-
return [
|
|
5344
|
-
...group,
|
|
5345
|
-
[
|
|
5346
|
-
param,
|
|
5347
|
-
new parameter_group_default(
|
|
5348
|
-
[
|
|
5349
|
-
[
|
|
5350
|
-
"data",
|
|
5351
|
-
new parameter_group_default(
|
|
5352
|
-
[
|
|
5353
|
-
[
|
|
5354
|
-
"id",
|
|
5355
|
-
new parameter_default({
|
|
5356
|
-
type: primaryKeyType,
|
|
5357
|
-
path: `${path}.data.id`,
|
|
5358
|
-
required: true
|
|
5359
|
-
})
|
|
5360
|
-
],
|
|
5361
|
-
[
|
|
5362
|
-
"type",
|
|
5363
|
-
new parameter_default({
|
|
5364
|
-
type: "string",
|
|
5365
|
-
path: `${path}.data.type`,
|
|
5366
|
-
values: [opts.model.resourceName],
|
|
5367
|
-
required: true
|
|
5368
|
-
})
|
|
5369
|
-
]
|
|
5370
|
-
],
|
|
5371
|
-
{
|
|
5372
|
-
type: "array",
|
|
5373
|
-
path: `${path}.data`,
|
|
5374
|
-
required: true
|
|
5375
|
-
}
|
|
5376
|
-
)
|
|
5377
|
-
]
|
|
5378
|
-
],
|
|
5379
|
-
{
|
|
5380
|
-
path
|
|
5381
|
-
}
|
|
5382
|
-
)
|
|
5383
|
-
]
|
|
5384
|
-
];
|
|
5385
|
-
}, []),
|
|
5955
|
+
}, []),
|
|
5956
|
+
...forbidden
|
|
5957
|
+
],
|
|
5386
5958
|
{
|
|
5387
5959
|
path: "data.relationships"
|
|
5388
5960
|
}
|
|
@@ -5490,10 +6062,9 @@ function getFilterParam({ filter }) {
|
|
|
5490
6062
|
)
|
|
5491
6063
|
];
|
|
5492
6064
|
}
|
|
5493
|
-
function getFieldsParam({
|
|
5494
|
-
model,
|
|
5495
|
-
|
|
5496
|
-
}) {
|
|
6065
|
+
function getFieldsParam(controller) {
|
|
6066
|
+
const { model, serializer } = controller;
|
|
6067
|
+
const { hasOne, hasMany, attributes } = serializer;
|
|
5497
6068
|
const relationships = [...hasOne, ...hasMany];
|
|
5498
6069
|
return [
|
|
5499
6070
|
"fields",
|
|
@@ -5522,7 +6093,7 @@ function getFieldsParam({
|
|
|
5522
6093
|
sanitize: true,
|
|
5523
6094
|
values: [
|
|
5524
6095
|
opts.model.primaryKey,
|
|
5525
|
-
...opts.model.
|
|
6096
|
+
...controller.serializerFor(opts.model).attributes
|
|
5526
6097
|
]
|
|
5527
6098
|
})
|
|
5528
6099
|
]
|
|
@@ -5540,16 +6111,30 @@ function getFieldsParam({
|
|
|
5540
6111
|
)
|
|
5541
6112
|
];
|
|
5542
6113
|
}
|
|
5543
|
-
function getIncludeParam({
|
|
5544
|
-
|
|
5545
|
-
}
|
|
6114
|
+
function getIncludeParam(controller) {
|
|
6115
|
+
const { model, maxIncludeDepth, serializer } = controller;
|
|
6116
|
+
const { hasOne, hasMany } = serializer;
|
|
5546
6117
|
const relationships = [...hasOne, ...hasMany];
|
|
5547
6118
|
return [
|
|
5548
6119
|
"include",
|
|
5549
6120
|
new parameter_default({
|
|
5550
6121
|
path: "include",
|
|
5551
6122
|
type: "array",
|
|
5552
|
-
|
|
6123
|
+
// Every top level name stays allowed (as before), plus the nested paths
|
|
6124
|
+
// (`comments.user`) reachable through each related serializer in the
|
|
6125
|
+
// controller's namespace (at every level, even below a root fallback
|
|
6126
|
+
// serializer), down to the controller's `maxIncludeDepth`.
|
|
6127
|
+
values: Array.from(
|
|
6128
|
+
/* @__PURE__ */ new Set([
|
|
6129
|
+
...relationships,
|
|
6130
|
+
...enumerateIncludePaths(
|
|
6131
|
+
model,
|
|
6132
|
+
relationships,
|
|
6133
|
+
maxIncludeDepth,
|
|
6134
|
+
(related) => controller.serializerFor(related)
|
|
6135
|
+
)
|
|
6136
|
+
])
|
|
6137
|
+
)
|
|
5553
6138
|
})
|
|
5554
6139
|
];
|
|
5555
6140
|
}
|
|
@@ -5587,6 +6172,15 @@ function getCollectionQueryParams(controller) {
|
|
|
5587
6172
|
return getCustomParams(controller);
|
|
5588
6173
|
}
|
|
5589
6174
|
|
|
6175
|
+
// src/packages/router/route/params/utils/validate-client-id.ts
|
|
6176
|
+
function validateClientId(params) {
|
|
6177
|
+
const { data } = params;
|
|
6178
|
+
if (data && typeof data === "object" && Reflect.has(data, "id")) {
|
|
6179
|
+
throw new client_generated_id_error_default();
|
|
6180
|
+
}
|
|
6181
|
+
return true;
|
|
6182
|
+
}
|
|
6183
|
+
|
|
5590
6184
|
// src/packages/router/route/params/utils/validate-resource-id.ts
|
|
5591
6185
|
function validateResourceId({
|
|
5592
6186
|
params: {
|
|
@@ -5752,6 +6346,9 @@ var Route = class extends set_default {
|
|
|
5752
6346
|
...req.params,
|
|
5753
6347
|
...this.parseParams(req.url.params)
|
|
5754
6348
|
};
|
|
6349
|
+
if (this.action === "create" && req.method === "POST") {
|
|
6350
|
+
validateClientId(params);
|
|
6351
|
+
}
|
|
5755
6352
|
if (req.method !== "OPTIONS") {
|
|
5756
6353
|
params = this.params.validate(params);
|
|
5757
6354
|
}
|
|
@@ -6067,7 +6664,7 @@ var router_default = Router;
|
|
|
6067
6664
|
import { join as joinPath2 } from "path";
|
|
6068
6665
|
|
|
6069
6666
|
// src/packages/loader/utils/format-key.ts
|
|
6070
|
-
import { dasherize as
|
|
6667
|
+
import { dasherize as dasherize6 } from "inflection";
|
|
6071
6668
|
var NAMESPACE_DELIMITER = /\$-/g;
|
|
6072
6669
|
function formatKey(key, formatter) {
|
|
6073
6670
|
return chain(key).pipe((str) => {
|
|
@@ -6075,7 +6672,7 @@ function formatKey(key, formatter) {
|
|
|
6075
6672
|
return formatter(str);
|
|
6076
6673
|
}
|
|
6077
6674
|
return str;
|
|
6078
|
-
}).pipe(underscore).pipe(
|
|
6675
|
+
}).pipe(underscore).pipe(dasherize6).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
|
|
6079
6676
|
}
|
|
6080
6677
|
|
|
6081
6678
|
// src/packages/loader/utils/bundle-for.ts
|
|
@@ -6147,26 +6744,10 @@ function bundleFor(path) {
|
|
|
6147
6744
|
return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
|
|
6148
6745
|
}
|
|
6149
6746
|
|
|
6150
|
-
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
6151
|
-
import { posix } from "path";
|
|
6152
|
-
function closestAncestor(source, key) {
|
|
6153
|
-
const name = posix.basename(key);
|
|
6154
|
-
let namespace = posix.dirname(key);
|
|
6155
|
-
if (namespace === ".") {
|
|
6156
|
-
return source.get(name);
|
|
6157
|
-
}
|
|
6158
|
-
namespace = posix.dirname(namespace);
|
|
6159
|
-
const ancestor = source.get(posix.join(namespace, name));
|
|
6160
|
-
if (ancestor) {
|
|
6161
|
-
return ancestor;
|
|
6162
|
-
}
|
|
6163
|
-
return closestAncestor(source, posix.join(posix.dirname(namespace), name));
|
|
6164
|
-
}
|
|
6165
|
-
|
|
6166
6747
|
// src/packages/loader/resolver/utils/closest-child.ts
|
|
6167
|
-
import { posix as
|
|
6748
|
+
import { posix as posix3 } from "path";
|
|
6168
6749
|
function closestChild(source, key) {
|
|
6169
|
-
const [[, result] = []] = Array.from(source).map(([path, value]) => [
|
|
6750
|
+
const [[, result] = []] = Array.from(source).map(([path, value]) => [posix3.basename(path), value]).filter(([resource2]) => key === resource2);
|
|
6170
6751
|
return result;
|
|
6171
6752
|
}
|
|
6172
6753
|
|
|
@@ -6189,7 +6770,7 @@ function resolve(group) {
|
|
|
6189
6770
|
}
|
|
6190
6771
|
|
|
6191
6772
|
// src/packages/loader/builder/utils/create-parent-builder.ts
|
|
6192
|
-
import { posix as
|
|
6773
|
+
import { posix as posix4 } from "path";
|
|
6193
6774
|
|
|
6194
6775
|
// src/packages/loader/builder/utils/sort-by-namespace.ts
|
|
6195
6776
|
function sortByNamespace([a], [b]) {
|
|
@@ -6210,7 +6791,7 @@ function createParentBuilder(construct) {
|
|
|
6210
6791
|
let grandparent = null;
|
|
6211
6792
|
if (key !== "root") {
|
|
6212
6793
|
const found = result.find((namespace) => {
|
|
6213
|
-
const dirname =
|
|
6794
|
+
const dirname = posix4.dirname(key);
|
|
6214
6795
|
if (namespace.key === "root") {
|
|
6215
6796
|
return dirname === ".";
|
|
6216
6797
|
}
|
|
@@ -6280,13 +6861,13 @@ function normalizePort(port) {
|
|
|
6280
6861
|
}
|
|
6281
6862
|
|
|
6282
6863
|
// src/packages/application/utils/create-controller.ts
|
|
6283
|
-
import { posix as
|
|
6864
|
+
import { posix as posix5 } from "path";
|
|
6284
6865
|
function createController(constructor, opts) {
|
|
6285
6866
|
const { key, store, serializers } = opts;
|
|
6286
|
-
const namespace =
|
|
6867
|
+
const namespace = posix5.dirname(key).replace(".", "");
|
|
6287
6868
|
let { parent } = opts;
|
|
6288
6869
|
let model = tryCatchSync(
|
|
6289
|
-
() => store.modelFor(
|
|
6870
|
+
() => store.modelFor(posix5.basename(key))
|
|
6290
6871
|
);
|
|
6291
6872
|
let serializer = serializers.get(key);
|
|
6292
6873
|
if (!model) {
|
|
@@ -6342,13 +6923,13 @@ function createController(constructor, opts) {
|
|
|
6342
6923
|
}
|
|
6343
6924
|
|
|
6344
6925
|
// src/packages/application/utils/create-serializer.ts
|
|
6345
|
-
import { posix as
|
|
6926
|
+
import { posix as posix6 } from "path";
|
|
6346
6927
|
function createSerializer(constructor, opts) {
|
|
6347
6928
|
const { key, store } = opts;
|
|
6348
|
-
const namespace =
|
|
6929
|
+
const namespace = posix6.dirname(key).replace(".", "");
|
|
6349
6930
|
let { parent } = opts;
|
|
6350
6931
|
let model = tryCatchSync(
|
|
6351
|
-
() => store.modelFor(
|
|
6932
|
+
() => store.modelFor(posix6.basename(key))
|
|
6352
6933
|
);
|
|
6353
6934
|
if (!model) {
|
|
6354
6935
|
model = null;
|
|
@@ -6372,6 +6953,85 @@ function createSerializer(constructor, opts) {
|
|
|
6372
6953
|
return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
|
|
6373
6954
|
}
|
|
6374
6955
|
|
|
6956
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6957
|
+
import { posix as posix7 } from "path";
|
|
6958
|
+
|
|
6959
|
+
// src/errors/namespaced-serializer-missing-error.ts
|
|
6960
|
+
var NamespacedSerializerMissingError = class extends ReferenceError {
|
|
6961
|
+
/**
|
|
6962
|
+
* @param missing - Each missing serializer key (`admin/comments`) with an
|
|
6963
|
+
* example of how it is reached (`admin/posts?include=comments`).
|
|
6964
|
+
*/
|
|
6965
|
+
constructor(missing) {
|
|
6966
|
+
const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
|
|
6967
|
+
super(
|
|
6968
|
+
[
|
|
6969
|
+
"Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
|
|
6970
|
+
...lines
|
|
6971
|
+
].join("\n")
|
|
6972
|
+
);
|
|
6973
|
+
}
|
|
6974
|
+
};
|
|
6975
|
+
var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
|
|
6976
|
+
|
|
6977
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6978
|
+
var distance = (via) => {
|
|
6979
|
+
const [, include] = via.split("?include=");
|
|
6980
|
+
return include ? include.split(".").length : 0;
|
|
6981
|
+
};
|
|
6982
|
+
function validateNamespacedSerializers(controllers, serializers) {
|
|
6983
|
+
const missing = /* @__PURE__ */ new Map();
|
|
6984
|
+
controllers.forEach((controller, key) => {
|
|
6985
|
+
const { namespace, model } = controller;
|
|
6986
|
+
if (!namespace || !model) {
|
|
6987
|
+
return;
|
|
6988
|
+
}
|
|
6989
|
+
const application = controllers.get(posix7.join(namespace, "application"));
|
|
6990
|
+
if (!application || application.serializerFallback !== false) {
|
|
6991
|
+
return;
|
|
6992
|
+
}
|
|
6993
|
+
const require2 = (type, via) => {
|
|
6994
|
+
const serializerKey = posix7.join(namespace, type);
|
|
6995
|
+
const serializer = serializers.get(serializerKey);
|
|
6996
|
+
if (!serializer) {
|
|
6997
|
+
const known = missing.get(serializerKey);
|
|
6998
|
+
if (known === void 0 || distance(via) < distance(known)) {
|
|
6999
|
+
missing.set(serializerKey, via);
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
return serializer;
|
|
7003
|
+
};
|
|
7004
|
+
const own = require2(model.resourceName, key);
|
|
7005
|
+
if (!own) {
|
|
7006
|
+
return;
|
|
7007
|
+
}
|
|
7008
|
+
const depth = Math.max(controller.maxIncludeDepth, 1);
|
|
7009
|
+
let level = [
|
|
7010
|
+
[model, own, []]
|
|
7011
|
+
];
|
|
7012
|
+
for (let current = 1; current <= depth && level.length; current += 1) {
|
|
7013
|
+
const next = [];
|
|
7014
|
+
level.forEach(([parent, serializer, path]) => {
|
|
7015
|
+
[...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
|
|
7016
|
+
const opts = parent.relationshipFor(name);
|
|
7017
|
+
if (!opts) {
|
|
7018
|
+
return;
|
|
7019
|
+
}
|
|
7020
|
+
const nextPath = [...path, name];
|
|
7021
|
+
const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
|
|
7022
|
+
if (related) {
|
|
7023
|
+
next.push([opts.model, related, nextPath]);
|
|
7024
|
+
}
|
|
7025
|
+
});
|
|
7026
|
+
});
|
|
7027
|
+
level = next;
|
|
7028
|
+
}
|
|
7029
|
+
});
|
|
7030
|
+
if (missing.size) {
|
|
7031
|
+
throw new namespaced_serializer_missing_error_default(missing);
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
|
|
6375
7035
|
// src/packages/application/initialize.ts
|
|
6376
7036
|
async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
|
|
6377
7037
|
const load = createLoader(path);
|
|
@@ -6394,6 +7054,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6394
7054
|
parent
|
|
6395
7055
|
})
|
|
6396
7056
|
);
|
|
7057
|
+
serializers.forEach((serializer) => {
|
|
7058
|
+
Reflect.defineProperty(serializer, "serializers", {
|
|
7059
|
+
value: serializers,
|
|
7060
|
+
writable: false,
|
|
7061
|
+
enumerable: false,
|
|
7062
|
+
configurable: false
|
|
7063
|
+
});
|
|
7064
|
+
});
|
|
6397
7065
|
models.forEach((model) => {
|
|
6398
7066
|
Reflect.defineProperty(model, "serializer", {
|
|
6399
7067
|
value: closestChild(serializers, model.resourceName),
|
|
@@ -6419,6 +7087,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6419
7087
|
configurable: false
|
|
6420
7088
|
});
|
|
6421
7089
|
});
|
|
7090
|
+
validateNamespacedSerializers(controllers, serializers);
|
|
6422
7091
|
const ApplicationController = controllers.get("application");
|
|
6423
7092
|
if (!ApplicationController) {
|
|
6424
7093
|
throw new controller_missing_error_default("application");
|