lumen-framework 3.0.3 → 3.1.0
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 +916 -257
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +907 -248
- 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) {
|
|
@@ -1848,7 +1864,9 @@ async function get(owner, key) {
|
|
|
1848
1864
|
let { foreignKey } = opts;
|
|
1849
1865
|
value = owner.currentChangeSet.get(key);
|
|
1850
1866
|
foreignKey = camelize5(foreignKey, true);
|
|
1851
|
-
if (!value) {
|
|
1867
|
+
if (!value && owner.absentRelationships.has(key)) {
|
|
1868
|
+
value = null;
|
|
1869
|
+
} else if (!value) {
|
|
1852
1870
|
switch (type) {
|
|
1853
1871
|
case "hasOne":
|
|
1854
1872
|
value = await getHasOne(owner, {
|
|
@@ -2510,13 +2528,37 @@ async function initializeClass({
|
|
|
2510
2528
|
return model;
|
|
2511
2529
|
}
|
|
2512
2530
|
|
|
2531
|
+
// src/packages/server/utils/source-for.ts
|
|
2532
|
+
import { dasherize as dasherize3 } from "inflection";
|
|
2533
|
+
function memberName(key) {
|
|
2534
|
+
return dasherize3(underscore(key));
|
|
2535
|
+
}
|
|
2536
|
+
function escapePointer(segment) {
|
|
2537
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2538
|
+
}
|
|
2539
|
+
function sourceFor(path) {
|
|
2540
|
+
const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
|
|
2541
|
+
if (!root) {
|
|
2542
|
+
return {};
|
|
2543
|
+
} else if (root === "data") {
|
|
2544
|
+
return {
|
|
2545
|
+
pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
return {
|
|
2549
|
+
parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2513
2553
|
// src/packages/database/validation/errors/validation-error.ts
|
|
2514
2554
|
var ValidationError = class extends Error {
|
|
2515
|
-
constructor(key
|
|
2516
|
-
super(`Validation failed for ${key}
|
|
2555
|
+
constructor(key) {
|
|
2556
|
+
super(`Validation failed for ${key}.`);
|
|
2557
|
+
this.key = key;
|
|
2558
|
+
this.source = sourceFor(`data.attributes.${key}`);
|
|
2517
2559
|
}
|
|
2518
2560
|
};
|
|
2519
|
-
var validation_error_default = ValidationError;
|
|
2561
|
+
var validation_error_default = createServerError(ValidationError, 422);
|
|
2520
2562
|
|
|
2521
2563
|
// src/packages/database/validation/index.ts
|
|
2522
2564
|
var Validation = class {
|
|
@@ -2556,12 +2598,32 @@ function validate(instance) {
|
|
|
2556
2598
|
validator: Reflect.get(instance.constructor.validates, key)
|
|
2557
2599
|
})).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
|
|
2558
2600
|
if (!validation.isValid()) {
|
|
2559
|
-
throw new validation_error_default(validation.key
|
|
2601
|
+
throw new validation_error_default(validation.key);
|
|
2560
2602
|
}
|
|
2561
2603
|
return result;
|
|
2562
2604
|
}, true);
|
|
2563
2605
|
}
|
|
2564
2606
|
|
|
2607
|
+
// src/packages/database/model/utils/process-write-error.ts
|
|
2608
|
+
function driverMessage(message) {
|
|
2609
|
+
const index = message.lastIndexOf(" - ");
|
|
2610
|
+
return index >= 0 ? message.slice(index + 3) : message;
|
|
2611
|
+
}
|
|
2612
|
+
function processWriteError(err) {
|
|
2613
|
+
if (!(err instanceof Error)) {
|
|
2614
|
+
return err;
|
|
2615
|
+
}
|
|
2616
|
+
const { code } = err;
|
|
2617
|
+
const { message } = err;
|
|
2618
|
+
if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
|
|
2619
|
+
return new unique_constraint_error_default(driverMessage(message));
|
|
2620
|
+
}
|
|
2621
|
+
return err;
|
|
2622
|
+
}
|
|
2623
|
+
function rethrowWriteError(err) {
|
|
2624
|
+
throw processWriteError(err);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2565
2627
|
// src/packages/database/model/utils/run-hooks.ts
|
|
2566
2628
|
function runHooks(record, trx, ...hooks) {
|
|
2567
2629
|
return hooks.filter((hook) => Boolean(hook)).reduce(
|
|
@@ -2601,6 +2663,12 @@ var Model = class {
|
|
|
2601
2663
|
writable: false,
|
|
2602
2664
|
enumerable: false,
|
|
2603
2665
|
configurable: false
|
|
2666
|
+
},
|
|
2667
|
+
absentRelationships: {
|
|
2668
|
+
value: /* @__PURE__ */ new Set(),
|
|
2669
|
+
writable: false,
|
|
2670
|
+
enumerable: false,
|
|
2671
|
+
configurable: false
|
|
2604
2672
|
}
|
|
2605
2673
|
});
|
|
2606
2674
|
const {
|
|
@@ -2947,9 +3015,9 @@ var Model = class {
|
|
|
2947
3015
|
return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
|
|
2948
3016
|
};
|
|
2949
3017
|
if (transaction) {
|
|
2950
|
-
return run(transaction);
|
|
3018
|
+
return run(transaction).catch(rethrowWriteError);
|
|
2951
3019
|
}
|
|
2952
|
-
return this.transaction(run);
|
|
3020
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
2953
3021
|
}
|
|
2954
3022
|
/**
|
|
2955
3023
|
* Permanently delete the instance from the database.
|
|
@@ -3069,9 +3137,9 @@ var Model = class {
|
|
|
3069
3137
|
return createTransactionResultProxy(instance, true);
|
|
3070
3138
|
};
|
|
3071
3139
|
if (transaction) {
|
|
3072
|
-
return run(transaction);
|
|
3140
|
+
return run(transaction).catch(rethrowWriteError);
|
|
3073
3141
|
}
|
|
3074
|
-
return this.transaction(run);
|
|
3142
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
3075
3143
|
}
|
|
3076
3144
|
/**
|
|
3077
3145
|
* Specify the transaction object to use for following save, update, or
|
|
@@ -3458,7 +3526,7 @@ var TRUE = /^true$/;
|
|
|
3458
3526
|
var BRACKETS = /(?:\[])/g;
|
|
3459
3527
|
|
|
3460
3528
|
// src/utils/transform-keys.ts
|
|
3461
|
-
import { camelize as camelize7, dasherize as
|
|
3529
|
+
import { camelize as camelize7, dasherize as dasherize4 } from "inflection";
|
|
3462
3530
|
function transformKeys(source, transformer, deep = false) {
|
|
3463
3531
|
if (Array.isArray(source)) {
|
|
3464
3532
|
return source.slice(0);
|
|
@@ -3489,7 +3557,7 @@ function camelizeKeys(source, deep) {
|
|
|
3489
3557
|
function dasherizeKeys(source, deep) {
|
|
3490
3558
|
return transformKeys(
|
|
3491
3559
|
source,
|
|
3492
|
-
(key) =>
|
|
3560
|
+
(key) => dasherize4(underscore(key)),
|
|
3493
3561
|
deep
|
|
3494
3562
|
);
|
|
3495
3563
|
}
|
|
@@ -3798,6 +3866,10 @@ function dataFor(status, err) {
|
|
|
3798
3866
|
if (title) {
|
|
3799
3867
|
errData.title = title;
|
|
3800
3868
|
}
|
|
3869
|
+
const source = err?.source;
|
|
3870
|
+
if (source && (source.pointer || source.parameter)) {
|
|
3871
|
+
errData.source = source;
|
|
3872
|
+
}
|
|
3801
3873
|
if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
|
|
3802
3874
|
errData.detail = err.message.replace(/^\[public\]/gi, "");
|
|
3803
3875
|
}
|
|
@@ -4020,9 +4092,10 @@ function merge(dest, source) {
|
|
|
4020
4092
|
}
|
|
4021
4093
|
|
|
4022
4094
|
// src/packages/controller/utils/params-to-query.ts
|
|
4023
|
-
function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
4095
|
+
function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
|
|
4024
4096
|
const relationships = entries(model.relationships);
|
|
4025
4097
|
const includedFields = omit(fields, model.resourceName);
|
|
4098
|
+
const included = include && include.map((path) => path.split(".")[0]);
|
|
4026
4099
|
let query = {
|
|
4027
4100
|
id,
|
|
4028
4101
|
filter,
|
|
@@ -4065,9 +4138,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4065
4138
|
if (!value.includes(relationship.model.primaryKey)) {
|
|
4066
4139
|
value = [relationship.model.primaryKey, ...value];
|
|
4067
4140
|
}
|
|
4068
|
-
if (
|
|
4069
|
-
value = [...value, ...relationship.model.
|
|
4070
|
-
} else if (!
|
|
4141
|
+
if (included && value.length === 1 && included.includes(name)) {
|
|
4142
|
+
value = [...value, ...serializerFor(relationship.model).attributes];
|
|
4143
|
+
} else if (!included && value.length > 1) {
|
|
4071
4144
|
value = value.slice(0, 1);
|
|
4072
4145
|
}
|
|
4073
4146
|
return {
|
|
@@ -4082,18 +4155,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4082
4155
|
}
|
|
4083
4156
|
|
|
4084
4157
|
// src/packages/controller/utils/find-one.ts
|
|
4085
|
-
function findOne(model, req) {
|
|
4158
|
+
function findOne(model, req, serializerFor) {
|
|
4086
4159
|
const params = merge(req.defaultParams, req.params);
|
|
4087
|
-
const { id, select, include } = paramsToQuery(model, params);
|
|
4160
|
+
const { id, select, include } = paramsToQuery(model, params, serializerFor);
|
|
4088
4161
|
return model.find(id).select(...select).include(include);
|
|
4089
4162
|
}
|
|
4090
4163
|
|
|
4091
4164
|
// src/packages/controller/utils/find-many.ts
|
|
4092
|
-
function findMany(model, req) {
|
|
4165
|
+
function findMany(model, req, serializerFor) {
|
|
4093
4166
|
const params = merge(req.defaultParams, req.params);
|
|
4094
4167
|
const { sort, page, limit, select, filter, include } = paramsToQuery(
|
|
4095
4168
|
model,
|
|
4096
|
-
params
|
|
4169
|
+
params,
|
|
4170
|
+
serializerFor
|
|
4097
4171
|
);
|
|
4098
4172
|
return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
|
|
4099
4173
|
}
|
|
@@ -4123,6 +4197,53 @@ function resolveRelationships(model, relationships = {}) {
|
|
|
4123
4197
|
);
|
|
4124
4198
|
}
|
|
4125
4199
|
|
|
4200
|
+
// src/packages/controller/errors/related-record-not-found-error.ts
|
|
4201
|
+
var RelatedRecordNotFoundError = class extends Error {
|
|
4202
|
+
constructor({ name, primaryKey }, primaryKeyValue, path) {
|
|
4203
|
+
super(
|
|
4204
|
+
`Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
|
|
4205
|
+
);
|
|
4206
|
+
this.source = sourceFor(path);
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
|
|
4210
|
+
|
|
4211
|
+
// src/packages/controller/utils/validate-relationships.ts
|
|
4212
|
+
async function validateRelationships(model, relationships = {}) {
|
|
4213
|
+
await Promise.all(
|
|
4214
|
+
entries(relationships).map(async ([key, value]) => {
|
|
4215
|
+
const opts = model.relationshipFor(key);
|
|
4216
|
+
const { data = null } = value || {};
|
|
4217
|
+
if (!opts || !data) {
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
const { model: related } = opts;
|
|
4221
|
+
const isMany = Array.isArray(data);
|
|
4222
|
+
const ids = (isMany ? data : [data]).map(
|
|
4223
|
+
(item) => Reflect.get(Object(item), "id")
|
|
4224
|
+
);
|
|
4225
|
+
if (!ids.length) {
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
const column = related.columnNameFor(related.primaryKey) || "id";
|
|
4229
|
+
const rows = await related.table().whereIn(
|
|
4230
|
+
`${related.tableName}.${column}`,
|
|
4231
|
+
ids.filter((id) => id != null)
|
|
4232
|
+
).select(`${related.tableName}.${column}`);
|
|
4233
|
+
const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
|
|
4234
|
+
const missing = ids.findIndex((id) => !found.has(String(id)));
|
|
4235
|
+
if (missing >= 0) {
|
|
4236
|
+
const path = `data.relationships.${key}.data`;
|
|
4237
|
+
throw new related_record_not_found_error_default(
|
|
4238
|
+
related,
|
|
4239
|
+
ids[missing],
|
|
4240
|
+
isMany ? `${path}.${missing}` : path
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
})
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4126
4247
|
// src/packages/controller/constants.ts
|
|
4127
4248
|
var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
|
|
4128
4249
|
|
|
@@ -4308,6 +4429,74 @@ var Controller = class {
|
|
|
4308
4429
|
* @public
|
|
4309
4430
|
*/
|
|
4310
4431
|
defaultPerPage = 25;
|
|
4432
|
+
/**
|
|
4433
|
+
* How many relationships deep an `?include` path may go on this
|
|
4434
|
+
* controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
|
|
4435
|
+
* only direct relationships (`comments`) can be included. Paths deeper than
|
|
4436
|
+
* this are rejected with `400 Bad Request`.
|
|
4437
|
+
*
|
|
4438
|
+
* Set it on `ApplicationController` to change it for the whole app, or on a
|
|
4439
|
+
* single controller to override it there.
|
|
4440
|
+
*
|
|
4441
|
+
* ```javascript
|
|
4442
|
+
* class ApplicationController extends Controller {
|
|
4443
|
+
* maxIncludeDepth = 2;
|
|
4444
|
+
* }
|
|
4445
|
+
* ```
|
|
4446
|
+
*
|
|
4447
|
+
* Every allowed path is enumerated up front from the serializers'
|
|
4448
|
+
* relationships, and each nested level costs its own queries per request, so
|
|
4449
|
+
* keep this small.
|
|
4450
|
+
*
|
|
4451
|
+
* @property maxIncludeDepth
|
|
4452
|
+
* @type {Number}
|
|
4453
|
+
* @default 3
|
|
4454
|
+
* @public
|
|
4455
|
+
*/
|
|
4456
|
+
maxIncludeDepth = 3;
|
|
4457
|
+
/**
|
|
4458
|
+
* Whether a namespace may fall back to the root Serializer of a type it has
|
|
4459
|
+
* no Serializer for. Read from a namespace's `ApplicationController` and
|
|
4460
|
+
* applies to the whole namespace.
|
|
4461
|
+
*
|
|
4462
|
+
* By default `app/controllers/admin/comments.js` without an
|
|
4463
|
+
* `app/serializers/admin/comments.js` — or an included type without one —
|
|
4464
|
+
* is serialized by the root Serializer, with every attribute and
|
|
4465
|
+
* relationship it declares. For a namespace that must only expose what it
|
|
4466
|
+
* declares itself, turn the fallback off:
|
|
4467
|
+
*
|
|
4468
|
+
* ```javascript
|
|
4469
|
+
* // app/controllers/admin/application.js
|
|
4470
|
+
* class AdminApplicationController extends ApplicationController {
|
|
4471
|
+
* serializerFallback = false;
|
|
4472
|
+
* }
|
|
4473
|
+
* ```
|
|
4474
|
+
*
|
|
4475
|
+
* The application then refuses to boot while any type the namespace can
|
|
4476
|
+
* serialize or `include` (down to each controller's `maxIncludeDepth`) has
|
|
4477
|
+
* no Serializer in that namespace, listing each missing one.
|
|
4478
|
+
*
|
|
4479
|
+
* @property serializerFallback
|
|
4480
|
+
* @type {Boolean}
|
|
4481
|
+
* @default true
|
|
4482
|
+
* @public
|
|
4483
|
+
*/
|
|
4484
|
+
serializerFallback = true;
|
|
4485
|
+
/**
|
|
4486
|
+
* The Serializer to serialize (and validate, and load) related resources of
|
|
4487
|
+
* this Controller's responses with: the related model's Serializer in this
|
|
4488
|
+
* Controller's namespace, falling back to the root one.
|
|
4489
|
+
*
|
|
4490
|
+
* Always this Controller's namespace — not its Serializer's, which is the
|
|
4491
|
+
* root one when the namespace has no Serializer for this resource.
|
|
4492
|
+
*
|
|
4493
|
+
* @method serializerFor
|
|
4494
|
+
* @private
|
|
4495
|
+
*/
|
|
4496
|
+
serializerFor(model) {
|
|
4497
|
+
const { serializer, namespace } = this;
|
|
4498
|
+
return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
|
|
4499
|
+
}
|
|
4311
4500
|
constructor({ model, namespace, serializer }) {
|
|
4312
4501
|
Object.assign(this, {
|
|
4313
4502
|
model,
|
|
@@ -4333,7 +4522,7 @@ var Controller = class {
|
|
|
4333
4522
|
* @public
|
|
4334
4523
|
*/
|
|
4335
4524
|
index(req) {
|
|
4336
|
-
return findMany(this.model, req);
|
|
4525
|
+
return findMany(this.model, req, (related) => this.serializerFor(related));
|
|
4337
4526
|
}
|
|
4338
4527
|
/**
|
|
4339
4528
|
* This method supports including relationships, and sparse fieldsets via
|
|
@@ -4348,7 +4537,7 @@ var Controller = class {
|
|
|
4348
4537
|
* @public
|
|
4349
4538
|
*/
|
|
4350
4539
|
show(req) {
|
|
4351
|
-
return findOne(this.model, req);
|
|
4540
|
+
return findOne(this.model, req, (related) => this.serializerFor(related));
|
|
4352
4541
|
}
|
|
4353
4542
|
/**
|
|
4354
4543
|
* Create and return a single Model instance that the Controller instance
|
|
@@ -4369,6 +4558,7 @@ var Controller = class {
|
|
|
4369
4558
|
data: { attributes, relationships }
|
|
4370
4559
|
}
|
|
4371
4560
|
} = req;
|
|
4561
|
+
await validateRelationships(model, relationships);
|
|
4372
4562
|
const record = await model.create({
|
|
4373
4563
|
...attributes,
|
|
4374
4564
|
...resolveRelationships(model, relationships)
|
|
@@ -4394,12 +4584,13 @@ var Controller = class {
|
|
|
4394
4584
|
*/
|
|
4395
4585
|
update(req) {
|
|
4396
4586
|
const { model } = this;
|
|
4397
|
-
return findOne(model, req).then((record) => {
|
|
4587
|
+
return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
|
|
4398
4588
|
const {
|
|
4399
4589
|
params: {
|
|
4400
4590
|
data: { attributes, relationships }
|
|
4401
4591
|
}
|
|
4402
4592
|
} = req;
|
|
4593
|
+
await validateRelationships(model, relationships);
|
|
4403
4594
|
return record.update({
|
|
4404
4595
|
...attributes,
|
|
4405
4596
|
...resolveRelationships(model, relationships)
|
|
@@ -4423,7 +4614,7 @@ var Controller = class {
|
|
|
4423
4614
|
* @public
|
|
4424
4615
|
*/
|
|
4425
4616
|
destroy(req) {
|
|
4426
|
-
return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
|
|
4617
|
+
return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
|
|
4427
4618
|
}
|
|
4428
4619
|
/**
|
|
4429
4620
|
* Respond to HEAD or OPTIONS requests.
|
|
@@ -4441,7 +4632,194 @@ var Controller = class {
|
|
|
4441
4632
|
var controller_default = Controller;
|
|
4442
4633
|
|
|
4443
4634
|
// src/packages/serializer/index.ts
|
|
4444
|
-
import {
|
|
4635
|
+
import { posix as posix2 } from "path";
|
|
4636
|
+
import { dasherize as dasherize5 } from "inflection";
|
|
4637
|
+
|
|
4638
|
+
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
4639
|
+
import { posix } from "path";
|
|
4640
|
+
function closestAncestor(source, key) {
|
|
4641
|
+
const name = posix.basename(key);
|
|
4642
|
+
let namespace = posix.dirname(key);
|
|
4643
|
+
if (namespace === ".") {
|
|
4644
|
+
return source.get(name);
|
|
4645
|
+
}
|
|
4646
|
+
namespace = posix.dirname(namespace);
|
|
4647
|
+
const ancestor = source.get(posix.join(namespace, name));
|
|
4648
|
+
if (ancestor) {
|
|
4649
|
+
return ancestor;
|
|
4650
|
+
}
|
|
4651
|
+
return closestAncestor(source, posix.join(posix.dirname(namespace), name));
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/packages/serializer/utils/load-linkage.ts
|
|
4655
|
+
import { camelize as camelize9 } from "inflection";
|
|
4656
|
+
var valueOf = (record, key) => Reflect.get(record, key);
|
|
4657
|
+
var toId = (value) => value == null ? null : String(value);
|
|
4658
|
+
async function loadLinkage(model, records, names) {
|
|
4659
|
+
const ids = Array.from(
|
|
4660
|
+
new Set(records.map((record) => record.getPrimaryKey()))
|
|
4661
|
+
);
|
|
4662
|
+
const linkage = /* @__PURE__ */ new Map();
|
|
4663
|
+
const belongsTo = [];
|
|
4664
|
+
const queries = [];
|
|
4665
|
+
ids.forEach((id) => {
|
|
4666
|
+
linkage.set(
|
|
4667
|
+
String(id),
|
|
4668
|
+
names.reduce((result, name) => {
|
|
4669
|
+
const opts = model.relationshipFor(name);
|
|
4670
|
+
result[name] = opts && opts.type === "hasMany" ? [] : null;
|
|
4671
|
+
return result;
|
|
4672
|
+
}, {})
|
|
4673
|
+
);
|
|
4674
|
+
});
|
|
4675
|
+
if (!ids.length) {
|
|
4676
|
+
return linkage;
|
|
4677
|
+
}
|
|
4678
|
+
const link = (ownerId, name, relatedId) => {
|
|
4679
|
+
const owner = linkage.get(String(ownerId));
|
|
4680
|
+
const id = toId(relatedId);
|
|
4681
|
+
if (!owner || id === null) {
|
|
4682
|
+
return;
|
|
4683
|
+
}
|
|
4684
|
+
const current = owner[name];
|
|
4685
|
+
if (Array.isArray(current)) {
|
|
4686
|
+
if (!current.includes(id)) {
|
|
4687
|
+
current.push(id);
|
|
4688
|
+
}
|
|
4689
|
+
} else if (current === null) {
|
|
4690
|
+
owner[name] = id;
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4693
|
+
names.forEach((name) => {
|
|
4694
|
+
const opts = model.relationshipFor(name);
|
|
4695
|
+
if (!opts) {
|
|
4696
|
+
return;
|
|
4697
|
+
}
|
|
4698
|
+
const { type, through, model: related } = opts;
|
|
4699
|
+
const foreignKey = camelize9(opts.foreignKey, true);
|
|
4700
|
+
if (type === "belongsTo") {
|
|
4701
|
+
belongsTo.push([name, foreignKey, related]);
|
|
4702
|
+
} else if (through) {
|
|
4703
|
+
const inverse = related.relationshipFor(opts.inverse);
|
|
4704
|
+
if (!inverse) {
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
const relatedKey = camelize9(inverse.foreignKey, true);
|
|
4708
|
+
queries.push(
|
|
4709
|
+
(async () => {
|
|
4710
|
+
const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
|
|
4711
|
+
rows.forEach((row) => {
|
|
4712
|
+
link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
|
|
4713
|
+
});
|
|
4714
|
+
})()
|
|
4715
|
+
);
|
|
4716
|
+
} else {
|
|
4717
|
+
queries.push(
|
|
4718
|
+
(async () => {
|
|
4719
|
+
const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
|
|
4720
|
+
rows.forEach((row) => {
|
|
4721
|
+
link(valueOf(row, foreignKey), name, row.getPrimaryKey());
|
|
4722
|
+
});
|
|
4723
|
+
})()
|
|
4724
|
+
);
|
|
4725
|
+
}
|
|
4726
|
+
});
|
|
4727
|
+
if (belongsTo.length) {
|
|
4728
|
+
queries.push(
|
|
4729
|
+
(async () => {
|
|
4730
|
+
const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
|
|
4731
|
+
rows.forEach((row) => {
|
|
4732
|
+
belongsTo.forEach(([name, key]) => {
|
|
4733
|
+
link(row.getPrimaryKey(), name, valueOf(row, key));
|
|
4734
|
+
});
|
|
4735
|
+
});
|
|
4736
|
+
await dropDangling(linkage, belongsTo);
|
|
4737
|
+
})()
|
|
4738
|
+
);
|
|
4739
|
+
}
|
|
4740
|
+
await Promise.all(queries);
|
|
4741
|
+
return linkage;
|
|
4742
|
+
}
|
|
4743
|
+
async function dropDangling(linkage, belongsTo) {
|
|
4744
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
4745
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4746
|
+
const ids = wanted.get(related) || /* @__PURE__ */ new Set();
|
|
4747
|
+
linkage.forEach(({ [name]: id }) => {
|
|
4748
|
+
if (typeof id === "string") {
|
|
4749
|
+
ids.add(id);
|
|
4750
|
+
}
|
|
4751
|
+
});
|
|
4752
|
+
wanted.set(related, ids);
|
|
4753
|
+
});
|
|
4754
|
+
const existing = /* @__PURE__ */ new Map();
|
|
4755
|
+
await Promise.all(
|
|
4756
|
+
Array.from(wanted, async ([related, ids]) => {
|
|
4757
|
+
if (!ids.size) {
|
|
4758
|
+
existing.set(related, ids);
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
|
|
4762
|
+
existing.set(
|
|
4763
|
+
related,
|
|
4764
|
+
new Set(rows.map((row) => String(row.getPrimaryKey())))
|
|
4765
|
+
);
|
|
4766
|
+
})
|
|
4767
|
+
);
|
|
4768
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4769
|
+
const found = existing.get(related);
|
|
4770
|
+
linkage.forEach((owner) => {
|
|
4771
|
+
const id = owner[name];
|
|
4772
|
+
if (typeof id === "string" && !found?.has(id)) {
|
|
4773
|
+
owner[name] = null;
|
|
4774
|
+
}
|
|
4775
|
+
});
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4779
|
+
// src/packages/serializer/utils/include-tree.ts
|
|
4780
|
+
function createIncludeTree(paths = []) {
|
|
4781
|
+
const tree = /* @__PURE__ */ new Map();
|
|
4782
|
+
paths.forEach((path) => {
|
|
4783
|
+
path.split(".").filter(Boolean).reduce((node, name) => {
|
|
4784
|
+
let child = node.get(name);
|
|
4785
|
+
if (!child) {
|
|
4786
|
+
child = /* @__PURE__ */ new Map();
|
|
4787
|
+
node.set(name, child);
|
|
4788
|
+
}
|
|
4789
|
+
return child;
|
|
4790
|
+
}, tree);
|
|
4791
|
+
});
|
|
4792
|
+
return tree;
|
|
4793
|
+
}
|
|
4794
|
+
function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
|
|
4795
|
+
if (depth < 1) {
|
|
4796
|
+
return [];
|
|
4797
|
+
}
|
|
4798
|
+
return names.reduce((paths, name) => {
|
|
4799
|
+
const opts = model.relationshipFor(name);
|
|
4800
|
+
if (!opts) {
|
|
4801
|
+
return paths;
|
|
4802
|
+
}
|
|
4803
|
+
const { model: related } = opts;
|
|
4804
|
+
const serializer = serializerFor(related);
|
|
4805
|
+
if (!serializer) {
|
|
4806
|
+
return [...paths, name];
|
|
4807
|
+
}
|
|
4808
|
+
const { hasOne, hasMany } = serializer;
|
|
4809
|
+
return [
|
|
4810
|
+
...paths,
|
|
4811
|
+
name,
|
|
4812
|
+
...enumerateIncludePaths(
|
|
4813
|
+
related,
|
|
4814
|
+
[...hasOne, ...hasMany],
|
|
4815
|
+
depth - 1,
|
|
4816
|
+
serializerFor
|
|
4817
|
+
).map((path) => `${name}.${path}`)
|
|
4818
|
+
];
|
|
4819
|
+
}, []);
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
// src/packages/serializer/index.ts
|
|
4445
4823
|
var Serializer = class {
|
|
4446
4824
|
/**
|
|
4447
4825
|
* An Array of the `hasOne` or `belongsTo` relationships on a Serializer
|
|
@@ -4528,9 +4906,17 @@ var Serializer = class {
|
|
|
4528
4906
|
* the resource and relationship objects in the returned [JSON API](
|
|
4529
4907
|
* http://jsonapi.org) document object.
|
|
4530
4908
|
*
|
|
4531
|
-
* @param {Array} options.include - An array of
|
|
4532
|
-
*
|
|
4533
|
-
* the returned [JSON API](http://jsonapi.org)
|
|
4909
|
+
* @param {Array} options.include - An array of relationship paths (e.g.
|
|
4910
|
+
* `'comments'` or `'comments.user'`) whose resources should be added to the
|
|
4911
|
+
* top level included object of the returned [JSON API](http://jsonapi.org)
|
|
4912
|
+
* document object. Intermediate resources of a nested path are included too.
|
|
4913
|
+
*
|
|
4914
|
+
* @param {String} options.namespace - The namespace of the request, i.e. of
|
|
4915
|
+
* the Controller handling it. Every link in the document is built in it, and
|
|
4916
|
+
* included resources are serialized by their Serializer in it (falling back
|
|
4917
|
+
* to the root). Defaults to this Serializer's namespace — which is the root
|
|
4918
|
+
* one when a namespaced Controller has no Serializer of its own, so the
|
|
4919
|
+
* Controller passes its namespace explicitly.
|
|
4534
4920
|
*
|
|
4535
4921
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
|
|
4536
4922
|
* object.
|
|
@@ -4541,38 +4927,61 @@ var Serializer = class {
|
|
|
4541
4927
|
data,
|
|
4542
4928
|
links,
|
|
4543
4929
|
domain,
|
|
4544
|
-
include
|
|
4930
|
+
include,
|
|
4931
|
+
namespace = this.namespace
|
|
4545
4932
|
}) {
|
|
4546
|
-
|
|
4547
|
-
const
|
|
4933
|
+
const tree = createIncludeTree(include);
|
|
4934
|
+
const related = /* @__PURE__ */ new Map();
|
|
4935
|
+
const included = /* @__PURE__ */ new Map();
|
|
4936
|
+
const names = Array.from(tree.keys());
|
|
4937
|
+
let primary;
|
|
4938
|
+
let serialized;
|
|
4548
4939
|
if (Array.isArray(data)) {
|
|
4549
|
-
|
|
4550
|
-
data
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
)
|
|
4940
|
+
primary = await Promise.all(
|
|
4941
|
+
data.map(
|
|
4942
|
+
(item) => this.formatOne({
|
|
4943
|
+
item,
|
|
4944
|
+
domain,
|
|
4945
|
+
related,
|
|
4946
|
+
namespace,
|
|
4947
|
+
include: names
|
|
4948
|
+
})
|
|
4559
4949
|
)
|
|
4560
|
-
|
|
4950
|
+
);
|
|
4951
|
+
serialized = { data: primary };
|
|
4561
4952
|
} else {
|
|
4562
|
-
|
|
4563
|
-
|
|
4953
|
+
const resource2 = await this.formatOne({
|
|
4954
|
+
domain,
|
|
4955
|
+
related,
|
|
4956
|
+
namespace,
|
|
4957
|
+
item: data,
|
|
4958
|
+
include: names,
|
|
4959
|
+
links: false
|
|
4960
|
+
});
|
|
4961
|
+
primary = [resource2];
|
|
4962
|
+
serialized = { data: resource2 };
|
|
4963
|
+
}
|
|
4964
|
+
for (const [name, children] of tree) {
|
|
4965
|
+
const opts = this.model.relationshipFor(name);
|
|
4966
|
+
const records = related.get(name);
|
|
4967
|
+
if (opts && records) {
|
|
4968
|
+
await this.addIncluded({
|
|
4564
4969
|
domain,
|
|
4565
|
-
|
|
4970
|
+
records,
|
|
4566
4971
|
included,
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4972
|
+
namespace,
|
|
4973
|
+
model: opts.model,
|
|
4974
|
+
tree: children
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4571
4977
|
}
|
|
4572
|
-
|
|
4978
|
+
primary.forEach((resource2) => {
|
|
4979
|
+
included.delete(resourceKey(resource2));
|
|
4980
|
+
});
|
|
4981
|
+
if (included.size) {
|
|
4573
4982
|
serialized = {
|
|
4574
4983
|
...serialized,
|
|
4575
|
-
included:
|
|
4984
|
+
included: Array.from(included.values())
|
|
4576
4985
|
};
|
|
4577
4986
|
}
|
|
4578
4987
|
return {
|
|
@@ -4587,6 +4996,12 @@ var Serializer = class {
|
|
|
4587
4996
|
* Transform a single Model instance into a [JSON API](http://jsonapi.org)
|
|
4588
4997
|
* resource object.
|
|
4589
4998
|
*
|
|
4999
|
+
* Relationships are serialized in one of two ways. By default each one is
|
|
5000
|
+
* read from the Model instance (for primary data the query has already
|
|
5001
|
+
* loaded them). When `linkage` is given — as it is for included resources,
|
|
5002
|
+
* whose relationships are batch-loaded by `addIncluded()` — the resource
|
|
5003
|
+
* linkage is built from it instead, without touching the database.
|
|
5004
|
+
*
|
|
4590
5005
|
* @method formatOne
|
|
4591
5006
|
*
|
|
4592
5007
|
* @param {Object} options - An options object used for building the returned
|
|
@@ -4603,17 +5018,19 @@ var Serializer = class {
|
|
|
4603
5018
|
* the top level links object or relationship links objects in the returned
|
|
4604
5019
|
* [JSON API](http://jsonapi.org) resource object.
|
|
4605
5020
|
*
|
|
4606
|
-
* @param {Array} options.include - An array of
|
|
4607
|
-
*
|
|
4608
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
5021
|
+
* @param {Array} options.include - An array of the relationship keys whose
|
|
5022
|
+
* related records should be collected into `options.related`.
|
|
4609
5023
|
*
|
|
4610
|
-
* @param {
|
|
4611
|
-
*
|
|
4612
|
-
* included
|
|
5024
|
+
* @param {Map} options.related - Collects, per relationship key in
|
|
5025
|
+
* `options.include`, the related Model instances that belong in the top
|
|
5026
|
+
* level included object of a [JSON API](http://jsonapi.org) document object.
|
|
4613
5027
|
*
|
|
4614
|
-
* @param {
|
|
4615
|
-
*
|
|
4616
|
-
*
|
|
5028
|
+
* @param {Object} options.linkage - Pre-loaded resource linkage (related
|
|
5029
|
+
* primary keys per relationship key) to serialize relationships from.
|
|
5030
|
+
*
|
|
5031
|
+
* @param {String} options.namespace - The namespace to build links in.
|
|
5032
|
+
* Defaults to this Serializer's; included resources pass the namespace of
|
|
5033
|
+
* the request, so every link in a document points into the same namespace.
|
|
4617
5034
|
*
|
|
4618
5035
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
|
|
4619
5036
|
* object.
|
|
@@ -4624,13 +5041,15 @@ var Serializer = class {
|
|
|
4624
5041
|
item,
|
|
4625
5042
|
links,
|
|
4626
5043
|
domain,
|
|
4627
|
-
include,
|
|
4628
|
-
|
|
4629
|
-
|
|
5044
|
+
include = [],
|
|
5045
|
+
related,
|
|
5046
|
+
linkage,
|
|
5047
|
+
namespace = this.namespace
|
|
4630
5048
|
}) {
|
|
4631
5049
|
const { resourceName: type } = item;
|
|
4632
5050
|
const id = String(item.getPrimaryKey());
|
|
4633
|
-
|
|
5051
|
+
const names = [...this.hasOne, ...this.hasMany];
|
|
5052
|
+
let relationships;
|
|
4634
5053
|
const attributes = dasherizeKeys(
|
|
4635
5054
|
item.getAttributes(
|
|
4636
5055
|
...Object.keys(item.rawColumnData).filter(
|
|
@@ -4643,34 +5062,41 @@ var Serializer = class {
|
|
|
4643
5062
|
type,
|
|
4644
5063
|
attributes
|
|
4645
5064
|
};
|
|
4646
|
-
if (
|
|
5065
|
+
if (linkage) {
|
|
5066
|
+
relationships = names.reduce(
|
|
5067
|
+
(hash, name) => ({
|
|
5068
|
+
...hash,
|
|
5069
|
+
[dasherize5(underscore(name))]: this.formatLinkage(
|
|
5070
|
+
domain,
|
|
5071
|
+
this.model.relationshipFor(name)?.model.resourceName,
|
|
5072
|
+
linkage[name],
|
|
5073
|
+
namespace
|
|
5074
|
+
)
|
|
5075
|
+
}),
|
|
5076
|
+
{}
|
|
5077
|
+
);
|
|
5078
|
+
} else {
|
|
5079
|
+
const collect = (name, records) => {
|
|
5080
|
+
if (related && include.includes(name)) {
|
|
5081
|
+
related.set(name, [...related.get(name) || [], ...records]);
|
|
5082
|
+
}
|
|
5083
|
+
};
|
|
4647
5084
|
relationships = await promiseHash(
|
|
4648
|
-
|
|
5085
|
+
names.reduce(
|
|
4649
5086
|
(hash, name) => ({
|
|
4650
5087
|
...hash,
|
|
4651
|
-
[
|
|
4652
|
-
const
|
|
4653
|
-
if (Array.isArray(
|
|
5088
|
+
[dasherize5(underscore(name))]: (async () => {
|
|
5089
|
+
const value = await Reflect.get(item, name);
|
|
5090
|
+
if (Array.isArray(value)) {
|
|
5091
|
+
collect(name, value);
|
|
4654
5092
|
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
|
-
})
|
|
5093
|
+
data: value.map(
|
|
5094
|
+
(relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
|
|
4665
5095
|
)
|
|
4666
5096
|
};
|
|
4667
|
-
} else if (
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
included,
|
|
4671
|
-
item: related,
|
|
4672
|
-
include: include.includes(name)
|
|
4673
|
-
});
|
|
5097
|
+
} else if (value && value.id != null) {
|
|
5098
|
+
collect(name, [value]);
|
|
5099
|
+
return this.formatRelationship(value, domain, namespace);
|
|
4674
5100
|
}
|
|
4675
5101
|
return {
|
|
4676
5102
|
data: null
|
|
@@ -4685,16 +5111,9 @@ var Serializer = class {
|
|
|
4685
5111
|
serialized.relationships = relationships;
|
|
4686
5112
|
}
|
|
4687
5113
|
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
|
-
}
|
|
5114
|
+
serialized.links = {
|
|
5115
|
+
self: this.linkFor(domain, type, id, namespace)
|
|
5116
|
+
};
|
|
4698
5117
|
}
|
|
4699
5118
|
return serialized;
|
|
4700
5119
|
}
|
|
@@ -4704,70 +5123,167 @@ var Serializer = class {
|
|
|
4704
5123
|
*
|
|
4705
5124
|
* @method formatRelationship
|
|
4706
5125
|
*
|
|
4707
|
-
* @param {
|
|
5126
|
+
* @param {Model} item - The Model instance to transform into the returned
|
|
4708
5127
|
* [JSON API](http://jsonapi.org) relationship object.
|
|
4709
5128
|
*
|
|
4710
|
-
* @param {
|
|
5129
|
+
* @param {String} domain - A string used to build links included in the
|
|
4711
5130
|
* returned [JSON API](http://jsonapi.org) relationship object.
|
|
4712
5131
|
*
|
|
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.
|
|
5132
|
+
* @return {Object} A [JSON API](http://jsonapi.org) relationship object.
|
|
4719
5133
|
*
|
|
4720
|
-
* @
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
5134
|
+
* @private
|
|
5135
|
+
*/
|
|
5136
|
+
formatRelationship(item, domain, namespace = this.namespace) {
|
|
5137
|
+
return this.formatLinkage(
|
|
5138
|
+
domain,
|
|
5139
|
+
item.resourceName,
|
|
5140
|
+
String(item.getPrimaryKey()),
|
|
5141
|
+
namespace
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
/**
|
|
5145
|
+
* Build a [JSON API](http://jsonapi.org) relationship object from resource
|
|
5146
|
+
* linkage, in the same shape `formatRelationship()` produces from Model
|
|
5147
|
+
* instances: to-one relationships carry a `links` object, to-many ones only
|
|
5148
|
+
* `data`, and a missing to-one relationship is `{ data: null }`.
|
|
4726
5149
|
*
|
|
5150
|
+
* @method formatLinkage
|
|
4727
5151
|
* @private
|
|
4728
5152
|
*/
|
|
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}`
|
|
5153
|
+
formatLinkage(domain, type, linkage, namespace = this.namespace) {
|
|
5154
|
+
if (Array.isArray(linkage)) {
|
|
5155
|
+
return {
|
|
5156
|
+
data: type ? linkage.map((id) => ({ id, type })) : []
|
|
4749
5157
|
};
|
|
4750
5158
|
}
|
|
4751
|
-
if (
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
domain,
|
|
4756
|
-
include: [],
|
|
4757
|
-
included: [],
|
|
4758
|
-
formatRelationships: false
|
|
4759
|
-
})
|
|
4760
|
-
);
|
|
5159
|
+
if (linkage == null || !type) {
|
|
5160
|
+
return {
|
|
5161
|
+
data: null
|
|
5162
|
+
};
|
|
4761
5163
|
}
|
|
4762
5164
|
return {
|
|
4763
5165
|
data: {
|
|
4764
|
-
id,
|
|
5166
|
+
id: linkage,
|
|
4765
5167
|
type
|
|
4766
5168
|
},
|
|
4767
|
-
links
|
|
5169
|
+
links: {
|
|
5170
|
+
self: this.linkFor(domain, type, linkage, namespace)
|
|
5171
|
+
}
|
|
4768
5172
|
};
|
|
4769
5173
|
}
|
|
5174
|
+
/**
|
|
5175
|
+
* Add `records` (instances of `model`) to `included` as resource objects,
|
|
5176
|
+
* then recurse into the relationships named in `tree`. Each is serialized by
|
|
5177
|
+
* `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
|
|
5178
|
+
* `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
|
|
5179
|
+
* is one and `CommentsSerializer` otherwise. The relationships of every level
|
|
5180
|
+
* are batch-loaded with one query per relationship, not one per record.
|
|
5181
|
+
*
|
|
5182
|
+
* @method addIncluded
|
|
5183
|
+
* @private
|
|
5184
|
+
*/
|
|
5185
|
+
async addIncluded({
|
|
5186
|
+
model,
|
|
5187
|
+
records,
|
|
5188
|
+
tree,
|
|
5189
|
+
domain,
|
|
5190
|
+
included,
|
|
5191
|
+
namespace
|
|
5192
|
+
}) {
|
|
5193
|
+
const serializer = this.serializerFor(model, namespace);
|
|
5194
|
+
const unique = Array.from(
|
|
5195
|
+
new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
|
|
5196
|
+
);
|
|
5197
|
+
if (!unique.length) {
|
|
5198
|
+
return;
|
|
5199
|
+
}
|
|
5200
|
+
const names = [...serializer.hasOne, ...serializer.hasMany];
|
|
5201
|
+
const linkage = await loadLinkage(model, unique, names);
|
|
5202
|
+
for (const item of unique) {
|
|
5203
|
+
const id = String(item.getPrimaryKey());
|
|
5204
|
+
const key = resourceKey({ id, type: item.resourceName });
|
|
5205
|
+
if (!included.has(key)) {
|
|
5206
|
+
included.set(
|
|
5207
|
+
key,
|
|
5208
|
+
await serializer.formatOne({
|
|
5209
|
+
item,
|
|
5210
|
+
domain,
|
|
5211
|
+
linkage: linkage.get(id),
|
|
5212
|
+
namespace
|
|
5213
|
+
})
|
|
5214
|
+
);
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
for (const [name, children] of tree) {
|
|
5218
|
+
const opts = model.relationshipFor(name);
|
|
5219
|
+
if (!opts || !names.includes(name)) {
|
|
5220
|
+
continue;
|
|
5221
|
+
}
|
|
5222
|
+
const { model: next } = opts;
|
|
5223
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5224
|
+
linkage.forEach(({ [name]: value }) => {
|
|
5225
|
+
(Array.isArray(value) ? value : [value]).forEach((relatedId) => {
|
|
5226
|
+
if (relatedId != null) {
|
|
5227
|
+
ids.add(relatedId);
|
|
5228
|
+
}
|
|
5229
|
+
});
|
|
5230
|
+
});
|
|
5231
|
+
if (ids.size) {
|
|
5232
|
+
const attributes = this.serializerFor(
|
|
5233
|
+
next,
|
|
5234
|
+
namespace
|
|
5235
|
+
).attributes.filter((attr) => next.attributeNames.includes(attr));
|
|
5236
|
+
const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
|
|
5237
|
+
await this.addIncluded({
|
|
5238
|
+
domain,
|
|
5239
|
+
included,
|
|
5240
|
+
namespace,
|
|
5241
|
+
model: next,
|
|
5242
|
+
records: nextRecords,
|
|
5243
|
+
tree: children
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
/**
|
|
5249
|
+
* Resolve the Serializer for `model` in `namespace` (this Serializer's by
|
|
5250
|
+
* default), the way a namespaced Controller resolves its own:
|
|
5251
|
+
* `admin/comments` if it exists, otherwise the closest ancestor namespace's,
|
|
5252
|
+
* down to the root `comments` Serializer. Falls back to `model.serializer`
|
|
5253
|
+
* when this Serializer was not created by an application (e.g. in
|
|
5254
|
+
* isolation).
|
|
5255
|
+
*
|
|
5256
|
+
* Pass the request's namespace when there is one: a Serializer's own
|
|
5257
|
+
* namespace is the root one whenever it is a namespaced Controller's
|
|
5258
|
+
* fallback.
|
|
5259
|
+
*
|
|
5260
|
+
* @method serializerFor
|
|
5261
|
+
* @private
|
|
5262
|
+
*/
|
|
5263
|
+
serializerFor(model, namespace = this.namespace) {
|
|
5264
|
+
const { serializers } = this;
|
|
5265
|
+
if (serializers) {
|
|
5266
|
+
const key = posix2.join(namespace || ".", model.resourceName);
|
|
5267
|
+
const serializer = serializers.get(key) || closestAncestor(serializers, key);
|
|
5268
|
+
if (serializer) {
|
|
5269
|
+
return serializer;
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
return model.serializer;
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* @private
|
|
5276
|
+
*/
|
|
5277
|
+
linkFor(domain, type, id, namespace = this.namespace) {
|
|
5278
|
+
if (namespace) {
|
|
5279
|
+
return `${domain}/${namespace}/${type}/${id}`;
|
|
5280
|
+
}
|
|
5281
|
+
return `${domain}/${type}/${id}`;
|
|
5282
|
+
}
|
|
4770
5283
|
};
|
|
5284
|
+
function resourceKey({ id, type }) {
|
|
5285
|
+
return `${type}:${id}`;
|
|
5286
|
+
}
|
|
4771
5287
|
var serializer_default = Serializer;
|
|
4772
5288
|
|
|
4773
5289
|
// src/packages/config/index.ts
|
|
@@ -4966,7 +5482,10 @@ function resource(action) {
|
|
|
4966
5482
|
data,
|
|
4967
5483
|
links,
|
|
4968
5484
|
domain,
|
|
4969
|
-
include
|
|
5485
|
+
include,
|
|
5486
|
+
// The request's namespace, not the serializer's: a namespaced
|
|
5487
|
+
// controller without its own serializer is given the root one.
|
|
5488
|
+
namespace
|
|
4970
5489
|
});
|
|
4971
5490
|
}
|
|
4972
5491
|
return data;
|
|
@@ -5047,6 +5566,7 @@ var ParameterTypeError = class extends TypeError {
|
|
|
5047
5566
|
Expected type '${type || "undefined"}' for parameter '${path}' but got
|
|
5048
5567
|
'${actual}'.
|
|
5049
5568
|
`);
|
|
5569
|
+
this.source = sourceFor(path);
|
|
5050
5570
|
}
|
|
5051
5571
|
};
|
|
5052
5572
|
var parameter_type_error_default = createServerError(ParameterTypeError, 400);
|
|
@@ -5059,6 +5579,7 @@ var ParameterValueError = class extends TypeError {
|
|
|
5059
5579
|
[${param.size ? Array.from(param.values()).join(", ") : ""}] but got
|
|
5060
5580
|
${String(actual)}.
|
|
5061
5581
|
`);
|
|
5582
|
+
this.source = sourceFor(param.path);
|
|
5062
5583
|
}
|
|
5063
5584
|
};
|
|
5064
5585
|
var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
@@ -5067,6 +5588,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
|
5067
5588
|
var InvalidParameterError = class extends TypeError {
|
|
5068
5589
|
constructor(path) {
|
|
5069
5590
|
super(`'${path}' is not a valid parameter for this resource.`);
|
|
5591
|
+
this.source = sourceFor(path);
|
|
5070
5592
|
}
|
|
5071
5593
|
};
|
|
5072
5594
|
var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
|
|
@@ -5082,6 +5604,7 @@ var ResourceMismatchError = class extends TypeError {
|
|
|
5082
5604
|
Expected '${String(expected)}' for parameter '${path}' but got
|
|
5083
5605
|
${String(normalized)}.
|
|
5084
5606
|
`);
|
|
5607
|
+
this.source = sourceFor(path);
|
|
5085
5608
|
}
|
|
5086
5609
|
};
|
|
5087
5610
|
var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
|
|
@@ -5090,14 +5613,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
|
|
|
5090
5613
|
var ParameterRequiredError = class extends TypeError {
|
|
5091
5614
|
constructor(path) {
|
|
5092
5615
|
super(`Missing required parameter '${path}'.`);
|
|
5616
|
+
this.source = sourceFor(path);
|
|
5093
5617
|
}
|
|
5094
5618
|
};
|
|
5095
5619
|
var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
|
|
5096
5620
|
|
|
5621
|
+
// src/packages/router/route/params/errors/client-generated-id-error.ts
|
|
5622
|
+
var ClientGeneratedIdError = class extends TypeError {
|
|
5623
|
+
constructor() {
|
|
5624
|
+
super("Client-generated IDs are not supported for this resource.");
|
|
5625
|
+
this.source = sourceFor("data.id");
|
|
5626
|
+
}
|
|
5627
|
+
};
|
|
5628
|
+
var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
|
|
5629
|
+
|
|
5630
|
+
// src/packages/router/route/params/errors/forbidden-parameter-error.ts
|
|
5631
|
+
var ForbiddenParameterError = class extends TypeError {
|
|
5632
|
+
constructor(path) {
|
|
5633
|
+
super(`Setting '${path}' is not supported for this resource.`);
|
|
5634
|
+
this.source = sourceFor(path);
|
|
5635
|
+
}
|
|
5636
|
+
};
|
|
5637
|
+
var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
|
|
5638
|
+
|
|
5097
5639
|
// src/packages/router/route/params/errors/parameter-not-nullable-error.ts
|
|
5098
5640
|
var ParameterNotNullableError = class extends TypeError {
|
|
5099
5641
|
constructor({ path }) {
|
|
5100
5642
|
super(`Parameter '${path}' is not nullable.`);
|
|
5643
|
+
this.source = sourceFor(path);
|
|
5101
5644
|
}
|
|
5102
5645
|
};
|
|
5103
5646
|
var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
|
|
@@ -5226,13 +5769,14 @@ var Parameter = class extends set_default {
|
|
|
5226
5769
|
path,
|
|
5227
5770
|
type,
|
|
5228
5771
|
required: Boolean(required),
|
|
5229
|
-
sanitize: Boolean(sanitize)
|
|
5772
|
+
sanitize: Boolean(sanitize),
|
|
5773
|
+
restricted: values !== void 0
|
|
5230
5774
|
});
|
|
5231
5775
|
this.freeze();
|
|
5232
5776
|
}
|
|
5233
5777
|
validate(value) {
|
|
5234
5778
|
validateType2(this, value);
|
|
5235
|
-
if (this.
|
|
5779
|
+
if (this.restricted) {
|
|
5236
5780
|
return validateValue(this, value);
|
|
5237
5781
|
}
|
|
5238
5782
|
return value;
|
|
@@ -5251,6 +5795,17 @@ function getURLParams(dynamicSegments) {
|
|
|
5251
5795
|
]);
|
|
5252
5796
|
}
|
|
5253
5797
|
|
|
5798
|
+
// src/packages/router/route/params/parameter/forbidden-parameter.ts
|
|
5799
|
+
var ForbiddenParameter = class extends parameter_default {
|
|
5800
|
+
constructor(path) {
|
|
5801
|
+
super({ path });
|
|
5802
|
+
}
|
|
5803
|
+
validate() {
|
|
5804
|
+
throw new forbidden_parameter_error_default(this.path);
|
|
5805
|
+
}
|
|
5806
|
+
};
|
|
5807
|
+
var forbidden_parameter_default = ForbiddenParameter;
|
|
5808
|
+
|
|
5254
5809
|
// src/packages/router/route/params/utils/get-data-params.ts
|
|
5255
5810
|
function getIDParam({ model }) {
|
|
5256
5811
|
const primaryKeyColumn = model.columnFor(model.primaryKey);
|
|
@@ -5303,16 +5858,48 @@ function getRelationshipsParam({
|
|
|
5303
5858
|
model,
|
|
5304
5859
|
params
|
|
5305
5860
|
}) {
|
|
5861
|
+
const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
|
|
5862
|
+
key,
|
|
5863
|
+
new forbidden_parameter_default(`data.relationships.${key}`)
|
|
5864
|
+
]);
|
|
5306
5865
|
return [
|
|
5307
5866
|
"relationships",
|
|
5308
5867
|
new parameter_group_default(
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5868
|
+
[
|
|
5869
|
+
...params.reduce((group, param) => {
|
|
5870
|
+
const path = `data.relationships.${param}`;
|
|
5871
|
+
const opts = model.relationshipFor(param);
|
|
5872
|
+
if (!opts) {
|
|
5873
|
+
return group;
|
|
5874
|
+
}
|
|
5875
|
+
if (opts.type === "hasMany") {
|
|
5876
|
+
return [
|
|
5877
|
+
...group,
|
|
5878
|
+
[
|
|
5879
|
+
param,
|
|
5880
|
+
new parameter_group_default(
|
|
5881
|
+
[
|
|
5882
|
+
[
|
|
5883
|
+
"data",
|
|
5884
|
+
new parameter_default({
|
|
5885
|
+
type: "array",
|
|
5886
|
+
path: `${path}.data`,
|
|
5887
|
+
required: true
|
|
5888
|
+
})
|
|
5889
|
+
]
|
|
5890
|
+
],
|
|
5891
|
+
{
|
|
5892
|
+
path
|
|
5893
|
+
}
|
|
5894
|
+
)
|
|
5895
|
+
]
|
|
5896
|
+
];
|
|
5897
|
+
}
|
|
5898
|
+
const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
|
|
5899
|
+
let primaryKeyType = "number";
|
|
5900
|
+
if (primaryKeyColumn) {
|
|
5901
|
+
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5902
|
+
}
|
|
5316
5903
|
return [
|
|
5317
5904
|
...group,
|
|
5318
5905
|
[
|
|
@@ -5321,11 +5908,32 @@ function getRelationshipsParam({
|
|
|
5321
5908
|
[
|
|
5322
5909
|
[
|
|
5323
5910
|
"data",
|
|
5324
|
-
new
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5911
|
+
new parameter_group_default(
|
|
5912
|
+
[
|
|
5913
|
+
[
|
|
5914
|
+
"id",
|
|
5915
|
+
new parameter_default({
|
|
5916
|
+
type: primaryKeyType,
|
|
5917
|
+
path: `${path}.data.id`,
|
|
5918
|
+
required: true
|
|
5919
|
+
})
|
|
5920
|
+
],
|
|
5921
|
+
[
|
|
5922
|
+
"type",
|
|
5923
|
+
new parameter_default({
|
|
5924
|
+
type: "string",
|
|
5925
|
+
path: `${path}.data.type`,
|
|
5926
|
+
values: [opts.model.resourceName],
|
|
5927
|
+
required: true
|
|
5928
|
+
})
|
|
5929
|
+
]
|
|
5930
|
+
],
|
|
5931
|
+
{
|
|
5932
|
+
type: "array",
|
|
5933
|
+
path: `${path}.data`,
|
|
5934
|
+
required: true
|
|
5935
|
+
}
|
|
5936
|
+
)
|
|
5329
5937
|
]
|
|
5330
5938
|
],
|
|
5331
5939
|
{
|
|
@@ -5334,55 +5942,9 @@ function getRelationshipsParam({
|
|
|
5334
5942
|
)
|
|
5335
5943
|
]
|
|
5336
5944
|
];
|
|
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
|
-
}, []),
|
|
5945
|
+
}, []),
|
|
5946
|
+
...forbidden
|
|
5947
|
+
],
|
|
5386
5948
|
{
|
|
5387
5949
|
path: "data.relationships"
|
|
5388
5950
|
}
|
|
@@ -5490,10 +6052,9 @@ function getFilterParam({ filter }) {
|
|
|
5490
6052
|
)
|
|
5491
6053
|
];
|
|
5492
6054
|
}
|
|
5493
|
-
function getFieldsParam({
|
|
5494
|
-
model,
|
|
5495
|
-
|
|
5496
|
-
}) {
|
|
6055
|
+
function getFieldsParam(controller) {
|
|
6056
|
+
const { model, serializer } = controller;
|
|
6057
|
+
const { hasOne, hasMany, attributes } = serializer;
|
|
5497
6058
|
const relationships = [...hasOne, ...hasMany];
|
|
5498
6059
|
return [
|
|
5499
6060
|
"fields",
|
|
@@ -5522,7 +6083,7 @@ function getFieldsParam({
|
|
|
5522
6083
|
sanitize: true,
|
|
5523
6084
|
values: [
|
|
5524
6085
|
opts.model.primaryKey,
|
|
5525
|
-
...opts.model.
|
|
6086
|
+
...controller.serializerFor(opts.model).attributes
|
|
5526
6087
|
]
|
|
5527
6088
|
})
|
|
5528
6089
|
]
|
|
@@ -5540,16 +6101,30 @@ function getFieldsParam({
|
|
|
5540
6101
|
)
|
|
5541
6102
|
];
|
|
5542
6103
|
}
|
|
5543
|
-
function getIncludeParam({
|
|
5544
|
-
|
|
5545
|
-
}
|
|
6104
|
+
function getIncludeParam(controller) {
|
|
6105
|
+
const { model, maxIncludeDepth, serializer } = controller;
|
|
6106
|
+
const { hasOne, hasMany } = serializer;
|
|
5546
6107
|
const relationships = [...hasOne, ...hasMany];
|
|
5547
6108
|
return [
|
|
5548
6109
|
"include",
|
|
5549
6110
|
new parameter_default({
|
|
5550
6111
|
path: "include",
|
|
5551
6112
|
type: "array",
|
|
5552
|
-
|
|
6113
|
+
// Every top level name stays allowed (as before), plus the nested paths
|
|
6114
|
+
// (`comments.user`) reachable through each related serializer in the
|
|
6115
|
+
// controller's namespace (at every level, even below a root fallback
|
|
6116
|
+
// serializer), down to the controller's `maxIncludeDepth`.
|
|
6117
|
+
values: Array.from(
|
|
6118
|
+
/* @__PURE__ */ new Set([
|
|
6119
|
+
...relationships,
|
|
6120
|
+
...enumerateIncludePaths(
|
|
6121
|
+
model,
|
|
6122
|
+
relationships,
|
|
6123
|
+
maxIncludeDepth,
|
|
6124
|
+
(related) => controller.serializerFor(related)
|
|
6125
|
+
)
|
|
6126
|
+
])
|
|
6127
|
+
)
|
|
5553
6128
|
})
|
|
5554
6129
|
];
|
|
5555
6130
|
}
|
|
@@ -5587,6 +6162,15 @@ function getCollectionQueryParams(controller) {
|
|
|
5587
6162
|
return getCustomParams(controller);
|
|
5588
6163
|
}
|
|
5589
6164
|
|
|
6165
|
+
// src/packages/router/route/params/utils/validate-client-id.ts
|
|
6166
|
+
function validateClientId(params) {
|
|
6167
|
+
const { data } = params;
|
|
6168
|
+
if (data && typeof data === "object" && Reflect.has(data, "id")) {
|
|
6169
|
+
throw new client_generated_id_error_default();
|
|
6170
|
+
}
|
|
6171
|
+
return true;
|
|
6172
|
+
}
|
|
6173
|
+
|
|
5590
6174
|
// src/packages/router/route/params/utils/validate-resource-id.ts
|
|
5591
6175
|
function validateResourceId({
|
|
5592
6176
|
params: {
|
|
@@ -5752,6 +6336,9 @@ var Route = class extends set_default {
|
|
|
5752
6336
|
...req.params,
|
|
5753
6337
|
...this.parseParams(req.url.params)
|
|
5754
6338
|
};
|
|
6339
|
+
if (this.action === "create" && req.method === "POST") {
|
|
6340
|
+
validateClientId(params);
|
|
6341
|
+
}
|
|
5755
6342
|
if (req.method !== "OPTIONS") {
|
|
5756
6343
|
params = this.params.validate(params);
|
|
5757
6344
|
}
|
|
@@ -6067,7 +6654,7 @@ var router_default = Router;
|
|
|
6067
6654
|
import { join as joinPath2 } from "path";
|
|
6068
6655
|
|
|
6069
6656
|
// src/packages/loader/utils/format-key.ts
|
|
6070
|
-
import { dasherize as
|
|
6657
|
+
import { dasherize as dasherize6 } from "inflection";
|
|
6071
6658
|
var NAMESPACE_DELIMITER = /\$-/g;
|
|
6072
6659
|
function formatKey(key, formatter) {
|
|
6073
6660
|
return chain(key).pipe((str) => {
|
|
@@ -6075,7 +6662,7 @@ function formatKey(key, formatter) {
|
|
|
6075
6662
|
return formatter(str);
|
|
6076
6663
|
}
|
|
6077
6664
|
return str;
|
|
6078
|
-
}).pipe(underscore).pipe(
|
|
6665
|
+
}).pipe(underscore).pipe(dasherize6).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
|
|
6079
6666
|
}
|
|
6080
6667
|
|
|
6081
6668
|
// src/packages/loader/utils/bundle-for.ts
|
|
@@ -6147,26 +6734,10 @@ function bundleFor(path) {
|
|
|
6147
6734
|
return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
|
|
6148
6735
|
}
|
|
6149
6736
|
|
|
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
6737
|
// src/packages/loader/resolver/utils/closest-child.ts
|
|
6167
|
-
import { posix as
|
|
6738
|
+
import { posix as posix3 } from "path";
|
|
6168
6739
|
function closestChild(source, key) {
|
|
6169
|
-
const [[, result] = []] = Array.from(source).map(([path, value]) => [
|
|
6740
|
+
const [[, result] = []] = Array.from(source).map(([path, value]) => [posix3.basename(path), value]).filter(([resource2]) => key === resource2);
|
|
6170
6741
|
return result;
|
|
6171
6742
|
}
|
|
6172
6743
|
|
|
@@ -6189,7 +6760,7 @@ function resolve(group) {
|
|
|
6189
6760
|
}
|
|
6190
6761
|
|
|
6191
6762
|
// src/packages/loader/builder/utils/create-parent-builder.ts
|
|
6192
|
-
import { posix as
|
|
6763
|
+
import { posix as posix4 } from "path";
|
|
6193
6764
|
|
|
6194
6765
|
// src/packages/loader/builder/utils/sort-by-namespace.ts
|
|
6195
6766
|
function sortByNamespace([a], [b]) {
|
|
@@ -6210,7 +6781,7 @@ function createParentBuilder(construct) {
|
|
|
6210
6781
|
let grandparent = null;
|
|
6211
6782
|
if (key !== "root") {
|
|
6212
6783
|
const found = result.find((namespace) => {
|
|
6213
|
-
const dirname =
|
|
6784
|
+
const dirname = posix4.dirname(key);
|
|
6214
6785
|
if (namespace.key === "root") {
|
|
6215
6786
|
return dirname === ".";
|
|
6216
6787
|
}
|
|
@@ -6280,13 +6851,13 @@ function normalizePort(port) {
|
|
|
6280
6851
|
}
|
|
6281
6852
|
|
|
6282
6853
|
// src/packages/application/utils/create-controller.ts
|
|
6283
|
-
import { posix as
|
|
6854
|
+
import { posix as posix5 } from "path";
|
|
6284
6855
|
function createController(constructor, opts) {
|
|
6285
6856
|
const { key, store, serializers } = opts;
|
|
6286
|
-
const namespace =
|
|
6857
|
+
const namespace = posix5.dirname(key).replace(".", "");
|
|
6287
6858
|
let { parent } = opts;
|
|
6288
6859
|
let model = tryCatchSync(
|
|
6289
|
-
() => store.modelFor(
|
|
6860
|
+
() => store.modelFor(posix5.basename(key))
|
|
6290
6861
|
);
|
|
6291
6862
|
let serializer = serializers.get(key);
|
|
6292
6863
|
if (!model) {
|
|
@@ -6342,13 +6913,13 @@ function createController(constructor, opts) {
|
|
|
6342
6913
|
}
|
|
6343
6914
|
|
|
6344
6915
|
// src/packages/application/utils/create-serializer.ts
|
|
6345
|
-
import { posix as
|
|
6916
|
+
import { posix as posix6 } from "path";
|
|
6346
6917
|
function createSerializer(constructor, opts) {
|
|
6347
6918
|
const { key, store } = opts;
|
|
6348
|
-
const namespace =
|
|
6919
|
+
const namespace = posix6.dirname(key).replace(".", "");
|
|
6349
6920
|
let { parent } = opts;
|
|
6350
6921
|
let model = tryCatchSync(
|
|
6351
|
-
() => store.modelFor(
|
|
6922
|
+
() => store.modelFor(posix6.basename(key))
|
|
6352
6923
|
);
|
|
6353
6924
|
if (!model) {
|
|
6354
6925
|
model = null;
|
|
@@ -6372,6 +6943,85 @@ function createSerializer(constructor, opts) {
|
|
|
6372
6943
|
return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
|
|
6373
6944
|
}
|
|
6374
6945
|
|
|
6946
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6947
|
+
import { posix as posix7 } from "path";
|
|
6948
|
+
|
|
6949
|
+
// src/errors/namespaced-serializer-missing-error.ts
|
|
6950
|
+
var NamespacedSerializerMissingError = class extends ReferenceError {
|
|
6951
|
+
/**
|
|
6952
|
+
* @param missing - Each missing serializer key (`admin/comments`) with an
|
|
6953
|
+
* example of how it is reached (`admin/posts?include=comments`).
|
|
6954
|
+
*/
|
|
6955
|
+
constructor(missing) {
|
|
6956
|
+
const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
|
|
6957
|
+
super(
|
|
6958
|
+
[
|
|
6959
|
+
"Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
|
|
6960
|
+
...lines
|
|
6961
|
+
].join("\n")
|
|
6962
|
+
);
|
|
6963
|
+
}
|
|
6964
|
+
};
|
|
6965
|
+
var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
|
|
6966
|
+
|
|
6967
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6968
|
+
var distance = (via) => {
|
|
6969
|
+
const [, include] = via.split("?include=");
|
|
6970
|
+
return include ? include.split(".").length : 0;
|
|
6971
|
+
};
|
|
6972
|
+
function validateNamespacedSerializers(controllers, serializers) {
|
|
6973
|
+
const missing = /* @__PURE__ */ new Map();
|
|
6974
|
+
controllers.forEach((controller, key) => {
|
|
6975
|
+
const { namespace, model } = controller;
|
|
6976
|
+
if (!namespace || !model) {
|
|
6977
|
+
return;
|
|
6978
|
+
}
|
|
6979
|
+
const application = controllers.get(posix7.join(namespace, "application"));
|
|
6980
|
+
if (!application || application.serializerFallback !== false) {
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
const require2 = (type, via) => {
|
|
6984
|
+
const serializerKey = posix7.join(namespace, type);
|
|
6985
|
+
const serializer = serializers.get(serializerKey);
|
|
6986
|
+
if (!serializer) {
|
|
6987
|
+
const known = missing.get(serializerKey);
|
|
6988
|
+
if (known === void 0 || distance(via) < distance(known)) {
|
|
6989
|
+
missing.set(serializerKey, via);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
return serializer;
|
|
6993
|
+
};
|
|
6994
|
+
const own = require2(model.resourceName, key);
|
|
6995
|
+
if (!own) {
|
|
6996
|
+
return;
|
|
6997
|
+
}
|
|
6998
|
+
const depth = Math.max(controller.maxIncludeDepth, 1);
|
|
6999
|
+
let level = [
|
|
7000
|
+
[model, own, []]
|
|
7001
|
+
];
|
|
7002
|
+
for (let current = 1; current <= depth && level.length; current += 1) {
|
|
7003
|
+
const next = [];
|
|
7004
|
+
level.forEach(([parent, serializer, path]) => {
|
|
7005
|
+
[...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
|
|
7006
|
+
const opts = parent.relationshipFor(name);
|
|
7007
|
+
if (!opts) {
|
|
7008
|
+
return;
|
|
7009
|
+
}
|
|
7010
|
+
const nextPath = [...path, name];
|
|
7011
|
+
const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
|
|
7012
|
+
if (related) {
|
|
7013
|
+
next.push([opts.model, related, nextPath]);
|
|
7014
|
+
}
|
|
7015
|
+
});
|
|
7016
|
+
});
|
|
7017
|
+
level = next;
|
|
7018
|
+
}
|
|
7019
|
+
});
|
|
7020
|
+
if (missing.size) {
|
|
7021
|
+
throw new namespaced_serializer_missing_error_default(missing);
|
|
7022
|
+
}
|
|
7023
|
+
}
|
|
7024
|
+
|
|
6375
7025
|
// src/packages/application/initialize.ts
|
|
6376
7026
|
async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
|
|
6377
7027
|
const load = createLoader(path);
|
|
@@ -6394,6 +7044,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6394
7044
|
parent
|
|
6395
7045
|
})
|
|
6396
7046
|
);
|
|
7047
|
+
serializers.forEach((serializer) => {
|
|
7048
|
+
Reflect.defineProperty(serializer, "serializers", {
|
|
7049
|
+
value: serializers,
|
|
7050
|
+
writable: false,
|
|
7051
|
+
enumerable: false,
|
|
7052
|
+
configurable: false
|
|
7053
|
+
});
|
|
7054
|
+
});
|
|
6397
7055
|
models.forEach((model) => {
|
|
6398
7056
|
Reflect.defineProperty(model, "serializer", {
|
|
6399
7057
|
value: closestChild(serializers, model.resourceName),
|
|
@@ -6419,6 +7077,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6419
7077
|
configurable: false
|
|
6420
7078
|
});
|
|
6421
7079
|
});
|
|
7080
|
+
validateNamespacedSerializers(controllers, serializers);
|
|
6422
7081
|
const ApplicationController = controllers.get("application");
|
|
6423
7082
|
if (!ApplicationController) {
|
|
6424
7083
|
throw new controller_missing_error_default("application");
|