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.js
CHANGED
|
@@ -43,6 +43,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
var import_chalk4 = require("chalk");
|
|
44
44
|
|
|
45
45
|
// src/packages/database/constants.ts
|
|
46
|
+
var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
|
|
47
|
+
var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
|
|
46
48
|
var VALID_DRIVERS = [
|
|
47
49
|
"pg",
|
|
48
50
|
"sqlite3",
|
|
@@ -892,7 +894,7 @@ function underscore(source = "", upper = false) {
|
|
|
892
894
|
var normalize_model_name_default = compose(import_inflection2.singularize, import_inflection2.dasherize, underscore);
|
|
893
895
|
|
|
894
896
|
// src/packages/database/model/index.ts
|
|
895
|
-
var
|
|
897
|
+
var import_inflection10 = require("inflection");
|
|
896
898
|
|
|
897
899
|
// src/packages/database/query/index.ts
|
|
898
900
|
var import_inflection5 = require("inflection");
|
|
@@ -1090,10 +1092,16 @@ async function buildResults({
|
|
|
1090
1092
|
}
|
|
1091
1093
|
);
|
|
1092
1094
|
}
|
|
1095
|
+
const missing = new Set(
|
|
1096
|
+
entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
|
|
1097
|
+
);
|
|
1098
|
+
const absent = Array.from(missing).filter(
|
|
1099
|
+
(name) => model.relationshipFor(name)?.type === "hasOne"
|
|
1100
|
+
);
|
|
1093
1101
|
const instance = Reflect.construct(model, [
|
|
1094
1102
|
entries(record).reduce((r, entry) => {
|
|
1095
1103
|
let [key, value] = entry;
|
|
1096
|
-
if (
|
|
1104
|
+
if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
|
|
1097
1105
|
return r;
|
|
1098
1106
|
} else if (key.indexOf(".") >= 0) {
|
|
1099
1107
|
const [a, b] = key.split(".");
|
|
@@ -1113,6 +1121,7 @@ async function buildResults({
|
|
|
1113
1121
|
};
|
|
1114
1122
|
}, {})
|
|
1115
1123
|
]);
|
|
1124
|
+
absent.forEach((name) => instance.absentRelationships.add(name));
|
|
1116
1125
|
instance.currentChangeSet.persist();
|
|
1117
1126
|
return instance;
|
|
1118
1127
|
});
|
|
@@ -1124,6 +1133,17 @@ function createRunner(target, opts) {
|
|
|
1124
1133
|
const { resolve: resolve2, reject } = opts;
|
|
1125
1134
|
let didRun = false;
|
|
1126
1135
|
RUNNERS.set(target, async () => {
|
|
1136
|
+
if (didRun) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
didRun = true;
|
|
1140
|
+
try {
|
|
1141
|
+
await run();
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
reject(err);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
async function run() {
|
|
1127
1147
|
let results;
|
|
1128
1148
|
const {
|
|
1129
1149
|
model,
|
|
@@ -1133,10 +1153,6 @@ function createRunner(target, opts) {
|
|
|
1133
1153
|
shouldCount,
|
|
1134
1154
|
relationships
|
|
1135
1155
|
} = target;
|
|
1136
|
-
if (didRun) {
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
didRun = true;
|
|
1140
1156
|
if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
|
|
1141
1157
|
target.select(...target.model.attributeNames);
|
|
1142
1158
|
}
|
|
@@ -1177,7 +1193,7 @@ function createRunner(target, opts) {
|
|
|
1177
1193
|
resolve2(result);
|
|
1178
1194
|
}
|
|
1179
1195
|
}
|
|
1180
|
-
}
|
|
1196
|
+
}
|
|
1181
1197
|
}
|
|
1182
1198
|
}
|
|
1183
1199
|
function runQuery(target) {
|
|
@@ -1882,7 +1898,9 @@ async function get(owner, key) {
|
|
|
1882
1898
|
let { foreignKey } = opts;
|
|
1883
1899
|
value = owner.currentChangeSet.get(key);
|
|
1884
1900
|
foreignKey = (0, import_inflection7.camelize)(foreignKey, true);
|
|
1885
|
-
if (!value) {
|
|
1901
|
+
if (!value && owner.absentRelationships.has(key)) {
|
|
1902
|
+
value = null;
|
|
1903
|
+
} else if (!value) {
|
|
1886
1904
|
switch (type) {
|
|
1887
1905
|
case "hasOne":
|
|
1888
1906
|
value = await getHasOne(owner, {
|
|
@@ -2544,13 +2562,37 @@ async function initializeClass({
|
|
|
2544
2562
|
return model;
|
|
2545
2563
|
}
|
|
2546
2564
|
|
|
2565
|
+
// src/packages/server/utils/source-for.ts
|
|
2566
|
+
var import_inflection9 = require("inflection");
|
|
2567
|
+
function memberName(key) {
|
|
2568
|
+
return (0, import_inflection9.dasherize)(underscore(key));
|
|
2569
|
+
}
|
|
2570
|
+
function escapePointer(segment) {
|
|
2571
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2572
|
+
}
|
|
2573
|
+
function sourceFor(path) {
|
|
2574
|
+
const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
|
|
2575
|
+
if (!root) {
|
|
2576
|
+
return {};
|
|
2577
|
+
} else if (root === "data") {
|
|
2578
|
+
return {
|
|
2579
|
+
pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2547
2587
|
// src/packages/database/validation/errors/validation-error.ts
|
|
2548
2588
|
var ValidationError = class extends Error {
|
|
2549
|
-
constructor(key
|
|
2550
|
-
super(`Validation failed for ${key}
|
|
2589
|
+
constructor(key) {
|
|
2590
|
+
super(`Validation failed for ${key}.`);
|
|
2591
|
+
this.key = key;
|
|
2592
|
+
this.source = sourceFor(`data.attributes.${key}`);
|
|
2551
2593
|
}
|
|
2552
2594
|
};
|
|
2553
|
-
var validation_error_default = ValidationError;
|
|
2595
|
+
var validation_error_default = createServerError(ValidationError, 422);
|
|
2554
2596
|
|
|
2555
2597
|
// src/packages/database/validation/index.ts
|
|
2556
2598
|
var Validation = class {
|
|
@@ -2590,12 +2632,32 @@ function validate(instance) {
|
|
|
2590
2632
|
validator: Reflect.get(instance.constructor.validates, key)
|
|
2591
2633
|
})).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
|
|
2592
2634
|
if (!validation.isValid()) {
|
|
2593
|
-
throw new validation_error_default(validation.key
|
|
2635
|
+
throw new validation_error_default(validation.key);
|
|
2594
2636
|
}
|
|
2595
2637
|
return result;
|
|
2596
2638
|
}, true);
|
|
2597
2639
|
}
|
|
2598
2640
|
|
|
2641
|
+
// src/packages/database/model/utils/process-write-error.ts
|
|
2642
|
+
function driverMessage(message) {
|
|
2643
|
+
const index = message.lastIndexOf(" - ");
|
|
2644
|
+
return index >= 0 ? message.slice(index + 3) : message;
|
|
2645
|
+
}
|
|
2646
|
+
function processWriteError(err) {
|
|
2647
|
+
if (!(err instanceof Error)) {
|
|
2648
|
+
return err;
|
|
2649
|
+
}
|
|
2650
|
+
const { code } = err;
|
|
2651
|
+
const { message } = err;
|
|
2652
|
+
if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
|
|
2653
|
+
return new unique_constraint_error_default(driverMessage(message));
|
|
2654
|
+
}
|
|
2655
|
+
return err;
|
|
2656
|
+
}
|
|
2657
|
+
function rethrowWriteError(err) {
|
|
2658
|
+
throw processWriteError(err);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2599
2661
|
// src/packages/database/model/utils/run-hooks.ts
|
|
2600
2662
|
function runHooks(record, trx, ...hooks) {
|
|
2601
2663
|
return hooks.filter((hook) => Boolean(hook)).reduce(
|
|
@@ -2635,6 +2697,12 @@ var Model = class {
|
|
|
2635
2697
|
writable: false,
|
|
2636
2698
|
enumerable: false,
|
|
2637
2699
|
configurable: false
|
|
2700
|
+
},
|
|
2701
|
+
absentRelationships: {
|
|
2702
|
+
value: /* @__PURE__ */ new Set(),
|
|
2703
|
+
writable: false,
|
|
2704
|
+
enumerable: false,
|
|
2705
|
+
configurable: false
|
|
2638
2706
|
}
|
|
2639
2707
|
});
|
|
2640
2708
|
const {
|
|
@@ -2981,9 +3049,9 @@ var Model = class {
|
|
|
2981
3049
|
return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
|
|
2982
3050
|
};
|
|
2983
3051
|
if (transaction) {
|
|
2984
|
-
return run(transaction);
|
|
3052
|
+
return run(transaction).catch(rethrowWriteError);
|
|
2985
3053
|
}
|
|
2986
|
-
return this.transaction(run);
|
|
3054
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
2987
3055
|
}
|
|
2988
3056
|
/**
|
|
2989
3057
|
* Permanently delete the instance from the database.
|
|
@@ -3103,9 +3171,9 @@ var Model = class {
|
|
|
3103
3171
|
return createTransactionResultProxy(instance, true);
|
|
3104
3172
|
};
|
|
3105
3173
|
if (transaction) {
|
|
3106
|
-
return run(transaction);
|
|
3174
|
+
return run(transaction).catch(rethrowWriteError);
|
|
3107
3175
|
}
|
|
3108
|
-
return this.transaction(run);
|
|
3176
|
+
return this.transaction(run).catch(rethrowWriteError);
|
|
3109
3177
|
}
|
|
3110
3178
|
/**
|
|
3111
3179
|
* Specify the transaction object to use for following save, update, or
|
|
@@ -3283,7 +3351,7 @@ var Model = class {
|
|
|
3283
3351
|
return Promise.resolve(this);
|
|
3284
3352
|
}
|
|
3285
3353
|
if (!this.tableName) {
|
|
3286
|
-
const getTableName = compose(
|
|
3354
|
+
const getTableName = compose(import_inflection10.pluralize, underscore);
|
|
3287
3355
|
const tableName = getTableName(this.name);
|
|
3288
3356
|
Reflect.defineProperty(this, "tableName", {
|
|
3289
3357
|
value: tableName,
|
|
@@ -3481,7 +3549,7 @@ function parseNestedObject(source) {
|
|
|
3481
3549
|
}
|
|
3482
3550
|
|
|
3483
3551
|
// src/packages/server/request/parser/utils/format.ts
|
|
3484
|
-
var
|
|
3552
|
+
var import_inflection12 = require("inflection");
|
|
3485
3553
|
|
|
3486
3554
|
// src/packages/server/request/parser/constants.ts
|
|
3487
3555
|
var INT = /^\d+$/;
|
|
@@ -3492,7 +3560,7 @@ var TRUE = /^true$/;
|
|
|
3492
3560
|
var BRACKETS = /(?:\[])/g;
|
|
3493
3561
|
|
|
3494
3562
|
// src/utils/transform-keys.ts
|
|
3495
|
-
var
|
|
3563
|
+
var import_inflection11 = require("inflection");
|
|
3496
3564
|
function transformKeys(source, transformer, deep = false) {
|
|
3497
3565
|
if (Array.isArray(source)) {
|
|
3498
3566
|
return source.slice(0);
|
|
@@ -3516,14 +3584,14 @@ function transformKeys(source, transformer, deep = false) {
|
|
|
3516
3584
|
function camelizeKeys(source, deep) {
|
|
3517
3585
|
return transformKeys(
|
|
3518
3586
|
source,
|
|
3519
|
-
(key) => (0,
|
|
3587
|
+
(key) => (0, import_inflection11.camelize)(underscore(key), true),
|
|
3520
3588
|
deep
|
|
3521
3589
|
);
|
|
3522
3590
|
}
|
|
3523
3591
|
function dasherizeKeys(source, deep) {
|
|
3524
3592
|
return transformKeys(
|
|
3525
3593
|
source,
|
|
3526
|
-
(key) => (0,
|
|
3594
|
+
(key) => (0, import_inflection11.dasherize)(underscore(key)),
|
|
3527
3595
|
deep
|
|
3528
3596
|
);
|
|
3529
3597
|
}
|
|
@@ -3538,7 +3606,7 @@ function makeArray(source) {
|
|
|
3538
3606
|
function formatString(source, method) {
|
|
3539
3607
|
if (method === "GET") {
|
|
3540
3608
|
if (source.indexOf(",") >= 0) {
|
|
3541
|
-
return source.split(",").map((str) => (0,
|
|
3609
|
+
return source.split(",").map((str) => (0, import_inflection12.camelize)(underscore(str), true));
|
|
3542
3610
|
} else if (INT.test(source)) {
|
|
3543
3611
|
return Number.parseInt(source, 10);
|
|
3544
3612
|
} else if (BOOL.test(source)) {
|
|
@@ -3565,9 +3633,9 @@ function formatObject(source, method, formatter) {
|
|
|
3565
3633
|
}
|
|
3566
3634
|
function formatSort(sort) {
|
|
3567
3635
|
if (sort.startsWith("-")) {
|
|
3568
|
-
return `-${(0,
|
|
3636
|
+
return `-${(0, import_inflection12.camelize)(underscore(sort.substr(1)), true)}`;
|
|
3569
3637
|
}
|
|
3570
|
-
return (0,
|
|
3638
|
+
return (0, import_inflection12.camelize)(underscore(sort), true);
|
|
3571
3639
|
}
|
|
3572
3640
|
function formatFields(fields) {
|
|
3573
3641
|
return entries(fields).reduce(
|
|
@@ -3832,6 +3900,10 @@ function dataFor(status, err) {
|
|
|
3832
3900
|
if (title) {
|
|
3833
3901
|
errData.title = title;
|
|
3834
3902
|
}
|
|
3903
|
+
const source = err?.source;
|
|
3904
|
+
if (source && (source.pointer || source.parameter)) {
|
|
3905
|
+
errData.source = source;
|
|
3906
|
+
}
|
|
3835
3907
|
if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
|
|
3836
3908
|
errData.detail = err.message.replace(/^\[public\]/gi, "");
|
|
3837
3909
|
}
|
|
@@ -4054,9 +4126,10 @@ function merge(dest, source) {
|
|
|
4054
4126
|
}
|
|
4055
4127
|
|
|
4056
4128
|
// src/packages/controller/utils/params-to-query.ts
|
|
4057
|
-
function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
4129
|
+
function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
|
|
4058
4130
|
const relationships = entries(model.relationships);
|
|
4059
4131
|
const includedFields = omit(fields, model.resourceName);
|
|
4132
|
+
const included = include && include.map((path) => path.split(".")[0]);
|
|
4060
4133
|
let query = {
|
|
4061
4134
|
id,
|
|
4062
4135
|
filter,
|
|
@@ -4099,9 +4172,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4099
4172
|
if (!value.includes(relationship.model.primaryKey)) {
|
|
4100
4173
|
value = [relationship.model.primaryKey, ...value];
|
|
4101
4174
|
}
|
|
4102
|
-
if (
|
|
4103
|
-
value = [...value, ...relationship.model.
|
|
4104
|
-
} else if (!
|
|
4175
|
+
if (included && value.length === 1 && included.includes(name)) {
|
|
4176
|
+
value = [...value, ...serializerFor(relationship.model).attributes];
|
|
4177
|
+
} else if (!included && value.length > 1) {
|
|
4105
4178
|
value = value.slice(0, 1);
|
|
4106
4179
|
}
|
|
4107
4180
|
return {
|
|
@@ -4116,18 +4189,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4116
4189
|
}
|
|
4117
4190
|
|
|
4118
4191
|
// src/packages/controller/utils/find-one.ts
|
|
4119
|
-
function findOne(model, req) {
|
|
4192
|
+
function findOne(model, req, serializerFor) {
|
|
4120
4193
|
const params = merge(req.defaultParams, req.params);
|
|
4121
|
-
const { id, select, include } = paramsToQuery(model, params);
|
|
4194
|
+
const { id, select, include } = paramsToQuery(model, params, serializerFor);
|
|
4122
4195
|
return model.find(id).select(...select).include(include);
|
|
4123
4196
|
}
|
|
4124
4197
|
|
|
4125
4198
|
// src/packages/controller/utils/find-many.ts
|
|
4126
|
-
function findMany(model, req) {
|
|
4199
|
+
function findMany(model, req, serializerFor) {
|
|
4127
4200
|
const params = merge(req.defaultParams, req.params);
|
|
4128
4201
|
const { sort, page, limit, select, filter, include } = paramsToQuery(
|
|
4129
4202
|
model,
|
|
4130
|
-
params
|
|
4203
|
+
params,
|
|
4204
|
+
serializerFor
|
|
4131
4205
|
);
|
|
4132
4206
|
return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
|
|
4133
4207
|
}
|
|
@@ -4157,6 +4231,53 @@ function resolveRelationships(model, relationships = {}) {
|
|
|
4157
4231
|
);
|
|
4158
4232
|
}
|
|
4159
4233
|
|
|
4234
|
+
// src/packages/controller/errors/related-record-not-found-error.ts
|
|
4235
|
+
var RelatedRecordNotFoundError = class extends Error {
|
|
4236
|
+
constructor({ name, primaryKey }, primaryKeyValue, path) {
|
|
4237
|
+
super(
|
|
4238
|
+
`Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
|
|
4239
|
+
);
|
|
4240
|
+
this.source = sourceFor(path);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
|
|
4244
|
+
|
|
4245
|
+
// src/packages/controller/utils/validate-relationships.ts
|
|
4246
|
+
async function validateRelationships(model, relationships = {}) {
|
|
4247
|
+
await Promise.all(
|
|
4248
|
+
entries(relationships).map(async ([key, value]) => {
|
|
4249
|
+
const opts = model.relationshipFor(key);
|
|
4250
|
+
const { data = null } = value || {};
|
|
4251
|
+
if (!opts || !data) {
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const { model: related } = opts;
|
|
4255
|
+
const isMany = Array.isArray(data);
|
|
4256
|
+
const ids = (isMany ? data : [data]).map(
|
|
4257
|
+
(item) => Reflect.get(Object(item), "id")
|
|
4258
|
+
);
|
|
4259
|
+
if (!ids.length) {
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
const column = related.columnNameFor(related.primaryKey) || "id";
|
|
4263
|
+
const rows = await related.table().whereIn(
|
|
4264
|
+
`${related.tableName}.${column}`,
|
|
4265
|
+
ids.filter((id) => id != null)
|
|
4266
|
+
).select(`${related.tableName}.${column}`);
|
|
4267
|
+
const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
|
|
4268
|
+
const missing = ids.findIndex((id) => !found.has(String(id)));
|
|
4269
|
+
if (missing >= 0) {
|
|
4270
|
+
const path = `data.relationships.${key}.data`;
|
|
4271
|
+
throw new related_record_not_found_error_default(
|
|
4272
|
+
related,
|
|
4273
|
+
ids[missing],
|
|
4274
|
+
isMany ? `${path}.${missing}` : path
|
|
4275
|
+
);
|
|
4276
|
+
}
|
|
4277
|
+
})
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4160
4281
|
// src/packages/controller/constants.ts
|
|
4161
4282
|
var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
|
|
4162
4283
|
|
|
@@ -4342,6 +4463,74 @@ var Controller = class {
|
|
|
4342
4463
|
* @public
|
|
4343
4464
|
*/
|
|
4344
4465
|
defaultPerPage = 25;
|
|
4466
|
+
/**
|
|
4467
|
+
* How many relationships deep an `?include` path may go on this
|
|
4468
|
+
* controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
|
|
4469
|
+
* only direct relationships (`comments`) can be included. Paths deeper than
|
|
4470
|
+
* this are rejected with `400 Bad Request`.
|
|
4471
|
+
*
|
|
4472
|
+
* Set it on `ApplicationController` to change it for the whole app, or on a
|
|
4473
|
+
* single controller to override it there.
|
|
4474
|
+
*
|
|
4475
|
+
* ```javascript
|
|
4476
|
+
* class ApplicationController extends Controller {
|
|
4477
|
+
* maxIncludeDepth = 2;
|
|
4478
|
+
* }
|
|
4479
|
+
* ```
|
|
4480
|
+
*
|
|
4481
|
+
* Every allowed path is enumerated up front from the serializers'
|
|
4482
|
+
* relationships, and each nested level costs its own queries per request, so
|
|
4483
|
+
* keep this small.
|
|
4484
|
+
*
|
|
4485
|
+
* @property maxIncludeDepth
|
|
4486
|
+
* @type {Number}
|
|
4487
|
+
* @default 3
|
|
4488
|
+
* @public
|
|
4489
|
+
*/
|
|
4490
|
+
maxIncludeDepth = 3;
|
|
4491
|
+
/**
|
|
4492
|
+
* Whether a namespace may fall back to the root Serializer of a type it has
|
|
4493
|
+
* no Serializer for. Read from a namespace's `ApplicationController` and
|
|
4494
|
+
* applies to the whole namespace.
|
|
4495
|
+
*
|
|
4496
|
+
* By default `app/controllers/admin/comments.js` without an
|
|
4497
|
+
* `app/serializers/admin/comments.js` — or an included type without one —
|
|
4498
|
+
* is serialized by the root Serializer, with every attribute and
|
|
4499
|
+
* relationship it declares. For a namespace that must only expose what it
|
|
4500
|
+
* declares itself, turn the fallback off:
|
|
4501
|
+
*
|
|
4502
|
+
* ```javascript
|
|
4503
|
+
* // app/controllers/admin/application.js
|
|
4504
|
+
* class AdminApplicationController extends ApplicationController {
|
|
4505
|
+
* serializerFallback = false;
|
|
4506
|
+
* }
|
|
4507
|
+
* ```
|
|
4508
|
+
*
|
|
4509
|
+
* The application then refuses to boot while any type the namespace can
|
|
4510
|
+
* serialize or `include` (down to each controller's `maxIncludeDepth`) has
|
|
4511
|
+
* no Serializer in that namespace, listing each missing one.
|
|
4512
|
+
*
|
|
4513
|
+
* @property serializerFallback
|
|
4514
|
+
* @type {Boolean}
|
|
4515
|
+
* @default true
|
|
4516
|
+
* @public
|
|
4517
|
+
*/
|
|
4518
|
+
serializerFallback = true;
|
|
4519
|
+
/**
|
|
4520
|
+
* The Serializer to serialize (and validate, and load) related resources of
|
|
4521
|
+
* this Controller's responses with: the related model's Serializer in this
|
|
4522
|
+
* Controller's namespace, falling back to the root one.
|
|
4523
|
+
*
|
|
4524
|
+
* Always this Controller's namespace — not its Serializer's, which is the
|
|
4525
|
+
* root one when the namespace has no Serializer for this resource.
|
|
4526
|
+
*
|
|
4527
|
+
* @method serializerFor
|
|
4528
|
+
* @private
|
|
4529
|
+
*/
|
|
4530
|
+
serializerFor(model) {
|
|
4531
|
+
const { serializer, namespace } = this;
|
|
4532
|
+
return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
|
|
4533
|
+
}
|
|
4345
4534
|
constructor({ model, namespace, serializer }) {
|
|
4346
4535
|
Object.assign(this, {
|
|
4347
4536
|
model,
|
|
@@ -4367,7 +4556,7 @@ var Controller = class {
|
|
|
4367
4556
|
* @public
|
|
4368
4557
|
*/
|
|
4369
4558
|
index(req) {
|
|
4370
|
-
return findMany(this.model, req);
|
|
4559
|
+
return findMany(this.model, req, (related) => this.serializerFor(related));
|
|
4371
4560
|
}
|
|
4372
4561
|
/**
|
|
4373
4562
|
* This method supports including relationships, and sparse fieldsets via
|
|
@@ -4382,7 +4571,7 @@ var Controller = class {
|
|
|
4382
4571
|
* @public
|
|
4383
4572
|
*/
|
|
4384
4573
|
show(req) {
|
|
4385
|
-
return findOne(this.model, req);
|
|
4574
|
+
return findOne(this.model, req, (related) => this.serializerFor(related));
|
|
4386
4575
|
}
|
|
4387
4576
|
/**
|
|
4388
4577
|
* Create and return a single Model instance that the Controller instance
|
|
@@ -4403,6 +4592,7 @@ var Controller = class {
|
|
|
4403
4592
|
data: { attributes, relationships }
|
|
4404
4593
|
}
|
|
4405
4594
|
} = req;
|
|
4595
|
+
await validateRelationships(model, relationships);
|
|
4406
4596
|
const record = await model.create({
|
|
4407
4597
|
...attributes,
|
|
4408
4598
|
...resolveRelationships(model, relationships)
|
|
@@ -4428,12 +4618,13 @@ var Controller = class {
|
|
|
4428
4618
|
*/
|
|
4429
4619
|
update(req) {
|
|
4430
4620
|
const { model } = this;
|
|
4431
|
-
return findOne(model, req).then((record) => {
|
|
4621
|
+
return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
|
|
4432
4622
|
const {
|
|
4433
4623
|
params: {
|
|
4434
4624
|
data: { attributes, relationships }
|
|
4435
4625
|
}
|
|
4436
4626
|
} = req;
|
|
4627
|
+
await validateRelationships(model, relationships);
|
|
4437
4628
|
return record.update({
|
|
4438
4629
|
...attributes,
|
|
4439
4630
|
...resolveRelationships(model, relationships)
|
|
@@ -4457,7 +4648,7 @@ var Controller = class {
|
|
|
4457
4648
|
* @public
|
|
4458
4649
|
*/
|
|
4459
4650
|
destroy(req) {
|
|
4460
|
-
return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
|
|
4651
|
+
return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
|
|
4461
4652
|
}
|
|
4462
4653
|
/**
|
|
4463
4654
|
* Respond to HEAD or OPTIONS requests.
|
|
@@ -4475,7 +4666,194 @@ var Controller = class {
|
|
|
4475
4666
|
var controller_default = Controller;
|
|
4476
4667
|
|
|
4477
4668
|
// src/packages/serializer/index.ts
|
|
4478
|
-
var
|
|
4669
|
+
var import_path3 = require("path");
|
|
4670
|
+
var import_inflection14 = require("inflection");
|
|
4671
|
+
|
|
4672
|
+
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
4673
|
+
var import_path2 = require("path");
|
|
4674
|
+
function closestAncestor(source, key) {
|
|
4675
|
+
const name = import_path2.posix.basename(key);
|
|
4676
|
+
let namespace = import_path2.posix.dirname(key);
|
|
4677
|
+
if (namespace === ".") {
|
|
4678
|
+
return source.get(name);
|
|
4679
|
+
}
|
|
4680
|
+
namespace = import_path2.posix.dirname(namespace);
|
|
4681
|
+
const ancestor = source.get(import_path2.posix.join(namespace, name));
|
|
4682
|
+
if (ancestor) {
|
|
4683
|
+
return ancestor;
|
|
4684
|
+
}
|
|
4685
|
+
return closestAncestor(source, import_path2.posix.join(import_path2.posix.dirname(namespace), name));
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
// src/packages/serializer/utils/load-linkage.ts
|
|
4689
|
+
var import_inflection13 = require("inflection");
|
|
4690
|
+
var valueOf = (record, key) => Reflect.get(record, key);
|
|
4691
|
+
var toId = (value) => value == null ? null : String(value);
|
|
4692
|
+
async function loadLinkage(model, records, names) {
|
|
4693
|
+
const ids = Array.from(
|
|
4694
|
+
new Set(records.map((record) => record.getPrimaryKey()))
|
|
4695
|
+
);
|
|
4696
|
+
const linkage = /* @__PURE__ */ new Map();
|
|
4697
|
+
const belongsTo = [];
|
|
4698
|
+
const queries = [];
|
|
4699
|
+
ids.forEach((id) => {
|
|
4700
|
+
linkage.set(
|
|
4701
|
+
String(id),
|
|
4702
|
+
names.reduce((result, name) => {
|
|
4703
|
+
const opts = model.relationshipFor(name);
|
|
4704
|
+
result[name] = opts && opts.type === "hasMany" ? [] : null;
|
|
4705
|
+
return result;
|
|
4706
|
+
}, {})
|
|
4707
|
+
);
|
|
4708
|
+
});
|
|
4709
|
+
if (!ids.length) {
|
|
4710
|
+
return linkage;
|
|
4711
|
+
}
|
|
4712
|
+
const link = (ownerId, name, relatedId) => {
|
|
4713
|
+
const owner = linkage.get(String(ownerId));
|
|
4714
|
+
const id = toId(relatedId);
|
|
4715
|
+
if (!owner || id === null) {
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
const current = owner[name];
|
|
4719
|
+
if (Array.isArray(current)) {
|
|
4720
|
+
if (!current.includes(id)) {
|
|
4721
|
+
current.push(id);
|
|
4722
|
+
}
|
|
4723
|
+
} else if (current === null) {
|
|
4724
|
+
owner[name] = id;
|
|
4725
|
+
}
|
|
4726
|
+
};
|
|
4727
|
+
names.forEach((name) => {
|
|
4728
|
+
const opts = model.relationshipFor(name);
|
|
4729
|
+
if (!opts) {
|
|
4730
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
const { type, through, model: related } = opts;
|
|
4733
|
+
const foreignKey = (0, import_inflection13.camelize)(opts.foreignKey, true);
|
|
4734
|
+
if (type === "belongsTo") {
|
|
4735
|
+
belongsTo.push([name, foreignKey, related]);
|
|
4736
|
+
} else if (through) {
|
|
4737
|
+
const inverse = related.relationshipFor(opts.inverse);
|
|
4738
|
+
if (!inverse) {
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
const relatedKey = (0, import_inflection13.camelize)(inverse.foreignKey, true);
|
|
4742
|
+
queries.push(
|
|
4743
|
+
(async () => {
|
|
4744
|
+
const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
|
|
4745
|
+
rows.forEach((row) => {
|
|
4746
|
+
link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
|
|
4747
|
+
});
|
|
4748
|
+
})()
|
|
4749
|
+
);
|
|
4750
|
+
} else {
|
|
4751
|
+
queries.push(
|
|
4752
|
+
(async () => {
|
|
4753
|
+
const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
|
|
4754
|
+
rows.forEach((row) => {
|
|
4755
|
+
link(valueOf(row, foreignKey), name, row.getPrimaryKey());
|
|
4756
|
+
});
|
|
4757
|
+
})()
|
|
4758
|
+
);
|
|
4759
|
+
}
|
|
4760
|
+
});
|
|
4761
|
+
if (belongsTo.length) {
|
|
4762
|
+
queries.push(
|
|
4763
|
+
(async () => {
|
|
4764
|
+
const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
|
|
4765
|
+
rows.forEach((row) => {
|
|
4766
|
+
belongsTo.forEach(([name, key]) => {
|
|
4767
|
+
link(row.getPrimaryKey(), name, valueOf(row, key));
|
|
4768
|
+
});
|
|
4769
|
+
});
|
|
4770
|
+
await dropDangling(linkage, belongsTo);
|
|
4771
|
+
})()
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
await Promise.all(queries);
|
|
4775
|
+
return linkage;
|
|
4776
|
+
}
|
|
4777
|
+
async function dropDangling(linkage, belongsTo) {
|
|
4778
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
4779
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4780
|
+
const ids = wanted.get(related) || /* @__PURE__ */ new Set();
|
|
4781
|
+
linkage.forEach(({ [name]: id }) => {
|
|
4782
|
+
if (typeof id === "string") {
|
|
4783
|
+
ids.add(id);
|
|
4784
|
+
}
|
|
4785
|
+
});
|
|
4786
|
+
wanted.set(related, ids);
|
|
4787
|
+
});
|
|
4788
|
+
const existing = /* @__PURE__ */ new Map();
|
|
4789
|
+
await Promise.all(
|
|
4790
|
+
Array.from(wanted, async ([related, ids]) => {
|
|
4791
|
+
if (!ids.size) {
|
|
4792
|
+
existing.set(related, ids);
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
|
|
4796
|
+
existing.set(
|
|
4797
|
+
related,
|
|
4798
|
+
new Set(rows.map((row) => String(row.getPrimaryKey())))
|
|
4799
|
+
);
|
|
4800
|
+
})
|
|
4801
|
+
);
|
|
4802
|
+
belongsTo.forEach(([name, , related]) => {
|
|
4803
|
+
const found = existing.get(related);
|
|
4804
|
+
linkage.forEach((owner) => {
|
|
4805
|
+
const id = owner[name];
|
|
4806
|
+
if (typeof id === "string" && !found?.has(id)) {
|
|
4807
|
+
owner[name] = null;
|
|
4808
|
+
}
|
|
4809
|
+
});
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// src/packages/serializer/utils/include-tree.ts
|
|
4814
|
+
function createIncludeTree(paths = []) {
|
|
4815
|
+
const tree = /* @__PURE__ */ new Map();
|
|
4816
|
+
paths.forEach((path) => {
|
|
4817
|
+
path.split(".").filter(Boolean).reduce((node, name) => {
|
|
4818
|
+
let child = node.get(name);
|
|
4819
|
+
if (!child) {
|
|
4820
|
+
child = /* @__PURE__ */ new Map();
|
|
4821
|
+
node.set(name, child);
|
|
4822
|
+
}
|
|
4823
|
+
return child;
|
|
4824
|
+
}, tree);
|
|
4825
|
+
});
|
|
4826
|
+
return tree;
|
|
4827
|
+
}
|
|
4828
|
+
function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
|
|
4829
|
+
if (depth < 1) {
|
|
4830
|
+
return [];
|
|
4831
|
+
}
|
|
4832
|
+
return names.reduce((paths, name) => {
|
|
4833
|
+
const opts = model.relationshipFor(name);
|
|
4834
|
+
if (!opts) {
|
|
4835
|
+
return paths;
|
|
4836
|
+
}
|
|
4837
|
+
const { model: related } = opts;
|
|
4838
|
+
const serializer = serializerFor(related);
|
|
4839
|
+
if (!serializer) {
|
|
4840
|
+
return [...paths, name];
|
|
4841
|
+
}
|
|
4842
|
+
const { hasOne, hasMany } = serializer;
|
|
4843
|
+
return [
|
|
4844
|
+
...paths,
|
|
4845
|
+
name,
|
|
4846
|
+
...enumerateIncludePaths(
|
|
4847
|
+
related,
|
|
4848
|
+
[...hasOne, ...hasMany],
|
|
4849
|
+
depth - 1,
|
|
4850
|
+
serializerFor
|
|
4851
|
+
).map((path) => `${name}.${path}`)
|
|
4852
|
+
];
|
|
4853
|
+
}, []);
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
// src/packages/serializer/index.ts
|
|
4479
4857
|
var Serializer = class {
|
|
4480
4858
|
/**
|
|
4481
4859
|
* An Array of the `hasOne` or `belongsTo` relationships on a Serializer
|
|
@@ -4562,9 +4940,17 @@ var Serializer = class {
|
|
|
4562
4940
|
* the resource and relationship objects in the returned [JSON API](
|
|
4563
4941
|
* http://jsonapi.org) document object.
|
|
4564
4942
|
*
|
|
4565
|
-
* @param {Array} options.include - An array of
|
|
4566
|
-
*
|
|
4567
|
-
* the returned [JSON API](http://jsonapi.org)
|
|
4943
|
+
* @param {Array} options.include - An array of relationship paths (e.g.
|
|
4944
|
+
* `'comments'` or `'comments.user'`) whose resources should be added to the
|
|
4945
|
+
* top level included object of the returned [JSON API](http://jsonapi.org)
|
|
4946
|
+
* document object. Intermediate resources of a nested path are included too.
|
|
4947
|
+
*
|
|
4948
|
+
* @param {String} options.namespace - The namespace of the request, i.e. of
|
|
4949
|
+
* the Controller handling it. Every link in the document is built in it, and
|
|
4950
|
+
* included resources are serialized by their Serializer in it (falling back
|
|
4951
|
+
* to the root). Defaults to this Serializer's namespace — which is the root
|
|
4952
|
+
* one when a namespaced Controller has no Serializer of its own, so the
|
|
4953
|
+
* Controller passes its namespace explicitly.
|
|
4568
4954
|
*
|
|
4569
4955
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
|
|
4570
4956
|
* object.
|
|
@@ -4575,38 +4961,61 @@ var Serializer = class {
|
|
|
4575
4961
|
data,
|
|
4576
4962
|
links,
|
|
4577
4963
|
domain,
|
|
4578
|
-
include
|
|
4964
|
+
include,
|
|
4965
|
+
namespace = this.namespace
|
|
4579
4966
|
}) {
|
|
4580
|
-
|
|
4581
|
-
const
|
|
4967
|
+
const tree = createIncludeTree(include);
|
|
4968
|
+
const related = /* @__PURE__ */ new Map();
|
|
4969
|
+
const included = /* @__PURE__ */ new Map();
|
|
4970
|
+
const names = Array.from(tree.keys());
|
|
4971
|
+
let primary;
|
|
4972
|
+
let serialized;
|
|
4582
4973
|
if (Array.isArray(data)) {
|
|
4583
|
-
|
|
4584
|
-
data
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
)
|
|
4974
|
+
primary = await Promise.all(
|
|
4975
|
+
data.map(
|
|
4976
|
+
(item) => this.formatOne({
|
|
4977
|
+
item,
|
|
4978
|
+
domain,
|
|
4979
|
+
related,
|
|
4980
|
+
namespace,
|
|
4981
|
+
include: names
|
|
4982
|
+
})
|
|
4593
4983
|
)
|
|
4594
|
-
|
|
4984
|
+
);
|
|
4985
|
+
serialized = { data: primary };
|
|
4595
4986
|
} else {
|
|
4596
|
-
|
|
4597
|
-
|
|
4987
|
+
const resource2 = await this.formatOne({
|
|
4988
|
+
domain,
|
|
4989
|
+
related,
|
|
4990
|
+
namespace,
|
|
4991
|
+
item: data,
|
|
4992
|
+
include: names,
|
|
4993
|
+
links: false
|
|
4994
|
+
});
|
|
4995
|
+
primary = [resource2];
|
|
4996
|
+
serialized = { data: resource2 };
|
|
4997
|
+
}
|
|
4998
|
+
for (const [name, children] of tree) {
|
|
4999
|
+
const opts = this.model.relationshipFor(name);
|
|
5000
|
+
const records = related.get(name);
|
|
5001
|
+
if (opts && records) {
|
|
5002
|
+
await this.addIncluded({
|
|
4598
5003
|
domain,
|
|
4599
|
-
|
|
5004
|
+
records,
|
|
4600
5005
|
included,
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
5006
|
+
namespace,
|
|
5007
|
+
model: opts.model,
|
|
5008
|
+
tree: children
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
4605
5011
|
}
|
|
4606
|
-
|
|
5012
|
+
primary.forEach((resource2) => {
|
|
5013
|
+
included.delete(resourceKey(resource2));
|
|
5014
|
+
});
|
|
5015
|
+
if (included.size) {
|
|
4607
5016
|
serialized = {
|
|
4608
5017
|
...serialized,
|
|
4609
|
-
included:
|
|
5018
|
+
included: Array.from(included.values())
|
|
4610
5019
|
};
|
|
4611
5020
|
}
|
|
4612
5021
|
return {
|
|
@@ -4621,6 +5030,12 @@ var Serializer = class {
|
|
|
4621
5030
|
* Transform a single Model instance into a [JSON API](http://jsonapi.org)
|
|
4622
5031
|
* resource object.
|
|
4623
5032
|
*
|
|
5033
|
+
* Relationships are serialized in one of two ways. By default each one is
|
|
5034
|
+
* read from the Model instance (for primary data the query has already
|
|
5035
|
+
* loaded them). When `linkage` is given — as it is for included resources,
|
|
5036
|
+
* whose relationships are batch-loaded by `addIncluded()` — the resource
|
|
5037
|
+
* linkage is built from it instead, without touching the database.
|
|
5038
|
+
*
|
|
4624
5039
|
* @method formatOne
|
|
4625
5040
|
*
|
|
4626
5041
|
* @param {Object} options - An options object used for building the returned
|
|
@@ -4637,17 +5052,19 @@ var Serializer = class {
|
|
|
4637
5052
|
* the top level links object or relationship links objects in the returned
|
|
4638
5053
|
* [JSON API](http://jsonapi.org) resource object.
|
|
4639
5054
|
*
|
|
4640
|
-
* @param {Array} options.include - An array of
|
|
4641
|
-
*
|
|
4642
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
5055
|
+
* @param {Array} options.include - An array of the relationship keys whose
|
|
5056
|
+
* related records should be collected into `options.related`.
|
|
4643
5057
|
*
|
|
4644
|
-
* @param {
|
|
4645
|
-
*
|
|
4646
|
-
* included
|
|
5058
|
+
* @param {Map} options.related - Collects, per relationship key in
|
|
5059
|
+
* `options.include`, the related Model instances that belong in the top
|
|
5060
|
+
* level included object of a [JSON API](http://jsonapi.org) document object.
|
|
4647
5061
|
*
|
|
4648
|
-
* @param {
|
|
4649
|
-
*
|
|
4650
|
-
*
|
|
5062
|
+
* @param {Object} options.linkage - Pre-loaded resource linkage (related
|
|
5063
|
+
* primary keys per relationship key) to serialize relationships from.
|
|
5064
|
+
*
|
|
5065
|
+
* @param {String} options.namespace - The namespace to build links in.
|
|
5066
|
+
* Defaults to this Serializer's; included resources pass the namespace of
|
|
5067
|
+
* the request, so every link in a document points into the same namespace.
|
|
4651
5068
|
*
|
|
4652
5069
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
|
|
4653
5070
|
* object.
|
|
@@ -4658,13 +5075,15 @@ var Serializer = class {
|
|
|
4658
5075
|
item,
|
|
4659
5076
|
links,
|
|
4660
5077
|
domain,
|
|
4661
|
-
include,
|
|
4662
|
-
|
|
4663
|
-
|
|
5078
|
+
include = [],
|
|
5079
|
+
related,
|
|
5080
|
+
linkage,
|
|
5081
|
+
namespace = this.namespace
|
|
4664
5082
|
}) {
|
|
4665
5083
|
const { resourceName: type } = item;
|
|
4666
5084
|
const id = String(item.getPrimaryKey());
|
|
4667
|
-
|
|
5085
|
+
const names = [...this.hasOne, ...this.hasMany];
|
|
5086
|
+
let relationships;
|
|
4668
5087
|
const attributes = dasherizeKeys(
|
|
4669
5088
|
item.getAttributes(
|
|
4670
5089
|
...Object.keys(item.rawColumnData).filter(
|
|
@@ -4677,34 +5096,41 @@ var Serializer = class {
|
|
|
4677
5096
|
type,
|
|
4678
5097
|
attributes
|
|
4679
5098
|
};
|
|
4680
|
-
if (
|
|
5099
|
+
if (linkage) {
|
|
5100
|
+
relationships = names.reduce(
|
|
5101
|
+
(hash, name) => ({
|
|
5102
|
+
...hash,
|
|
5103
|
+
[(0, import_inflection14.dasherize)(underscore(name))]: this.formatLinkage(
|
|
5104
|
+
domain,
|
|
5105
|
+
this.model.relationshipFor(name)?.model.resourceName,
|
|
5106
|
+
linkage[name],
|
|
5107
|
+
namespace
|
|
5108
|
+
)
|
|
5109
|
+
}),
|
|
5110
|
+
{}
|
|
5111
|
+
);
|
|
5112
|
+
} else {
|
|
5113
|
+
const collect = (name, records) => {
|
|
5114
|
+
if (related && include.includes(name)) {
|
|
5115
|
+
related.set(name, [...related.get(name) || [], ...records]);
|
|
5116
|
+
}
|
|
5117
|
+
};
|
|
4681
5118
|
relationships = await promiseHash(
|
|
4682
|
-
|
|
5119
|
+
names.reduce(
|
|
4683
5120
|
(hash, name) => ({
|
|
4684
5121
|
...hash,
|
|
4685
|
-
[(0,
|
|
4686
|
-
const
|
|
4687
|
-
if (Array.isArray(
|
|
5122
|
+
[(0, import_inflection14.dasherize)(underscore(name))]: (async () => {
|
|
5123
|
+
const value = await Reflect.get(item, name);
|
|
5124
|
+
if (Array.isArray(value)) {
|
|
5125
|
+
collect(name, value);
|
|
4688
5126
|
return {
|
|
4689
|
-
data:
|
|
4690
|
-
|
|
4691
|
-
const { data: relatedData } = await this.formatRelationship({
|
|
4692
|
-
domain,
|
|
4693
|
-
included,
|
|
4694
|
-
item: relatedItem,
|
|
4695
|
-
include: include.includes(name)
|
|
4696
|
-
});
|
|
4697
|
-
return relatedData;
|
|
4698
|
-
})
|
|
5127
|
+
data: value.map(
|
|
5128
|
+
(relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
|
|
4699
5129
|
)
|
|
4700
5130
|
};
|
|
4701
|
-
} else if (
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
included,
|
|
4705
|
-
item: related,
|
|
4706
|
-
include: include.includes(name)
|
|
4707
|
-
});
|
|
5131
|
+
} else if (value && value.id != null) {
|
|
5132
|
+
collect(name, [value]);
|
|
5133
|
+
return this.formatRelationship(value, domain, namespace);
|
|
4708
5134
|
}
|
|
4709
5135
|
return {
|
|
4710
5136
|
data: null
|
|
@@ -4719,16 +5145,9 @@ var Serializer = class {
|
|
|
4719
5145
|
serialized.relationships = relationships;
|
|
4720
5146
|
}
|
|
4721
5147
|
if (links || typeof links !== "boolean") {
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4726
|
-
};
|
|
4727
|
-
} else {
|
|
4728
|
-
serialized.links = {
|
|
4729
|
-
self: `${domain}/${type}/${id}`
|
|
4730
|
-
};
|
|
4731
|
-
}
|
|
5148
|
+
serialized.links = {
|
|
5149
|
+
self: this.linkFor(domain, type, id, namespace)
|
|
5150
|
+
};
|
|
4732
5151
|
}
|
|
4733
5152
|
return serialized;
|
|
4734
5153
|
}
|
|
@@ -4738,70 +5157,167 @@ var Serializer = class {
|
|
|
4738
5157
|
*
|
|
4739
5158
|
* @method formatRelationship
|
|
4740
5159
|
*
|
|
4741
|
-
* @param {
|
|
5160
|
+
* @param {Model} item - The Model instance to transform into the returned
|
|
4742
5161
|
* [JSON API](http://jsonapi.org) relationship object.
|
|
4743
5162
|
*
|
|
4744
|
-
* @param {
|
|
5163
|
+
* @param {String} domain - A string used to build links included in the
|
|
4745
5164
|
* returned [JSON API](http://jsonapi.org) relationship object.
|
|
4746
5165
|
*
|
|
4747
|
-
* @
|
|
4748
|
-
* the returned [JSON API](http://jsonapi.org) relationship object.
|
|
4749
|
-
*
|
|
4750
|
-
* @param {Array} options.include - An array of strings containing the
|
|
4751
|
-
* relationship keys that should be added to the top level included object of
|
|
4752
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
5166
|
+
* @return {Object} A [JSON API](http://jsonapi.org) relationship object.
|
|
4753
5167
|
*
|
|
4754
|
-
* @
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
5168
|
+
* @private
|
|
5169
|
+
*/
|
|
5170
|
+
formatRelationship(item, domain, namespace = this.namespace) {
|
|
5171
|
+
return this.formatLinkage(
|
|
5172
|
+
domain,
|
|
5173
|
+
item.resourceName,
|
|
5174
|
+
String(item.getPrimaryKey()),
|
|
5175
|
+
namespace
|
|
5176
|
+
);
|
|
5177
|
+
}
|
|
5178
|
+
/**
|
|
5179
|
+
* Build a [JSON API](http://jsonapi.org) relationship object from resource
|
|
5180
|
+
* linkage, in the same shape `formatRelationship()` produces from Model
|
|
5181
|
+
* instances: to-one relationships carry a `links` object, to-many ones only
|
|
5182
|
+
* `data`, and a missing to-one relationship is `{ data: null }`.
|
|
4760
5183
|
*
|
|
5184
|
+
* @method formatLinkage
|
|
4761
5185
|
* @private
|
|
4762
5186
|
*/
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
included
|
|
4768
|
-
}) {
|
|
4769
|
-
const { namespace } = this;
|
|
4770
|
-
const {
|
|
4771
|
-
resourceName: type,
|
|
4772
|
-
constructor: { serializer }
|
|
4773
|
-
} = item;
|
|
4774
|
-
const id = String(item.getPrimaryKey());
|
|
4775
|
-
let links;
|
|
4776
|
-
if (namespace) {
|
|
4777
|
-
links = {
|
|
4778
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4779
|
-
};
|
|
4780
|
-
} else {
|
|
4781
|
-
links = {
|
|
4782
|
-
self: `${domain}/${type}/${id}`
|
|
5187
|
+
formatLinkage(domain, type, linkage, namespace = this.namespace) {
|
|
5188
|
+
if (Array.isArray(linkage)) {
|
|
5189
|
+
return {
|
|
5190
|
+
data: type ? linkage.map((id) => ({ id, type })) : []
|
|
4783
5191
|
};
|
|
4784
5192
|
}
|
|
4785
|
-
if (
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
domain,
|
|
4790
|
-
include: [],
|
|
4791
|
-
included: [],
|
|
4792
|
-
formatRelationships: false
|
|
4793
|
-
})
|
|
4794
|
-
);
|
|
5193
|
+
if (linkage == null || !type) {
|
|
5194
|
+
return {
|
|
5195
|
+
data: null
|
|
5196
|
+
};
|
|
4795
5197
|
}
|
|
4796
5198
|
return {
|
|
4797
5199
|
data: {
|
|
4798
|
-
id,
|
|
5200
|
+
id: linkage,
|
|
4799
5201
|
type
|
|
4800
5202
|
},
|
|
4801
|
-
links
|
|
5203
|
+
links: {
|
|
5204
|
+
self: this.linkFor(domain, type, linkage, namespace)
|
|
5205
|
+
}
|
|
4802
5206
|
};
|
|
4803
5207
|
}
|
|
5208
|
+
/**
|
|
5209
|
+
* Add `records` (instances of `model`) to `included` as resource objects,
|
|
5210
|
+
* then recurse into the relationships named in `tree`. Each is serialized by
|
|
5211
|
+
* `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
|
|
5212
|
+
* `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
|
|
5213
|
+
* is one and `CommentsSerializer` otherwise. The relationships of every level
|
|
5214
|
+
* are batch-loaded with one query per relationship, not one per record.
|
|
5215
|
+
*
|
|
5216
|
+
* @method addIncluded
|
|
5217
|
+
* @private
|
|
5218
|
+
*/
|
|
5219
|
+
async addIncluded({
|
|
5220
|
+
model,
|
|
5221
|
+
records,
|
|
5222
|
+
tree,
|
|
5223
|
+
domain,
|
|
5224
|
+
included,
|
|
5225
|
+
namespace
|
|
5226
|
+
}) {
|
|
5227
|
+
const serializer = this.serializerFor(model, namespace);
|
|
5228
|
+
const unique = Array.from(
|
|
5229
|
+
new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
|
|
5230
|
+
);
|
|
5231
|
+
if (!unique.length) {
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
const names = [...serializer.hasOne, ...serializer.hasMany];
|
|
5235
|
+
const linkage = await loadLinkage(model, unique, names);
|
|
5236
|
+
for (const item of unique) {
|
|
5237
|
+
const id = String(item.getPrimaryKey());
|
|
5238
|
+
const key = resourceKey({ id, type: item.resourceName });
|
|
5239
|
+
if (!included.has(key)) {
|
|
5240
|
+
included.set(
|
|
5241
|
+
key,
|
|
5242
|
+
await serializer.formatOne({
|
|
5243
|
+
item,
|
|
5244
|
+
domain,
|
|
5245
|
+
linkage: linkage.get(id),
|
|
5246
|
+
namespace
|
|
5247
|
+
})
|
|
5248
|
+
);
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
for (const [name, children] of tree) {
|
|
5252
|
+
const opts = model.relationshipFor(name);
|
|
5253
|
+
if (!opts || !names.includes(name)) {
|
|
5254
|
+
continue;
|
|
5255
|
+
}
|
|
5256
|
+
const { model: next } = opts;
|
|
5257
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5258
|
+
linkage.forEach(({ [name]: value }) => {
|
|
5259
|
+
(Array.isArray(value) ? value : [value]).forEach((relatedId) => {
|
|
5260
|
+
if (relatedId != null) {
|
|
5261
|
+
ids.add(relatedId);
|
|
5262
|
+
}
|
|
5263
|
+
});
|
|
5264
|
+
});
|
|
5265
|
+
if (ids.size) {
|
|
5266
|
+
const attributes = this.serializerFor(
|
|
5267
|
+
next,
|
|
5268
|
+
namespace
|
|
5269
|
+
).attributes.filter((attr) => next.attributeNames.includes(attr));
|
|
5270
|
+
const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
|
|
5271
|
+
await this.addIncluded({
|
|
5272
|
+
domain,
|
|
5273
|
+
included,
|
|
5274
|
+
namespace,
|
|
5275
|
+
model: next,
|
|
5276
|
+
records: nextRecords,
|
|
5277
|
+
tree: children
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Resolve the Serializer for `model` in `namespace` (this Serializer's by
|
|
5284
|
+
* default), the way a namespaced Controller resolves its own:
|
|
5285
|
+
* `admin/comments` if it exists, otherwise the closest ancestor namespace's,
|
|
5286
|
+
* down to the root `comments` Serializer. Falls back to `model.serializer`
|
|
5287
|
+
* when this Serializer was not created by an application (e.g. in
|
|
5288
|
+
* isolation).
|
|
5289
|
+
*
|
|
5290
|
+
* Pass the request's namespace when there is one: a Serializer's own
|
|
5291
|
+
* namespace is the root one whenever it is a namespaced Controller's
|
|
5292
|
+
* fallback.
|
|
5293
|
+
*
|
|
5294
|
+
* @method serializerFor
|
|
5295
|
+
* @private
|
|
5296
|
+
*/
|
|
5297
|
+
serializerFor(model, namespace = this.namespace) {
|
|
5298
|
+
const { serializers } = this;
|
|
5299
|
+
if (serializers) {
|
|
5300
|
+
const key = import_path3.posix.join(namespace || ".", model.resourceName);
|
|
5301
|
+
const serializer = serializers.get(key) || closestAncestor(serializers, key);
|
|
5302
|
+
if (serializer) {
|
|
5303
|
+
return serializer;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
return model.serializer;
|
|
5307
|
+
}
|
|
5308
|
+
/**
|
|
5309
|
+
* @private
|
|
5310
|
+
*/
|
|
5311
|
+
linkFor(domain, type, id, namespace = this.namespace) {
|
|
5312
|
+
if (namespace) {
|
|
5313
|
+
return `${domain}/${namespace}/${type}/${id}`;
|
|
5314
|
+
}
|
|
5315
|
+
return `${domain}/${type}/${id}`;
|
|
5316
|
+
}
|
|
4804
5317
|
};
|
|
5318
|
+
function resourceKey({ id, type }) {
|
|
5319
|
+
return `${type}:${id}`;
|
|
5320
|
+
}
|
|
4805
5321
|
var serializer_default = Serializer;
|
|
4806
5322
|
|
|
4807
5323
|
// src/packages/config/index.ts
|
|
@@ -5000,7 +5516,10 @@ function resource(action) {
|
|
|
5000
5516
|
data,
|
|
5001
5517
|
links,
|
|
5002
5518
|
domain,
|
|
5003
|
-
include
|
|
5519
|
+
include,
|
|
5520
|
+
// The request's namespace, not the serializer's: a namespaced
|
|
5521
|
+
// controller without its own serializer is given the root one.
|
|
5522
|
+
namespace
|
|
5004
5523
|
});
|
|
5005
5524
|
}
|
|
5006
5525
|
return data;
|
|
@@ -5081,6 +5600,7 @@ var ParameterTypeError = class extends TypeError {
|
|
|
5081
5600
|
Expected type '${type || "undefined"}' for parameter '${path}' but got
|
|
5082
5601
|
'${actual}'.
|
|
5083
5602
|
`);
|
|
5603
|
+
this.source = sourceFor(path);
|
|
5084
5604
|
}
|
|
5085
5605
|
};
|
|
5086
5606
|
var parameter_type_error_default = createServerError(ParameterTypeError, 400);
|
|
@@ -5093,6 +5613,7 @@ var ParameterValueError = class extends TypeError {
|
|
|
5093
5613
|
[${param.size ? Array.from(param.values()).join(", ") : ""}] but got
|
|
5094
5614
|
${String(actual)}.
|
|
5095
5615
|
`);
|
|
5616
|
+
this.source = sourceFor(param.path);
|
|
5096
5617
|
}
|
|
5097
5618
|
};
|
|
5098
5619
|
var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
@@ -5101,6 +5622,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
|
5101
5622
|
var InvalidParameterError = class extends TypeError {
|
|
5102
5623
|
constructor(path) {
|
|
5103
5624
|
super(`'${path}' is not a valid parameter for this resource.`);
|
|
5625
|
+
this.source = sourceFor(path);
|
|
5104
5626
|
}
|
|
5105
5627
|
};
|
|
5106
5628
|
var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
|
|
@@ -5116,6 +5638,7 @@ var ResourceMismatchError = class extends TypeError {
|
|
|
5116
5638
|
Expected '${String(expected)}' for parameter '${path}' but got
|
|
5117
5639
|
${String(normalized)}.
|
|
5118
5640
|
`);
|
|
5641
|
+
this.source = sourceFor(path);
|
|
5119
5642
|
}
|
|
5120
5643
|
};
|
|
5121
5644
|
var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
|
|
@@ -5124,14 +5647,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
|
|
|
5124
5647
|
var ParameterRequiredError = class extends TypeError {
|
|
5125
5648
|
constructor(path) {
|
|
5126
5649
|
super(`Missing required parameter '${path}'.`);
|
|
5650
|
+
this.source = sourceFor(path);
|
|
5127
5651
|
}
|
|
5128
5652
|
};
|
|
5129
5653
|
var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
|
|
5130
5654
|
|
|
5655
|
+
// src/packages/router/route/params/errors/client-generated-id-error.ts
|
|
5656
|
+
var ClientGeneratedIdError = class extends TypeError {
|
|
5657
|
+
constructor() {
|
|
5658
|
+
super("Client-generated IDs are not supported for this resource.");
|
|
5659
|
+
this.source = sourceFor("data.id");
|
|
5660
|
+
}
|
|
5661
|
+
};
|
|
5662
|
+
var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
|
|
5663
|
+
|
|
5664
|
+
// src/packages/router/route/params/errors/forbidden-parameter-error.ts
|
|
5665
|
+
var ForbiddenParameterError = class extends TypeError {
|
|
5666
|
+
constructor(path) {
|
|
5667
|
+
super(`Setting '${path}' is not supported for this resource.`);
|
|
5668
|
+
this.source = sourceFor(path);
|
|
5669
|
+
}
|
|
5670
|
+
};
|
|
5671
|
+
var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
|
|
5672
|
+
|
|
5131
5673
|
// src/packages/router/route/params/errors/parameter-not-nullable-error.ts
|
|
5132
5674
|
var ParameterNotNullableError = class extends TypeError {
|
|
5133
5675
|
constructor({ path }) {
|
|
5134
5676
|
super(`Parameter '${path}' is not nullable.`);
|
|
5677
|
+
this.source = sourceFor(path);
|
|
5135
5678
|
}
|
|
5136
5679
|
};
|
|
5137
5680
|
var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
|
|
@@ -5260,13 +5803,14 @@ var Parameter = class extends set_default {
|
|
|
5260
5803
|
path,
|
|
5261
5804
|
type,
|
|
5262
5805
|
required: Boolean(required),
|
|
5263
|
-
sanitize: Boolean(sanitize)
|
|
5806
|
+
sanitize: Boolean(sanitize),
|
|
5807
|
+
restricted: values !== void 0
|
|
5264
5808
|
});
|
|
5265
5809
|
this.freeze();
|
|
5266
5810
|
}
|
|
5267
5811
|
validate(value) {
|
|
5268
5812
|
validateType2(this, value);
|
|
5269
|
-
if (this.
|
|
5813
|
+
if (this.restricted) {
|
|
5270
5814
|
return validateValue(this, value);
|
|
5271
5815
|
}
|
|
5272
5816
|
return value;
|
|
@@ -5285,6 +5829,17 @@ function getURLParams(dynamicSegments) {
|
|
|
5285
5829
|
]);
|
|
5286
5830
|
}
|
|
5287
5831
|
|
|
5832
|
+
// src/packages/router/route/params/parameter/forbidden-parameter.ts
|
|
5833
|
+
var ForbiddenParameter = class extends parameter_default {
|
|
5834
|
+
constructor(path) {
|
|
5835
|
+
super({ path });
|
|
5836
|
+
}
|
|
5837
|
+
validate() {
|
|
5838
|
+
throw new forbidden_parameter_error_default(this.path);
|
|
5839
|
+
}
|
|
5840
|
+
};
|
|
5841
|
+
var forbidden_parameter_default = ForbiddenParameter;
|
|
5842
|
+
|
|
5288
5843
|
// src/packages/router/route/params/utils/get-data-params.ts
|
|
5289
5844
|
function getIDParam({ model }) {
|
|
5290
5845
|
const primaryKeyColumn = model.columnFor(model.primaryKey);
|
|
@@ -5337,16 +5892,48 @@ function getRelationshipsParam({
|
|
|
5337
5892
|
model,
|
|
5338
5893
|
params
|
|
5339
5894
|
}) {
|
|
5895
|
+
const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
|
|
5896
|
+
key,
|
|
5897
|
+
new forbidden_parameter_default(`data.relationships.${key}`)
|
|
5898
|
+
]);
|
|
5340
5899
|
return [
|
|
5341
5900
|
"relationships",
|
|
5342
5901
|
new parameter_group_default(
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5902
|
+
[
|
|
5903
|
+
...params.reduce((group, param) => {
|
|
5904
|
+
const path = `data.relationships.${param}`;
|
|
5905
|
+
const opts = model.relationshipFor(param);
|
|
5906
|
+
if (!opts) {
|
|
5907
|
+
return group;
|
|
5908
|
+
}
|
|
5909
|
+
if (opts.type === "hasMany") {
|
|
5910
|
+
return [
|
|
5911
|
+
...group,
|
|
5912
|
+
[
|
|
5913
|
+
param,
|
|
5914
|
+
new parameter_group_default(
|
|
5915
|
+
[
|
|
5916
|
+
[
|
|
5917
|
+
"data",
|
|
5918
|
+
new parameter_default({
|
|
5919
|
+
type: "array",
|
|
5920
|
+
path: `${path}.data`,
|
|
5921
|
+
required: true
|
|
5922
|
+
})
|
|
5923
|
+
]
|
|
5924
|
+
],
|
|
5925
|
+
{
|
|
5926
|
+
path
|
|
5927
|
+
}
|
|
5928
|
+
)
|
|
5929
|
+
]
|
|
5930
|
+
];
|
|
5931
|
+
}
|
|
5932
|
+
const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
|
|
5933
|
+
let primaryKeyType = "number";
|
|
5934
|
+
if (primaryKeyColumn) {
|
|
5935
|
+
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5936
|
+
}
|
|
5350
5937
|
return [
|
|
5351
5938
|
...group,
|
|
5352
5939
|
[
|
|
@@ -5355,11 +5942,32 @@ function getRelationshipsParam({
|
|
|
5355
5942
|
[
|
|
5356
5943
|
[
|
|
5357
5944
|
"data",
|
|
5358
|
-
new
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5945
|
+
new parameter_group_default(
|
|
5946
|
+
[
|
|
5947
|
+
[
|
|
5948
|
+
"id",
|
|
5949
|
+
new parameter_default({
|
|
5950
|
+
type: primaryKeyType,
|
|
5951
|
+
path: `${path}.data.id`,
|
|
5952
|
+
required: true
|
|
5953
|
+
})
|
|
5954
|
+
],
|
|
5955
|
+
[
|
|
5956
|
+
"type",
|
|
5957
|
+
new parameter_default({
|
|
5958
|
+
type: "string",
|
|
5959
|
+
path: `${path}.data.type`,
|
|
5960
|
+
values: [opts.model.resourceName],
|
|
5961
|
+
required: true
|
|
5962
|
+
})
|
|
5963
|
+
]
|
|
5964
|
+
],
|
|
5965
|
+
{
|
|
5966
|
+
type: "array",
|
|
5967
|
+
path: `${path}.data`,
|
|
5968
|
+
required: true
|
|
5969
|
+
}
|
|
5970
|
+
)
|
|
5363
5971
|
]
|
|
5364
5972
|
],
|
|
5365
5973
|
{
|
|
@@ -5368,55 +5976,9 @@ function getRelationshipsParam({
|
|
|
5368
5976
|
)
|
|
5369
5977
|
]
|
|
5370
5978
|
];
|
|
5371
|
-
}
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
if (primaryKeyColumn) {
|
|
5375
|
-
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5376
|
-
}
|
|
5377
|
-
return [
|
|
5378
|
-
...group,
|
|
5379
|
-
[
|
|
5380
|
-
param,
|
|
5381
|
-
new parameter_group_default(
|
|
5382
|
-
[
|
|
5383
|
-
[
|
|
5384
|
-
"data",
|
|
5385
|
-
new parameter_group_default(
|
|
5386
|
-
[
|
|
5387
|
-
[
|
|
5388
|
-
"id",
|
|
5389
|
-
new parameter_default({
|
|
5390
|
-
type: primaryKeyType,
|
|
5391
|
-
path: `${path}.data.id`,
|
|
5392
|
-
required: true
|
|
5393
|
-
})
|
|
5394
|
-
],
|
|
5395
|
-
[
|
|
5396
|
-
"type",
|
|
5397
|
-
new parameter_default({
|
|
5398
|
-
type: "string",
|
|
5399
|
-
path: `${path}.data.type`,
|
|
5400
|
-
values: [opts.model.resourceName],
|
|
5401
|
-
required: true
|
|
5402
|
-
})
|
|
5403
|
-
]
|
|
5404
|
-
],
|
|
5405
|
-
{
|
|
5406
|
-
type: "array",
|
|
5407
|
-
path: `${path}.data`,
|
|
5408
|
-
required: true
|
|
5409
|
-
}
|
|
5410
|
-
)
|
|
5411
|
-
]
|
|
5412
|
-
],
|
|
5413
|
-
{
|
|
5414
|
-
path
|
|
5415
|
-
}
|
|
5416
|
-
)
|
|
5417
|
-
]
|
|
5418
|
-
];
|
|
5419
|
-
}, []),
|
|
5979
|
+
}, []),
|
|
5980
|
+
...forbidden
|
|
5981
|
+
],
|
|
5420
5982
|
{
|
|
5421
5983
|
path: "data.relationships"
|
|
5422
5984
|
}
|
|
@@ -5524,10 +6086,9 @@ function getFilterParam({ filter }) {
|
|
|
5524
6086
|
)
|
|
5525
6087
|
];
|
|
5526
6088
|
}
|
|
5527
|
-
function getFieldsParam({
|
|
5528
|
-
model,
|
|
5529
|
-
|
|
5530
|
-
}) {
|
|
6089
|
+
function getFieldsParam(controller) {
|
|
6090
|
+
const { model, serializer } = controller;
|
|
6091
|
+
const { hasOne, hasMany, attributes } = serializer;
|
|
5531
6092
|
const relationships = [...hasOne, ...hasMany];
|
|
5532
6093
|
return [
|
|
5533
6094
|
"fields",
|
|
@@ -5556,7 +6117,7 @@ function getFieldsParam({
|
|
|
5556
6117
|
sanitize: true,
|
|
5557
6118
|
values: [
|
|
5558
6119
|
opts.model.primaryKey,
|
|
5559
|
-
...opts.model.
|
|
6120
|
+
...controller.serializerFor(opts.model).attributes
|
|
5560
6121
|
]
|
|
5561
6122
|
})
|
|
5562
6123
|
]
|
|
@@ -5574,16 +6135,30 @@ function getFieldsParam({
|
|
|
5574
6135
|
)
|
|
5575
6136
|
];
|
|
5576
6137
|
}
|
|
5577
|
-
function getIncludeParam({
|
|
5578
|
-
|
|
5579
|
-
}
|
|
6138
|
+
function getIncludeParam(controller) {
|
|
6139
|
+
const { model, maxIncludeDepth, serializer } = controller;
|
|
6140
|
+
const { hasOne, hasMany } = serializer;
|
|
5580
6141
|
const relationships = [...hasOne, ...hasMany];
|
|
5581
6142
|
return [
|
|
5582
6143
|
"include",
|
|
5583
6144
|
new parameter_default({
|
|
5584
6145
|
path: "include",
|
|
5585
6146
|
type: "array",
|
|
5586
|
-
|
|
6147
|
+
// Every top level name stays allowed (as before), plus the nested paths
|
|
6148
|
+
// (`comments.user`) reachable through each related serializer in the
|
|
6149
|
+
// controller's namespace (at every level, even below a root fallback
|
|
6150
|
+
// serializer), down to the controller's `maxIncludeDepth`.
|
|
6151
|
+
values: Array.from(
|
|
6152
|
+
/* @__PURE__ */ new Set([
|
|
6153
|
+
...relationships,
|
|
6154
|
+
...enumerateIncludePaths(
|
|
6155
|
+
model,
|
|
6156
|
+
relationships,
|
|
6157
|
+
maxIncludeDepth,
|
|
6158
|
+
(related) => controller.serializerFor(related)
|
|
6159
|
+
)
|
|
6160
|
+
])
|
|
6161
|
+
)
|
|
5587
6162
|
})
|
|
5588
6163
|
];
|
|
5589
6164
|
}
|
|
@@ -5621,6 +6196,15 @@ function getCollectionQueryParams(controller) {
|
|
|
5621
6196
|
return getCustomParams(controller);
|
|
5622
6197
|
}
|
|
5623
6198
|
|
|
6199
|
+
// src/packages/router/route/params/utils/validate-client-id.ts
|
|
6200
|
+
function validateClientId(params) {
|
|
6201
|
+
const { data } = params;
|
|
6202
|
+
if (data && typeof data === "object" && Reflect.has(data, "id")) {
|
|
6203
|
+
throw new client_generated_id_error_default();
|
|
6204
|
+
}
|
|
6205
|
+
return true;
|
|
6206
|
+
}
|
|
6207
|
+
|
|
5624
6208
|
// src/packages/router/route/params/utils/validate-resource-id.ts
|
|
5625
6209
|
function validateResourceId({
|
|
5626
6210
|
params: {
|
|
@@ -5786,6 +6370,9 @@ var Route = class extends set_default {
|
|
|
5786
6370
|
...req.params,
|
|
5787
6371
|
...this.parseParams(req.url.params)
|
|
5788
6372
|
};
|
|
6373
|
+
if (this.action === "create" && req.method === "POST") {
|
|
6374
|
+
validateClientId(params);
|
|
6375
|
+
}
|
|
5789
6376
|
if (req.method !== "OPTIONS") {
|
|
5790
6377
|
params = this.params.validate(params);
|
|
5791
6378
|
}
|
|
@@ -6098,10 +6685,10 @@ var Router = class extends map_default {
|
|
|
6098
6685
|
var router_default = Router;
|
|
6099
6686
|
|
|
6100
6687
|
// src/packages/loader/utils/bundle-for.ts
|
|
6101
|
-
var
|
|
6688
|
+
var import_path4 = require("path");
|
|
6102
6689
|
|
|
6103
6690
|
// src/packages/loader/utils/format-key.ts
|
|
6104
|
-
var
|
|
6691
|
+
var import_inflection15 = require("inflection");
|
|
6105
6692
|
var NAMESPACE_DELIMITER = /\$-/g;
|
|
6106
6693
|
function formatKey(key, formatter) {
|
|
6107
6694
|
return chain(key).pipe((str) => {
|
|
@@ -6109,7 +6696,7 @@ function formatKey(key, formatter) {
|
|
|
6109
6696
|
return formatter(str);
|
|
6110
6697
|
}
|
|
6111
6698
|
return str;
|
|
6112
|
-
}).pipe(underscore).pipe(
|
|
6699
|
+
}).pipe(underscore).pipe(import_inflection15.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
|
|
6113
6700
|
}
|
|
6114
6701
|
|
|
6115
6702
|
// src/packages/loader/utils/bundle-for.ts
|
|
@@ -6176,31 +6763,15 @@ function normalize2(manifest) {
|
|
|
6176
6763
|
}
|
|
6177
6764
|
function bundleFor(path) {
|
|
6178
6765
|
const manifest = Reflect.apply(require, null, [
|
|
6179
|
-
(0,
|
|
6766
|
+
(0, import_path4.join)(path, "dist", "bundle")
|
|
6180
6767
|
]);
|
|
6181
6768
|
return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
|
|
6182
6769
|
}
|
|
6183
6770
|
|
|
6184
|
-
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
6185
|
-
var import_path3 = require("path");
|
|
6186
|
-
function closestAncestor(source, key) {
|
|
6187
|
-
const name = import_path3.posix.basename(key);
|
|
6188
|
-
let namespace = import_path3.posix.dirname(key);
|
|
6189
|
-
if (namespace === ".") {
|
|
6190
|
-
return source.get(name);
|
|
6191
|
-
}
|
|
6192
|
-
namespace = import_path3.posix.dirname(namespace);
|
|
6193
|
-
const ancestor = source.get(import_path3.posix.join(namespace, name));
|
|
6194
|
-
if (ancestor) {
|
|
6195
|
-
return ancestor;
|
|
6196
|
-
}
|
|
6197
|
-
return closestAncestor(source, import_path3.posix.join(import_path3.posix.dirname(namespace), name));
|
|
6198
|
-
}
|
|
6199
|
-
|
|
6200
6771
|
// src/packages/loader/resolver/utils/closest-child.ts
|
|
6201
|
-
var
|
|
6772
|
+
var import_path5 = require("path");
|
|
6202
6773
|
function closestChild(source, key) {
|
|
6203
|
-
const [[, result] = []] = Array.from(source).map(([path, value]) => [
|
|
6774
|
+
const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path5.posix.basename(path), value]).filter(([resource2]) => key === resource2);
|
|
6204
6775
|
return result;
|
|
6205
6776
|
}
|
|
6206
6777
|
|
|
@@ -6223,7 +6794,7 @@ function resolve(group) {
|
|
|
6223
6794
|
}
|
|
6224
6795
|
|
|
6225
6796
|
// src/packages/loader/builder/utils/create-parent-builder.ts
|
|
6226
|
-
var
|
|
6797
|
+
var import_path6 = require("path");
|
|
6227
6798
|
|
|
6228
6799
|
// src/packages/loader/builder/utils/sort-by-namespace.ts
|
|
6229
6800
|
function sortByNamespace([a], [b]) {
|
|
@@ -6244,7 +6815,7 @@ function createParentBuilder(construct) {
|
|
|
6244
6815
|
let grandparent = null;
|
|
6245
6816
|
if (key !== "root") {
|
|
6246
6817
|
const found = result.find((namespace) => {
|
|
6247
|
-
const dirname =
|
|
6818
|
+
const dirname = import_path6.posix.dirname(key);
|
|
6248
6819
|
if (namespace.key === "root") {
|
|
6249
6820
|
return dirname === ".";
|
|
6250
6821
|
}
|
|
@@ -6314,13 +6885,13 @@ function normalizePort(port) {
|
|
|
6314
6885
|
}
|
|
6315
6886
|
|
|
6316
6887
|
// src/packages/application/utils/create-controller.ts
|
|
6317
|
-
var
|
|
6888
|
+
var import_path7 = require("path");
|
|
6318
6889
|
function createController(constructor, opts) {
|
|
6319
6890
|
const { key, store, serializers } = opts;
|
|
6320
|
-
const namespace =
|
|
6891
|
+
const namespace = import_path7.posix.dirname(key).replace(".", "");
|
|
6321
6892
|
let { parent } = opts;
|
|
6322
6893
|
let model = tryCatchSync(
|
|
6323
|
-
() => store.modelFor(
|
|
6894
|
+
() => store.modelFor(import_path7.posix.basename(key))
|
|
6324
6895
|
);
|
|
6325
6896
|
let serializer = serializers.get(key);
|
|
6326
6897
|
if (!model) {
|
|
@@ -6376,13 +6947,13 @@ function createController(constructor, opts) {
|
|
|
6376
6947
|
}
|
|
6377
6948
|
|
|
6378
6949
|
// src/packages/application/utils/create-serializer.ts
|
|
6379
|
-
var
|
|
6950
|
+
var import_path8 = require("path");
|
|
6380
6951
|
function createSerializer(constructor, opts) {
|
|
6381
6952
|
const { key, store } = opts;
|
|
6382
|
-
const namespace =
|
|
6953
|
+
const namespace = import_path8.posix.dirname(key).replace(".", "");
|
|
6383
6954
|
let { parent } = opts;
|
|
6384
6955
|
let model = tryCatchSync(
|
|
6385
|
-
() => store.modelFor(
|
|
6956
|
+
() => store.modelFor(import_path8.posix.basename(key))
|
|
6386
6957
|
);
|
|
6387
6958
|
if (!model) {
|
|
6388
6959
|
model = null;
|
|
@@ -6406,6 +6977,85 @@ function createSerializer(constructor, opts) {
|
|
|
6406
6977
|
return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
|
|
6407
6978
|
}
|
|
6408
6979
|
|
|
6980
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
6981
|
+
var import_path9 = require("path");
|
|
6982
|
+
|
|
6983
|
+
// src/errors/namespaced-serializer-missing-error.ts
|
|
6984
|
+
var NamespacedSerializerMissingError = class extends ReferenceError {
|
|
6985
|
+
/**
|
|
6986
|
+
* @param missing - Each missing serializer key (`admin/comments`) with an
|
|
6987
|
+
* example of how it is reached (`admin/posts?include=comments`).
|
|
6988
|
+
*/
|
|
6989
|
+
constructor(missing) {
|
|
6990
|
+
const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
|
|
6991
|
+
super(
|
|
6992
|
+
[
|
|
6993
|
+
"Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
|
|
6994
|
+
...lines
|
|
6995
|
+
].join("\n")
|
|
6996
|
+
);
|
|
6997
|
+
}
|
|
6998
|
+
};
|
|
6999
|
+
var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
|
|
7000
|
+
|
|
7001
|
+
// src/packages/application/utils/validate-namespaced-serializers.ts
|
|
7002
|
+
var distance = (via) => {
|
|
7003
|
+
const [, include] = via.split("?include=");
|
|
7004
|
+
return include ? include.split(".").length : 0;
|
|
7005
|
+
};
|
|
7006
|
+
function validateNamespacedSerializers(controllers, serializers) {
|
|
7007
|
+
const missing = /* @__PURE__ */ new Map();
|
|
7008
|
+
controllers.forEach((controller, key) => {
|
|
7009
|
+
const { namespace, model } = controller;
|
|
7010
|
+
if (!namespace || !model) {
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
const application = controllers.get(import_path9.posix.join(namespace, "application"));
|
|
7014
|
+
if (!application || application.serializerFallback !== false) {
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
const require2 = (type, via) => {
|
|
7018
|
+
const serializerKey = import_path9.posix.join(namespace, type);
|
|
7019
|
+
const serializer = serializers.get(serializerKey);
|
|
7020
|
+
if (!serializer) {
|
|
7021
|
+
const known = missing.get(serializerKey);
|
|
7022
|
+
if (known === void 0 || distance(via) < distance(known)) {
|
|
7023
|
+
missing.set(serializerKey, via);
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
return serializer;
|
|
7027
|
+
};
|
|
7028
|
+
const own = require2(model.resourceName, key);
|
|
7029
|
+
if (!own) {
|
|
7030
|
+
return;
|
|
7031
|
+
}
|
|
7032
|
+
const depth = Math.max(controller.maxIncludeDepth, 1);
|
|
7033
|
+
let level = [
|
|
7034
|
+
[model, own, []]
|
|
7035
|
+
];
|
|
7036
|
+
for (let current = 1; current <= depth && level.length; current += 1) {
|
|
7037
|
+
const next = [];
|
|
7038
|
+
level.forEach(([parent, serializer, path]) => {
|
|
7039
|
+
[...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
|
|
7040
|
+
const opts = parent.relationshipFor(name);
|
|
7041
|
+
if (!opts) {
|
|
7042
|
+
return;
|
|
7043
|
+
}
|
|
7044
|
+
const nextPath = [...path, name];
|
|
7045
|
+
const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
|
|
7046
|
+
if (related) {
|
|
7047
|
+
next.push([opts.model, related, nextPath]);
|
|
7048
|
+
}
|
|
7049
|
+
});
|
|
7050
|
+
});
|
|
7051
|
+
level = next;
|
|
7052
|
+
}
|
|
7053
|
+
});
|
|
7054
|
+
if (missing.size) {
|
|
7055
|
+
throw new namespaced_serializer_missing_error_default(missing);
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
|
|
6409
7059
|
// src/packages/application/initialize.ts
|
|
6410
7060
|
async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
|
|
6411
7061
|
const load = createLoader(path);
|
|
@@ -6428,6 +7078,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6428
7078
|
parent
|
|
6429
7079
|
})
|
|
6430
7080
|
);
|
|
7081
|
+
serializers.forEach((serializer) => {
|
|
7082
|
+
Reflect.defineProperty(serializer, "serializers", {
|
|
7083
|
+
value: serializers,
|
|
7084
|
+
writable: false,
|
|
7085
|
+
enumerable: false,
|
|
7086
|
+
configurable: false
|
|
7087
|
+
});
|
|
7088
|
+
});
|
|
6431
7089
|
models.forEach((model) => {
|
|
6432
7090
|
Reflect.defineProperty(model, "serializer", {
|
|
6433
7091
|
value: closestChild(serializers, model.resourceName),
|
|
@@ -6453,6 +7111,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6453
7111
|
configurable: false
|
|
6454
7112
|
});
|
|
6455
7113
|
});
|
|
7114
|
+
validateNamespacedSerializers(controllers, serializers);
|
|
6456
7115
|
const ApplicationController = controllers.get("application");
|
|
6457
7116
|
if (!ApplicationController) {
|
|
6458
7117
|
throw new controller_missing_error_default("application");
|