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.
Files changed (48) hide show
  1. package/dist/cli.cjs +71 -38
  2. package/dist/cli.cjs.map +4 -4
  3. package/dist/index.js +978 -272
  4. package/dist/index.js.map +4 -4
  5. package/dist/index.mjs +969 -263
  6. package/dist/index.mjs.map +4 -4
  7. package/dist/types/errors/namespaced-serializer-missing-error.d.ts +15 -0
  8. package/dist/types/packages/application/utils/validate-namespaced-serializers.d.ts +15 -0
  9. package/dist/types/packages/controller/errors/index.d.ts +1 -0
  10. package/dist/types/packages/controller/errors/related-record-not-found-error.d.ts +14 -0
  11. package/dist/types/packages/controller/index.d.ts +65 -0
  12. package/dist/types/packages/controller/utils/find-many.d.ts +2 -1
  13. package/dist/types/packages/controller/utils/find-one.d.ts +2 -1
  14. package/dist/types/packages/controller/utils/params-to-query.d.ts +7 -2
  15. package/dist/types/packages/controller/utils/validate-relationships.d.ts +8 -0
  16. package/dist/types/packages/database/constants.d.ts +1 -0
  17. package/dist/types/packages/database/model/index.d.ts +11 -0
  18. package/dist/types/packages/database/model/utils/process-write-error.d.ts +9 -1
  19. package/dist/types/packages/database/validation/errors/validation-error.d.ts +11 -2
  20. package/dist/types/packages/jsonapi/errors/not-acceptable-error.d.ts +1 -1
  21. package/dist/types/packages/jsonapi/index.d.ts +3 -1
  22. package/dist/types/packages/jsonapi/interfaces.d.ts +1 -1
  23. package/dist/types/packages/jsonapi/utils/has-media-type-params.d.ts +6 -0
  24. package/dist/types/packages/jsonapi/utils/is-jsonapi.d.ts +2 -0
  25. package/dist/types/packages/jsonapi/utils/media-type.d.ts +19 -0
  26. package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +13 -0
  27. package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +14 -0
  28. package/dist/types/packages/router/route/params/errors/index.d.ts +2 -0
  29. package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +2 -0
  30. package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +2 -0
  31. package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +2 -0
  32. package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +2 -0
  33. package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +2 -0
  34. package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +2 -0
  35. package/dist/types/packages/router/route/params/index.d.ts +1 -0
  36. package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +12 -0
  37. package/dist/types/packages/router/route/params/parameter/index.d.ts +8 -0
  38. package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +8 -0
  39. package/dist/types/packages/serializer/index.d.ts +111 -36
  40. package/dist/types/packages/serializer/utils/include-tree.d.ts +29 -0
  41. package/dist/types/packages/serializer/utils/load-linkage.d.ts +22 -0
  42. package/dist/types/packages/server/index.d.ts +2 -1
  43. package/dist/types/packages/server/interfaces.d.ts +5 -0
  44. package/dist/types/packages/server/utils/source-for.d.ts +10 -0
  45. package/dist/types/packages/server/utils/validate-accept.d.ts +5 -1
  46. package/dist/types/packages/server/utils/validate-content-type.d.ts +4 -0
  47. package/package.json +1 -1
  48. 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 (value == null && pkPattern.test(key)) {
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, value) {
2516
- super(`Validation failed for ${key}: ${value}`);
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, String(validation.value));
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 dasherize3 } from "inflection";
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) => dasherize3(underscore(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(contentType) {
3765
+ constructor(accept) {
3698
3766
  super(line`
3699
- Media type parameters is not supported. Try your request again
3700
- without specifying '${contentType.replace(MIME_TYPE, "")}'.
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 is not supported. Try your request again
3711
- without specifying '${contentType.replace(MIME_TYPE, "")}'.
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, 400);
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.startsWith(MIME_TYPE);
3843
+ return parseMediaType(value).type === MIME_TYPE;
3731
3844
  }
3732
3845
 
3733
- // src/packages/jsonapi/utils/has-media-type.ts
3734
- var PATTERN2 = /^application\/vnd.api\+json;charset=.+$/;
3735
- function hasMediaType(value) {
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(contentType) {
3838
- if (contentType && hasMediaType(contentType)) {
3839
- throw new not_acceptable_error_default(contentType);
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 (hasMediaType(contentType)) {
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 (include && value.length === 1 && include.includes(name)) {
4022
- value = [...value, ...relationship.model.serializer.attributes];
4023
- } else if (!include && value.length > 1) {
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 { dasherize as dasherize4 } from "inflection";
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 strings containing the
4485
- * relationship keys that should be added to the top level included object of
4486
- * the returned [JSON API](http://jsonapi.org) document object.
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
- let serialized = {};
4500
- const included = [];
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
- serialized = {
4503
- data: await Promise.all(
4504
- data.map(
4505
- (item) => this.formatOne({
4506
- item,
4507
- domain,
4508
- include,
4509
- included
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
- serialized = {
4516
- data: await this.formatOne({
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
- include,
4970
+ records,
4519
4971
  included,
4520
- item: data,
4521
- links: false
4522
- })
4523
- };
4972
+ namespace,
4973
+ model: opts.model,
4974
+ tree: children
4975
+ });
4976
+ }
4524
4977
  }
4525
- if (included.length) {
4978
+ primary.forEach((resource2) => {
4979
+ included.delete(resourceKey(resource2));
4980
+ });
4981
+ if (included.size) {
4526
4982
  serialized = {
4527
4983
  ...serialized,
4528
- included: uniq(included, "id", "type")
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 strings containing the
4560
- * relationship keys that should be added to the top level included object of
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 {Array} options.included - An array of [JSON API](
4564
- * http://jsonapi.org) resource objects that will be added to the top level
4565
- * included array of a [JSON API](http://jsonapi.org) document object.
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 {Boolean} options.formatRelationships - Wether or not
4568
- * relationships should be formatted and included in the returned
4569
- * [JSON API](http://jsonapi.org) resource object.
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
- included,
4582
- formatRelationships = true
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
- let relationships = {};
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 (formatRelationships) {
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
- [...this.hasOne, ...this.hasMany].reduce(
5085
+ names.reduce(
4602
5086
  (hash, name) => ({
4603
5087
  ...hash,
4604
- [dasherize4(underscore(name))]: (async () => {
4605
- const related = await Reflect.get(item, name);
4606
- if (Array.isArray(related)) {
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: await Promise.all(
4609
- related.map(async (relatedItem) => {
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 (related && related.id != null) {
4621
- return this.formatRelationship({
4622
- domain,
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
- const { namespace } = this;
4642
- if (namespace) {
4643
- serialized.links = {
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 {Object} options - An options object used for building the returned
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 {Model} options.item - The Model instance to transform into the
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
- * @param {String} options.domain - A string used to build links included in
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
- * @return {Promise} Resolves with a [JSON API](http://jsonapi.org)
4678
- * relationship object.
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
- async formatRelationship({
4683
- item,
4684
- domain,
4685
- include,
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 (include) {
4705
- included.push(
4706
- await serializer.formatOne({
4707
- item,
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.size > 0) {
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
- params.reduce((group, param) => {
5263
- const path = `data.relationships.${param}`;
5264
- const opts = model.relationshipFor(param);
5265
- if (!opts) {
5266
- return group;
5267
- }
5268
- if (opts.type === "hasMany") {
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 parameter_default({
5278
- type: "array",
5279
- path: `${path}.data`,
5280
- required: true
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
- const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5292
- let primaryKeyType = "number";
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
- serializer: { hasOne, hasMany, attributes }
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.serializer.attributes
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
- serializer: { hasOne, hasMany }
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
- values: relationships
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 dasherize5 } from "inflection";
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(dasherize5).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
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 posix2 } from "path";
6738
+ import { posix as posix3 } from "path";
6121
6739
  function closestChild(source, key) {
6122
- const [[, result] = []] = Array.from(source).map(([path, value]) => [posix2.basename(path), value]).filter(([resource2]) => key === resource2);
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 posix3 } from "path";
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 = posix3.dirname(key);
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 posix4 } from "path";
6854
+ import { posix as posix5 } from "path";
6237
6855
  function createController(constructor, opts) {
6238
6856
  const { key, store, serializers } = opts;
6239
- const namespace = posix4.dirname(key).replace(".", "");
6857
+ const namespace = posix5.dirname(key).replace(".", "");
6240
6858
  let { parent } = opts;
6241
6859
  let model = tryCatchSync(
6242
- () => store.modelFor(posix4.basename(key))
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 posix5 } from "path";
6916
+ import { posix as posix6 } from "path";
6299
6917
  function createSerializer(constructor, opts) {
6300
6918
  const { key, store } = opts;
6301
- const namespace = posix5.dirname(key).replace(".", "");
6919
+ const namespace = posix6.dirname(key).replace(".", "");
6302
6920
  let { parent } = opts;
6303
6921
  let model = tryCatchSync(
6304
- () => store.modelFor(posix5.basename(key))
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");