lumen-framework 3.0.2 → 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 +71 -38
- package/dist/cli.cjs.map +4 -4
- package/dist/index.js +978 -272
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +969 -263
- 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/errors/not-acceptable-error.d.ts +1 -1
- package/dist/types/packages/jsonapi/index.d.ts +3 -1
- package/dist/types/packages/jsonapi/interfaces.d.ts +1 -1
- package/dist/types/packages/jsonapi/utils/has-media-type-params.d.ts +6 -0
- package/dist/types/packages/jsonapi/utils/is-jsonapi.d.ts +2 -0
- package/dist/types/packages/jsonapi/utils/media-type.d.ts +19 -0
- 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/dist/types/packages/server/utils/validate-accept.d.ts +5 -1
- package/dist/types/packages/server/utils/validate-content-type.d.ts +4 -0
- package/package.json +1 -1
- package/dist/types/packages/jsonapi/utils/has-media-type.d.ts +0 -4
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
|
}
|
|
@@ -3694,10 +3762,11 @@ var MIME_TYPE = "application/vnd.api+json";
|
|
|
3694
3762
|
|
|
3695
3763
|
// src/packages/jsonapi/errors/not-acceptable-error.ts
|
|
3696
3764
|
var NotAcceptableError = class extends TypeError {
|
|
3697
|
-
constructor(
|
|
3765
|
+
constructor(accept) {
|
|
3698
3766
|
super(line`
|
|
3699
|
-
|
|
3700
|
-
|
|
3767
|
+
Every '${MIME_TYPE}' in Accept: '${accept}' has media type parameters,
|
|
3768
|
+
which are not supported. Try your request again with '${MIME_TYPE}'
|
|
3769
|
+
without parameters.
|
|
3701
3770
|
`);
|
|
3702
3771
|
}
|
|
3703
3772
|
};
|
|
@@ -3707,8 +3776,9 @@ var not_acceptable_error_default = createServerError(NotAcceptableError, 406);
|
|
|
3707
3776
|
var UnsupportedMediaTypeError = class extends TypeError {
|
|
3708
3777
|
constructor(contentType) {
|
|
3709
3778
|
super(line`
|
|
3710
|
-
Media type parameters
|
|
3711
|
-
|
|
3779
|
+
Media type parameters are not supported in Content-Type:
|
|
3780
|
+
'${contentType}'. Try your request again with Content-Type:
|
|
3781
|
+
'${MIME_TYPE}' without parameters.
|
|
3712
3782
|
`);
|
|
3713
3783
|
}
|
|
3714
3784
|
};
|
|
@@ -3723,17 +3793,59 @@ var InvalidContentTypeError = class extends TypeError {
|
|
|
3723
3793
|
`);
|
|
3724
3794
|
}
|
|
3725
3795
|
};
|
|
3726
|
-
var invalid_content_type_error_default = createServerError(InvalidContentTypeError,
|
|
3796
|
+
var invalid_content_type_error_default = createServerError(InvalidContentTypeError, 415);
|
|
3797
|
+
|
|
3798
|
+
// src/packages/jsonapi/utils/media-type.ts
|
|
3799
|
+
var QUALITY = /^q\s*=/i;
|
|
3800
|
+
function splitUnquoted(value, delimiter) {
|
|
3801
|
+
const parts = [];
|
|
3802
|
+
let current = "";
|
|
3803
|
+
let quoted = false;
|
|
3804
|
+
let escaped = false;
|
|
3805
|
+
for (const char of value) {
|
|
3806
|
+
if (escaped) {
|
|
3807
|
+
escaped = false;
|
|
3808
|
+
} else if (quoted && char === "\\") {
|
|
3809
|
+
escaped = true;
|
|
3810
|
+
} else if (char === '"') {
|
|
3811
|
+
quoted = !quoted;
|
|
3812
|
+
} else if (!quoted && char === delimiter) {
|
|
3813
|
+
parts.push(current);
|
|
3814
|
+
current = "";
|
|
3815
|
+
continue;
|
|
3816
|
+
}
|
|
3817
|
+
current += char;
|
|
3818
|
+
}
|
|
3819
|
+
return [...parts, current];
|
|
3820
|
+
}
|
|
3821
|
+
function parseMediaType(value) {
|
|
3822
|
+
const [type = "", ...params] = splitUnquoted(value, ";").map(
|
|
3823
|
+
(part) => part.trim()
|
|
3824
|
+
);
|
|
3825
|
+
return {
|
|
3826
|
+
type: type.toLowerCase(),
|
|
3827
|
+
params: params.filter(Boolean)
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
function parseAccept(value) {
|
|
3831
|
+
return splitUnquoted(value, ",").map((range) => {
|
|
3832
|
+
const { type, params } = parseMediaType(range);
|
|
3833
|
+
const quality = params.findIndex((param) => QUALITY.test(param));
|
|
3834
|
+
return {
|
|
3835
|
+
type,
|
|
3836
|
+
params: quality === -1 ? params : params.slice(0, quality)
|
|
3837
|
+
};
|
|
3838
|
+
}).filter(({ type }) => type.length > 0);
|
|
3839
|
+
}
|
|
3727
3840
|
|
|
3728
3841
|
// src/packages/jsonapi/utils/is-jsonapi.ts
|
|
3729
3842
|
function isJSONAPI(value) {
|
|
3730
|
-
return value.
|
|
3843
|
+
return parseMediaType(value).type === MIME_TYPE;
|
|
3731
3844
|
}
|
|
3732
3845
|
|
|
3733
|
-
// src/packages/jsonapi/utils/has-media-type.ts
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
return PATTERN2.test(value);
|
|
3846
|
+
// src/packages/jsonapi/utils/has-media-type-params.ts
|
|
3847
|
+
function hasMediaTypeParams(value) {
|
|
3848
|
+
return parseMediaType(value).params.length > 0;
|
|
3737
3849
|
}
|
|
3738
3850
|
|
|
3739
3851
|
// src/utils/env.ts
|
|
@@ -3754,6 +3866,10 @@ function dataFor(status, err) {
|
|
|
3754
3866
|
if (title) {
|
|
3755
3867
|
errData.title = title;
|
|
3756
3868
|
}
|
|
3869
|
+
const source = err?.source;
|
|
3870
|
+
if (source && (source.pointer || source.parameter)) {
|
|
3871
|
+
errData.source = source;
|
|
3872
|
+
}
|
|
3757
3873
|
if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
|
|
3758
3874
|
errData.detail = err.message.replace(/^\[public\]/gi, "");
|
|
3759
3875
|
}
|
|
@@ -3834,9 +3950,12 @@ function createResponder(req, res) {
|
|
|
3834
3950
|
}
|
|
3835
3951
|
|
|
3836
3952
|
// src/packages/server/utils/validate-accept.ts
|
|
3837
|
-
function validateAccept(
|
|
3838
|
-
if (
|
|
3839
|
-
|
|
3953
|
+
function validateAccept(accept) {
|
|
3954
|
+
if (accept) {
|
|
3955
|
+
const ranges = parseAccept(accept).filter(({ type }) => type === MIME_TYPE);
|
|
3956
|
+
if (ranges.length && ranges.every(({ params }) => params.length)) {
|
|
3957
|
+
throw new not_acceptable_error_default(accept);
|
|
3958
|
+
}
|
|
3840
3959
|
}
|
|
3841
3960
|
return true;
|
|
3842
3961
|
}
|
|
@@ -3845,7 +3964,7 @@ function validateAccept(contentType) {
|
|
|
3845
3964
|
function validateContentType(contentType) {
|
|
3846
3965
|
if (!contentType || !isJSONAPI(contentType)) {
|
|
3847
3966
|
throw new invalid_content_type_error_default(contentType);
|
|
3848
|
-
} else if (
|
|
3967
|
+
} else if (hasMediaTypeParams(contentType)) {
|
|
3849
3968
|
throw new unsupported_media_type_error_default(contentType);
|
|
3850
3969
|
}
|
|
3851
3970
|
return true;
|
|
@@ -3973,9 +4092,10 @@ function merge(dest, source) {
|
|
|
3973
4092
|
}
|
|
3974
4093
|
|
|
3975
4094
|
// src/packages/controller/utils/params-to-query.ts
|
|
3976
|
-
function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
4095
|
+
function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
|
|
3977
4096
|
const relationships = entries(model.relationships);
|
|
3978
4097
|
const includedFields = omit(fields, model.resourceName);
|
|
4098
|
+
const included = include && include.map((path) => path.split(".")[0]);
|
|
3979
4099
|
let query = {
|
|
3980
4100
|
id,
|
|
3981
4101
|
filter,
|
|
@@ -4018,9 +4138,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4018
4138
|
if (!value.includes(relationship.model.primaryKey)) {
|
|
4019
4139
|
value = [relationship.model.primaryKey, ...value];
|
|
4020
4140
|
}
|
|
4021
|
-
if (
|
|
4022
|
-
value = [...value, ...relationship.model.
|
|
4023
|
-
} 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) {
|
|
4024
4144
|
value = value.slice(0, 1);
|
|
4025
4145
|
}
|
|
4026
4146
|
return {
|
|
@@ -4035,18 +4155,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
|
|
|
4035
4155
|
}
|
|
4036
4156
|
|
|
4037
4157
|
// src/packages/controller/utils/find-one.ts
|
|
4038
|
-
function findOne(model, req) {
|
|
4158
|
+
function findOne(model, req, serializerFor) {
|
|
4039
4159
|
const params = merge(req.defaultParams, req.params);
|
|
4040
|
-
const { id, select, include } = paramsToQuery(model, params);
|
|
4160
|
+
const { id, select, include } = paramsToQuery(model, params, serializerFor);
|
|
4041
4161
|
return model.find(id).select(...select).include(include);
|
|
4042
4162
|
}
|
|
4043
4163
|
|
|
4044
4164
|
// src/packages/controller/utils/find-many.ts
|
|
4045
|
-
function findMany(model, req) {
|
|
4165
|
+
function findMany(model, req, serializerFor) {
|
|
4046
4166
|
const params = merge(req.defaultParams, req.params);
|
|
4047
4167
|
const { sort, page, limit, select, filter, include } = paramsToQuery(
|
|
4048
4168
|
model,
|
|
4049
|
-
params
|
|
4169
|
+
params,
|
|
4170
|
+
serializerFor
|
|
4050
4171
|
);
|
|
4051
4172
|
return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
|
|
4052
4173
|
}
|
|
@@ -4076,6 +4197,53 @@ function resolveRelationships(model, relationships = {}) {
|
|
|
4076
4197
|
);
|
|
4077
4198
|
}
|
|
4078
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
|
+
|
|
4079
4247
|
// src/packages/controller/constants.ts
|
|
4080
4248
|
var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
|
|
4081
4249
|
|
|
@@ -4261,6 +4429,74 @@ var Controller = class {
|
|
|
4261
4429
|
* @public
|
|
4262
4430
|
*/
|
|
4263
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
|
+
}
|
|
4264
4500
|
constructor({ model, namespace, serializer }) {
|
|
4265
4501
|
Object.assign(this, {
|
|
4266
4502
|
model,
|
|
@@ -4286,7 +4522,7 @@ var Controller = class {
|
|
|
4286
4522
|
* @public
|
|
4287
4523
|
*/
|
|
4288
4524
|
index(req) {
|
|
4289
|
-
return findMany(this.model, req);
|
|
4525
|
+
return findMany(this.model, req, (related) => this.serializerFor(related));
|
|
4290
4526
|
}
|
|
4291
4527
|
/**
|
|
4292
4528
|
* This method supports including relationships, and sparse fieldsets via
|
|
@@ -4301,7 +4537,7 @@ var Controller = class {
|
|
|
4301
4537
|
* @public
|
|
4302
4538
|
*/
|
|
4303
4539
|
show(req) {
|
|
4304
|
-
return findOne(this.model, req);
|
|
4540
|
+
return findOne(this.model, req, (related) => this.serializerFor(related));
|
|
4305
4541
|
}
|
|
4306
4542
|
/**
|
|
4307
4543
|
* Create and return a single Model instance that the Controller instance
|
|
@@ -4322,6 +4558,7 @@ var Controller = class {
|
|
|
4322
4558
|
data: { attributes, relationships }
|
|
4323
4559
|
}
|
|
4324
4560
|
} = req;
|
|
4561
|
+
await validateRelationships(model, relationships);
|
|
4325
4562
|
const record = await model.create({
|
|
4326
4563
|
...attributes,
|
|
4327
4564
|
...resolveRelationships(model, relationships)
|
|
@@ -4347,12 +4584,13 @@ var Controller = class {
|
|
|
4347
4584
|
*/
|
|
4348
4585
|
update(req) {
|
|
4349
4586
|
const { model } = this;
|
|
4350
|
-
return findOne(model, req).then((record) => {
|
|
4587
|
+
return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
|
|
4351
4588
|
const {
|
|
4352
4589
|
params: {
|
|
4353
4590
|
data: { attributes, relationships }
|
|
4354
4591
|
}
|
|
4355
4592
|
} = req;
|
|
4593
|
+
await validateRelationships(model, relationships);
|
|
4356
4594
|
return record.update({
|
|
4357
4595
|
...attributes,
|
|
4358
4596
|
...resolveRelationships(model, relationships)
|
|
@@ -4376,7 +4614,7 @@ var Controller = class {
|
|
|
4376
4614
|
* @public
|
|
4377
4615
|
*/
|
|
4378
4616
|
destroy(req) {
|
|
4379
|
-
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);
|
|
4380
4618
|
}
|
|
4381
4619
|
/**
|
|
4382
4620
|
* Respond to HEAD or OPTIONS requests.
|
|
@@ -4394,7 +4632,194 @@ var Controller = class {
|
|
|
4394
4632
|
var controller_default = Controller;
|
|
4395
4633
|
|
|
4396
4634
|
// src/packages/serializer/index.ts
|
|
4397
|
-
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
|
|
4398
4823
|
var Serializer = class {
|
|
4399
4824
|
/**
|
|
4400
4825
|
* An Array of the `hasOne` or `belongsTo` relationships on a Serializer
|
|
@@ -4481,9 +4906,17 @@ var Serializer = class {
|
|
|
4481
4906
|
* the resource and relationship objects in the returned [JSON API](
|
|
4482
4907
|
* http://jsonapi.org) document object.
|
|
4483
4908
|
*
|
|
4484
|
-
* @param {Array} options.include - An array of
|
|
4485
|
-
*
|
|
4486
|
-
* 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.
|
|
4487
4920
|
*
|
|
4488
4921
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
|
|
4489
4922
|
* object.
|
|
@@ -4494,38 +4927,61 @@ var Serializer = class {
|
|
|
4494
4927
|
data,
|
|
4495
4928
|
links,
|
|
4496
4929
|
domain,
|
|
4497
|
-
include
|
|
4930
|
+
include,
|
|
4931
|
+
namespace = this.namespace
|
|
4498
4932
|
}) {
|
|
4499
|
-
|
|
4500
|
-
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;
|
|
4501
4939
|
if (Array.isArray(data)) {
|
|
4502
|
-
|
|
4503
|
-
data
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
)
|
|
4940
|
+
primary = await Promise.all(
|
|
4941
|
+
data.map(
|
|
4942
|
+
(item) => this.formatOne({
|
|
4943
|
+
item,
|
|
4944
|
+
domain,
|
|
4945
|
+
related,
|
|
4946
|
+
namespace,
|
|
4947
|
+
include: names
|
|
4948
|
+
})
|
|
4512
4949
|
)
|
|
4513
|
-
|
|
4950
|
+
);
|
|
4951
|
+
serialized = { data: primary };
|
|
4514
4952
|
} else {
|
|
4515
|
-
|
|
4516
|
-
|
|
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({
|
|
4517
4969
|
domain,
|
|
4518
|
-
|
|
4970
|
+
records,
|
|
4519
4971
|
included,
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4972
|
+
namespace,
|
|
4973
|
+
model: opts.model,
|
|
4974
|
+
tree: children
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4524
4977
|
}
|
|
4525
|
-
|
|
4978
|
+
primary.forEach((resource2) => {
|
|
4979
|
+
included.delete(resourceKey(resource2));
|
|
4980
|
+
});
|
|
4981
|
+
if (included.size) {
|
|
4526
4982
|
serialized = {
|
|
4527
4983
|
...serialized,
|
|
4528
|
-
included:
|
|
4984
|
+
included: Array.from(included.values())
|
|
4529
4985
|
};
|
|
4530
4986
|
}
|
|
4531
4987
|
return {
|
|
@@ -4540,6 +4996,12 @@ var Serializer = class {
|
|
|
4540
4996
|
* Transform a single Model instance into a [JSON API](http://jsonapi.org)
|
|
4541
4997
|
* resource object.
|
|
4542
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
|
+
*
|
|
4543
5005
|
* @method formatOne
|
|
4544
5006
|
*
|
|
4545
5007
|
* @param {Object} options - An options object used for building the returned
|
|
@@ -4556,17 +5018,19 @@ var Serializer = class {
|
|
|
4556
5018
|
* the top level links object or relationship links objects in the returned
|
|
4557
5019
|
* [JSON API](http://jsonapi.org) resource object.
|
|
4558
5020
|
*
|
|
4559
|
-
* @param {Array} options.include - An array of
|
|
4560
|
-
*
|
|
4561
|
-
* 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`.
|
|
4562
5023
|
*
|
|
4563
|
-
* @param {
|
|
4564
|
-
*
|
|
4565
|
-
* 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.
|
|
4566
5027
|
*
|
|
4567
|
-
* @param {
|
|
4568
|
-
*
|
|
4569
|
-
*
|
|
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.
|
|
4570
5034
|
*
|
|
4571
5035
|
* @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
|
|
4572
5036
|
* object.
|
|
@@ -4577,13 +5041,15 @@ var Serializer = class {
|
|
|
4577
5041
|
item,
|
|
4578
5042
|
links,
|
|
4579
5043
|
domain,
|
|
4580
|
-
include,
|
|
4581
|
-
|
|
4582
|
-
|
|
5044
|
+
include = [],
|
|
5045
|
+
related,
|
|
5046
|
+
linkage,
|
|
5047
|
+
namespace = this.namespace
|
|
4583
5048
|
}) {
|
|
4584
5049
|
const { resourceName: type } = item;
|
|
4585
5050
|
const id = String(item.getPrimaryKey());
|
|
4586
|
-
|
|
5051
|
+
const names = [...this.hasOne, ...this.hasMany];
|
|
5052
|
+
let relationships;
|
|
4587
5053
|
const attributes = dasherizeKeys(
|
|
4588
5054
|
item.getAttributes(
|
|
4589
5055
|
...Object.keys(item.rawColumnData).filter(
|
|
@@ -4596,34 +5062,41 @@ var Serializer = class {
|
|
|
4596
5062
|
type,
|
|
4597
5063
|
attributes
|
|
4598
5064
|
};
|
|
4599
|
-
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
|
+
};
|
|
4600
5084
|
relationships = await promiseHash(
|
|
4601
|
-
|
|
5085
|
+
names.reduce(
|
|
4602
5086
|
(hash, name) => ({
|
|
4603
5087
|
...hash,
|
|
4604
|
-
[
|
|
4605
|
-
const
|
|
4606
|
-
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);
|
|
4607
5092
|
return {
|
|
4608
|
-
data:
|
|
4609
|
-
|
|
4610
|
-
const { data: relatedData } = await this.formatRelationship({
|
|
4611
|
-
domain,
|
|
4612
|
-
included,
|
|
4613
|
-
item: relatedItem,
|
|
4614
|
-
include: include.includes(name)
|
|
4615
|
-
});
|
|
4616
|
-
return relatedData;
|
|
4617
|
-
})
|
|
5093
|
+
data: value.map(
|
|
5094
|
+
(relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
|
|
4618
5095
|
)
|
|
4619
5096
|
};
|
|
4620
|
-
} else if (
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
included,
|
|
4624
|
-
item: related,
|
|
4625
|
-
include: include.includes(name)
|
|
4626
|
-
});
|
|
5097
|
+
} else if (value && value.id != null) {
|
|
5098
|
+
collect(name, [value]);
|
|
5099
|
+
return this.formatRelationship(value, domain, namespace);
|
|
4627
5100
|
}
|
|
4628
5101
|
return {
|
|
4629
5102
|
data: null
|
|
@@ -4638,16 +5111,9 @@ var Serializer = class {
|
|
|
4638
5111
|
serialized.relationships = relationships;
|
|
4639
5112
|
}
|
|
4640
5113
|
if (links || typeof links !== "boolean") {
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4645
|
-
};
|
|
4646
|
-
} else {
|
|
4647
|
-
serialized.links = {
|
|
4648
|
-
self: `${domain}/${type}/${id}`
|
|
4649
|
-
};
|
|
4650
|
-
}
|
|
5114
|
+
serialized.links = {
|
|
5115
|
+
self: this.linkFor(domain, type, id, namespace)
|
|
5116
|
+
};
|
|
4651
5117
|
}
|
|
4652
5118
|
return serialized;
|
|
4653
5119
|
}
|
|
@@ -4657,70 +5123,167 @@ var Serializer = class {
|
|
|
4657
5123
|
*
|
|
4658
5124
|
* @method formatRelationship
|
|
4659
5125
|
*
|
|
4660
|
-
* @param {
|
|
5126
|
+
* @param {Model} item - The Model instance to transform into the returned
|
|
4661
5127
|
* [JSON API](http://jsonapi.org) relationship object.
|
|
4662
5128
|
*
|
|
4663
|
-
* @param {
|
|
5129
|
+
* @param {String} domain - A string used to build links included in the
|
|
4664
5130
|
* returned [JSON API](http://jsonapi.org) relationship object.
|
|
4665
5131
|
*
|
|
4666
|
-
* @
|
|
4667
|
-
* the returned [JSON API](http://jsonapi.org) relationship object.
|
|
4668
|
-
*
|
|
4669
|
-
* @param {Array} options.include - An array of strings containing the
|
|
4670
|
-
* relationship keys that should be added to the top level included object of
|
|
4671
|
-
* a [JSON API](http://jsonapi.org) document object.
|
|
4672
|
-
*
|
|
4673
|
-
* @param {Array} options.included - An array of [JSON API](
|
|
4674
|
-
* http://jsonapi.org) resource objects that will be added to the top level
|
|
4675
|
-
* included array of a [JSON API](http://jsonapi.org) document object.
|
|
5132
|
+
* @return {Object} A [JSON API](http://jsonapi.org) relationship object.
|
|
4676
5133
|
*
|
|
4677
|
-
* @
|
|
4678
|
-
|
|
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 }`.
|
|
4679
5149
|
*
|
|
5150
|
+
* @method formatLinkage
|
|
4680
5151
|
* @private
|
|
4681
5152
|
*/
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
included
|
|
4687
|
-
}) {
|
|
4688
|
-
const { namespace } = this;
|
|
4689
|
-
const {
|
|
4690
|
-
resourceName: type,
|
|
4691
|
-
constructor: { serializer }
|
|
4692
|
-
} = item;
|
|
4693
|
-
const id = String(item.getPrimaryKey());
|
|
4694
|
-
let links;
|
|
4695
|
-
if (namespace) {
|
|
4696
|
-
links = {
|
|
4697
|
-
self: `${domain}/${namespace}/${type}/${id}`
|
|
4698
|
-
};
|
|
4699
|
-
} else {
|
|
4700
|
-
links = {
|
|
4701
|
-
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 })) : []
|
|
4702
5157
|
};
|
|
4703
5158
|
}
|
|
4704
|
-
if (
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
domain,
|
|
4709
|
-
include: [],
|
|
4710
|
-
included: [],
|
|
4711
|
-
formatRelationships: false
|
|
4712
|
-
})
|
|
4713
|
-
);
|
|
5159
|
+
if (linkage == null || !type) {
|
|
5160
|
+
return {
|
|
5161
|
+
data: null
|
|
5162
|
+
};
|
|
4714
5163
|
}
|
|
4715
5164
|
return {
|
|
4716
5165
|
data: {
|
|
4717
|
-
id,
|
|
5166
|
+
id: linkage,
|
|
4718
5167
|
type
|
|
4719
5168
|
},
|
|
4720
|
-
links
|
|
5169
|
+
links: {
|
|
5170
|
+
self: this.linkFor(domain, type, linkage, namespace)
|
|
5171
|
+
}
|
|
4721
5172
|
};
|
|
4722
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
|
+
}
|
|
4723
5283
|
};
|
|
5284
|
+
function resourceKey({ id, type }) {
|
|
5285
|
+
return `${type}:${id}`;
|
|
5286
|
+
}
|
|
4724
5287
|
var serializer_default = Serializer;
|
|
4725
5288
|
|
|
4726
5289
|
// src/packages/config/index.ts
|
|
@@ -4919,7 +5482,10 @@ function resource(action) {
|
|
|
4919
5482
|
data,
|
|
4920
5483
|
links,
|
|
4921
5484
|
domain,
|
|
4922
|
-
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
|
|
4923
5489
|
});
|
|
4924
5490
|
}
|
|
4925
5491
|
return data;
|
|
@@ -5000,6 +5566,7 @@ var ParameterTypeError = class extends TypeError {
|
|
|
5000
5566
|
Expected type '${type || "undefined"}' for parameter '${path}' but got
|
|
5001
5567
|
'${actual}'.
|
|
5002
5568
|
`);
|
|
5569
|
+
this.source = sourceFor(path);
|
|
5003
5570
|
}
|
|
5004
5571
|
};
|
|
5005
5572
|
var parameter_type_error_default = createServerError(ParameterTypeError, 400);
|
|
@@ -5012,6 +5579,7 @@ var ParameterValueError = class extends TypeError {
|
|
|
5012
5579
|
[${param.size ? Array.from(param.values()).join(", ") : ""}] but got
|
|
5013
5580
|
${String(actual)}.
|
|
5014
5581
|
`);
|
|
5582
|
+
this.source = sourceFor(param.path);
|
|
5015
5583
|
}
|
|
5016
5584
|
};
|
|
5017
5585
|
var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
@@ -5020,6 +5588,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
|
|
|
5020
5588
|
var InvalidParameterError = class extends TypeError {
|
|
5021
5589
|
constructor(path) {
|
|
5022
5590
|
super(`'${path}' is not a valid parameter for this resource.`);
|
|
5591
|
+
this.source = sourceFor(path);
|
|
5023
5592
|
}
|
|
5024
5593
|
};
|
|
5025
5594
|
var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
|
|
@@ -5035,6 +5604,7 @@ var ResourceMismatchError = class extends TypeError {
|
|
|
5035
5604
|
Expected '${String(expected)}' for parameter '${path}' but got
|
|
5036
5605
|
${String(normalized)}.
|
|
5037
5606
|
`);
|
|
5607
|
+
this.source = sourceFor(path);
|
|
5038
5608
|
}
|
|
5039
5609
|
};
|
|
5040
5610
|
var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
|
|
@@ -5043,14 +5613,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
|
|
|
5043
5613
|
var ParameterRequiredError = class extends TypeError {
|
|
5044
5614
|
constructor(path) {
|
|
5045
5615
|
super(`Missing required parameter '${path}'.`);
|
|
5616
|
+
this.source = sourceFor(path);
|
|
5046
5617
|
}
|
|
5047
5618
|
};
|
|
5048
5619
|
var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
|
|
5049
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
|
+
|
|
5050
5639
|
// src/packages/router/route/params/errors/parameter-not-nullable-error.ts
|
|
5051
5640
|
var ParameterNotNullableError = class extends TypeError {
|
|
5052
5641
|
constructor({ path }) {
|
|
5053
5642
|
super(`Parameter '${path}' is not nullable.`);
|
|
5643
|
+
this.source = sourceFor(path);
|
|
5054
5644
|
}
|
|
5055
5645
|
};
|
|
5056
5646
|
var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
|
|
@@ -5179,13 +5769,14 @@ var Parameter = class extends set_default {
|
|
|
5179
5769
|
path,
|
|
5180
5770
|
type,
|
|
5181
5771
|
required: Boolean(required),
|
|
5182
|
-
sanitize: Boolean(sanitize)
|
|
5772
|
+
sanitize: Boolean(sanitize),
|
|
5773
|
+
restricted: values !== void 0
|
|
5183
5774
|
});
|
|
5184
5775
|
this.freeze();
|
|
5185
5776
|
}
|
|
5186
5777
|
validate(value) {
|
|
5187
5778
|
validateType2(this, value);
|
|
5188
|
-
if (this.
|
|
5779
|
+
if (this.restricted) {
|
|
5189
5780
|
return validateValue(this, value);
|
|
5190
5781
|
}
|
|
5191
5782
|
return value;
|
|
@@ -5204,6 +5795,17 @@ function getURLParams(dynamicSegments) {
|
|
|
5204
5795
|
]);
|
|
5205
5796
|
}
|
|
5206
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
|
+
|
|
5207
5809
|
// src/packages/router/route/params/utils/get-data-params.ts
|
|
5208
5810
|
function getIDParam({ model }) {
|
|
5209
5811
|
const primaryKeyColumn = model.columnFor(model.primaryKey);
|
|
@@ -5256,16 +5858,48 @@ function getRelationshipsParam({
|
|
|
5256
5858
|
model,
|
|
5257
5859
|
params
|
|
5258
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
|
+
]);
|
|
5259
5865
|
return [
|
|
5260
5866
|
"relationships",
|
|
5261
5867
|
new parameter_group_default(
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
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
|
+
}
|
|
5269
5903
|
return [
|
|
5270
5904
|
...group,
|
|
5271
5905
|
[
|
|
@@ -5274,11 +5908,32 @@ function getRelationshipsParam({
|
|
|
5274
5908
|
[
|
|
5275
5909
|
[
|
|
5276
5910
|
"data",
|
|
5277
|
-
new
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
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
|
+
)
|
|
5282
5937
|
]
|
|
5283
5938
|
],
|
|
5284
5939
|
{
|
|
@@ -5287,55 +5942,9 @@ function getRelationshipsParam({
|
|
|
5287
5942
|
)
|
|
5288
5943
|
]
|
|
5289
5944
|
];
|
|
5290
|
-
}
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
if (primaryKeyColumn) {
|
|
5294
|
-
primaryKeyType = typeForColumn(primaryKeyColumn);
|
|
5295
|
-
}
|
|
5296
|
-
return [
|
|
5297
|
-
...group,
|
|
5298
|
-
[
|
|
5299
|
-
param,
|
|
5300
|
-
new parameter_group_default(
|
|
5301
|
-
[
|
|
5302
|
-
[
|
|
5303
|
-
"data",
|
|
5304
|
-
new parameter_group_default(
|
|
5305
|
-
[
|
|
5306
|
-
[
|
|
5307
|
-
"id",
|
|
5308
|
-
new parameter_default({
|
|
5309
|
-
type: primaryKeyType,
|
|
5310
|
-
path: `${path}.data.id`,
|
|
5311
|
-
required: true
|
|
5312
|
-
})
|
|
5313
|
-
],
|
|
5314
|
-
[
|
|
5315
|
-
"type",
|
|
5316
|
-
new parameter_default({
|
|
5317
|
-
type: "string",
|
|
5318
|
-
path: `${path}.data.type`,
|
|
5319
|
-
values: [opts.model.resourceName],
|
|
5320
|
-
required: true
|
|
5321
|
-
})
|
|
5322
|
-
]
|
|
5323
|
-
],
|
|
5324
|
-
{
|
|
5325
|
-
type: "array",
|
|
5326
|
-
path: `${path}.data`,
|
|
5327
|
-
required: true
|
|
5328
|
-
}
|
|
5329
|
-
)
|
|
5330
|
-
]
|
|
5331
|
-
],
|
|
5332
|
-
{
|
|
5333
|
-
path
|
|
5334
|
-
}
|
|
5335
|
-
)
|
|
5336
|
-
]
|
|
5337
|
-
];
|
|
5338
|
-
}, []),
|
|
5945
|
+
}, []),
|
|
5946
|
+
...forbidden
|
|
5947
|
+
],
|
|
5339
5948
|
{
|
|
5340
5949
|
path: "data.relationships"
|
|
5341
5950
|
}
|
|
@@ -5443,10 +6052,9 @@ function getFilterParam({ filter }) {
|
|
|
5443
6052
|
)
|
|
5444
6053
|
];
|
|
5445
6054
|
}
|
|
5446
|
-
function getFieldsParam({
|
|
5447
|
-
model,
|
|
5448
|
-
|
|
5449
|
-
}) {
|
|
6055
|
+
function getFieldsParam(controller) {
|
|
6056
|
+
const { model, serializer } = controller;
|
|
6057
|
+
const { hasOne, hasMany, attributes } = serializer;
|
|
5450
6058
|
const relationships = [...hasOne, ...hasMany];
|
|
5451
6059
|
return [
|
|
5452
6060
|
"fields",
|
|
@@ -5475,7 +6083,7 @@ function getFieldsParam({
|
|
|
5475
6083
|
sanitize: true,
|
|
5476
6084
|
values: [
|
|
5477
6085
|
opts.model.primaryKey,
|
|
5478
|
-
...opts.model.
|
|
6086
|
+
...controller.serializerFor(opts.model).attributes
|
|
5479
6087
|
]
|
|
5480
6088
|
})
|
|
5481
6089
|
]
|
|
@@ -5493,16 +6101,30 @@ function getFieldsParam({
|
|
|
5493
6101
|
)
|
|
5494
6102
|
];
|
|
5495
6103
|
}
|
|
5496
|
-
function getIncludeParam({
|
|
5497
|
-
|
|
5498
|
-
}
|
|
6104
|
+
function getIncludeParam(controller) {
|
|
6105
|
+
const { model, maxIncludeDepth, serializer } = controller;
|
|
6106
|
+
const { hasOne, hasMany } = serializer;
|
|
5499
6107
|
const relationships = [...hasOne, ...hasMany];
|
|
5500
6108
|
return [
|
|
5501
6109
|
"include",
|
|
5502
6110
|
new parameter_default({
|
|
5503
6111
|
path: "include",
|
|
5504
6112
|
type: "array",
|
|
5505
|
-
|
|
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
|
+
)
|
|
5506
6128
|
})
|
|
5507
6129
|
];
|
|
5508
6130
|
}
|
|
@@ -5540,6 +6162,15 @@ function getCollectionQueryParams(controller) {
|
|
|
5540
6162
|
return getCustomParams(controller);
|
|
5541
6163
|
}
|
|
5542
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
|
+
|
|
5543
6174
|
// src/packages/router/route/params/utils/validate-resource-id.ts
|
|
5544
6175
|
function validateResourceId({
|
|
5545
6176
|
params: {
|
|
@@ -5705,6 +6336,9 @@ var Route = class extends set_default {
|
|
|
5705
6336
|
...req.params,
|
|
5706
6337
|
...this.parseParams(req.url.params)
|
|
5707
6338
|
};
|
|
6339
|
+
if (this.action === "create" && req.method === "POST") {
|
|
6340
|
+
validateClientId(params);
|
|
6341
|
+
}
|
|
5708
6342
|
if (req.method !== "OPTIONS") {
|
|
5709
6343
|
params = this.params.validate(params);
|
|
5710
6344
|
}
|
|
@@ -6020,7 +6654,7 @@ var router_default = Router;
|
|
|
6020
6654
|
import { join as joinPath2 } from "path";
|
|
6021
6655
|
|
|
6022
6656
|
// src/packages/loader/utils/format-key.ts
|
|
6023
|
-
import { dasherize as
|
|
6657
|
+
import { dasherize as dasherize6 } from "inflection";
|
|
6024
6658
|
var NAMESPACE_DELIMITER = /\$-/g;
|
|
6025
6659
|
function formatKey(key, formatter) {
|
|
6026
6660
|
return chain(key).pipe((str) => {
|
|
@@ -6028,7 +6662,7 @@ function formatKey(key, formatter) {
|
|
|
6028
6662
|
return formatter(str);
|
|
6029
6663
|
}
|
|
6030
6664
|
return str;
|
|
6031
|
-
}).pipe(underscore).pipe(
|
|
6665
|
+
}).pipe(underscore).pipe(dasherize6).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
|
|
6032
6666
|
}
|
|
6033
6667
|
|
|
6034
6668
|
// src/packages/loader/utils/bundle-for.ts
|
|
@@ -6100,26 +6734,10 @@ function bundleFor(path) {
|
|
|
6100
6734
|
return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
|
|
6101
6735
|
}
|
|
6102
6736
|
|
|
6103
|
-
// src/packages/loader/resolver/utils/closest-ancestor.ts
|
|
6104
|
-
import { posix } from "path";
|
|
6105
|
-
function closestAncestor(source, key) {
|
|
6106
|
-
const name = posix.basename(key);
|
|
6107
|
-
let namespace = posix.dirname(key);
|
|
6108
|
-
if (namespace === ".") {
|
|
6109
|
-
return source.get(name);
|
|
6110
|
-
}
|
|
6111
|
-
namespace = posix.dirname(namespace);
|
|
6112
|
-
const ancestor = source.get(posix.join(namespace, name));
|
|
6113
|
-
if (ancestor) {
|
|
6114
|
-
return ancestor;
|
|
6115
|
-
}
|
|
6116
|
-
return closestAncestor(source, posix.join(posix.dirname(namespace), name));
|
|
6117
|
-
}
|
|
6118
|
-
|
|
6119
6737
|
// src/packages/loader/resolver/utils/closest-child.ts
|
|
6120
|
-
import { posix as
|
|
6738
|
+
import { posix as posix3 } from "path";
|
|
6121
6739
|
function closestChild(source, key) {
|
|
6122
|
-
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);
|
|
6123
6741
|
return result;
|
|
6124
6742
|
}
|
|
6125
6743
|
|
|
@@ -6142,7 +6760,7 @@ function resolve(group) {
|
|
|
6142
6760
|
}
|
|
6143
6761
|
|
|
6144
6762
|
// src/packages/loader/builder/utils/create-parent-builder.ts
|
|
6145
|
-
import { posix as
|
|
6763
|
+
import { posix as posix4 } from "path";
|
|
6146
6764
|
|
|
6147
6765
|
// src/packages/loader/builder/utils/sort-by-namespace.ts
|
|
6148
6766
|
function sortByNamespace([a], [b]) {
|
|
@@ -6163,7 +6781,7 @@ function createParentBuilder(construct) {
|
|
|
6163
6781
|
let grandparent = null;
|
|
6164
6782
|
if (key !== "root") {
|
|
6165
6783
|
const found = result.find((namespace) => {
|
|
6166
|
-
const dirname =
|
|
6784
|
+
const dirname = posix4.dirname(key);
|
|
6167
6785
|
if (namespace.key === "root") {
|
|
6168
6786
|
return dirname === ".";
|
|
6169
6787
|
}
|
|
@@ -6233,13 +6851,13 @@ function normalizePort(port) {
|
|
|
6233
6851
|
}
|
|
6234
6852
|
|
|
6235
6853
|
// src/packages/application/utils/create-controller.ts
|
|
6236
|
-
import { posix as
|
|
6854
|
+
import { posix as posix5 } from "path";
|
|
6237
6855
|
function createController(constructor, opts) {
|
|
6238
6856
|
const { key, store, serializers } = opts;
|
|
6239
|
-
const namespace =
|
|
6857
|
+
const namespace = posix5.dirname(key).replace(".", "");
|
|
6240
6858
|
let { parent } = opts;
|
|
6241
6859
|
let model = tryCatchSync(
|
|
6242
|
-
() => store.modelFor(
|
|
6860
|
+
() => store.modelFor(posix5.basename(key))
|
|
6243
6861
|
);
|
|
6244
6862
|
let serializer = serializers.get(key);
|
|
6245
6863
|
if (!model) {
|
|
@@ -6295,13 +6913,13 @@ function createController(constructor, opts) {
|
|
|
6295
6913
|
}
|
|
6296
6914
|
|
|
6297
6915
|
// src/packages/application/utils/create-serializer.ts
|
|
6298
|
-
import { posix as
|
|
6916
|
+
import { posix as posix6 } from "path";
|
|
6299
6917
|
function createSerializer(constructor, opts) {
|
|
6300
6918
|
const { key, store } = opts;
|
|
6301
|
-
const namespace =
|
|
6919
|
+
const namespace = posix6.dirname(key).replace(".", "");
|
|
6302
6920
|
let { parent } = opts;
|
|
6303
6921
|
let model = tryCatchSync(
|
|
6304
|
-
() => store.modelFor(
|
|
6922
|
+
() => store.modelFor(posix6.basename(key))
|
|
6305
6923
|
);
|
|
6306
6924
|
if (!model) {
|
|
6307
6925
|
model = null;
|
|
@@ -6325,6 +6943,85 @@ function createSerializer(constructor, opts) {
|
|
|
6325
6943
|
return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
|
|
6326
6944
|
}
|
|
6327
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
|
+
|
|
6328
7025
|
// src/packages/application/initialize.ts
|
|
6329
7026
|
async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
|
|
6330
7027
|
const load = createLoader(path);
|
|
@@ -6347,6 +7044,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6347
7044
|
parent
|
|
6348
7045
|
})
|
|
6349
7046
|
);
|
|
7047
|
+
serializers.forEach((serializer) => {
|
|
7048
|
+
Reflect.defineProperty(serializer, "serializers", {
|
|
7049
|
+
value: serializers,
|
|
7050
|
+
writable: false,
|
|
7051
|
+
enumerable: false,
|
|
7052
|
+
configurable: false
|
|
7053
|
+
});
|
|
7054
|
+
});
|
|
6350
7055
|
models.forEach((model) => {
|
|
6351
7056
|
Reflect.defineProperty(model, "serializer", {
|
|
6352
7057
|
value: closestChild(serializers, model.resourceName),
|
|
@@ -6372,6 +7077,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
|
|
|
6372
7077
|
configurable: false
|
|
6373
7078
|
});
|
|
6374
7079
|
});
|
|
7080
|
+
validateNamespacedSerializers(controllers, serializers);
|
|
6375
7081
|
const ApplicationController = controllers.get("application");
|
|
6376
7082
|
if (!ApplicationController) {
|
|
6377
7083
|
throw new controller_missing_error_default("application");
|