lumen-framework 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli.cjs +70 -38
  2. package/dist/cli.cjs.map +4 -4
  3. package/dist/index.js +928 -259
  4. package/dist/index.js.map +4 -4
  5. package/dist/index.mjs +919 -250
  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/interfaces.d.ts +1 -1
  21. package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +13 -0
  22. package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +14 -0
  23. package/dist/types/packages/router/route/params/errors/index.d.ts +2 -0
  24. package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +2 -0
  25. package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +2 -0
  26. package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +2 -0
  27. package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +2 -0
  28. package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +2 -0
  29. package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +2 -0
  30. package/dist/types/packages/router/route/params/index.d.ts +1 -0
  31. package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +12 -0
  32. package/dist/types/packages/router/route/params/parameter/index.d.ts +8 -0
  33. package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +8 -0
  34. package/dist/types/packages/serializer/index.d.ts +111 -36
  35. package/dist/types/packages/serializer/utils/include-tree.d.ts +29 -0
  36. package/dist/types/packages/serializer/utils/load-linkage.d.ts +22 -0
  37. package/dist/types/packages/server/index.d.ts +2 -1
  38. package/dist/types/packages/server/interfaces.d.ts +5 -0
  39. package/dist/types/packages/server/utils/source-for.d.ts +10 -0
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ module.exports = __toCommonJS(index_exports);
43
43
  var import_chalk4 = require("chalk");
44
44
 
45
45
  // src/packages/database/constants.ts
46
+ var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
47
+ var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
46
48
  var VALID_DRIVERS = [
47
49
  "pg",
48
50
  "sqlite3",
@@ -892,7 +894,7 @@ function underscore(source = "", upper = false) {
892
894
  var normalize_model_name_default = compose(import_inflection2.singularize, import_inflection2.dasherize, underscore);
893
895
 
894
896
  // src/packages/database/model/index.ts
895
- var import_inflection9 = require("inflection");
897
+ var import_inflection10 = require("inflection");
896
898
 
897
899
  // src/packages/database/query/index.ts
898
900
  var import_inflection5 = require("inflection");
@@ -1090,10 +1092,16 @@ async function buildResults({
1090
1092
  }
1091
1093
  );
1092
1094
  }
1095
+ const missing = new Set(
1096
+ entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
1097
+ );
1098
+ const absent = Array.from(missing).filter(
1099
+ (name) => model.relationshipFor(name)?.type === "hasOne"
1100
+ );
1093
1101
  const instance = Reflect.construct(model, [
1094
1102
  entries(record).reduce((r, entry) => {
1095
1103
  let [key, value] = entry;
1096
- if (value == null && pkPattern.test(key)) {
1104
+ if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
1097
1105
  return r;
1098
1106
  } else if (key.indexOf(".") >= 0) {
1099
1107
  const [a, b] = key.split(".");
@@ -1113,6 +1121,7 @@ async function buildResults({
1113
1121
  };
1114
1122
  }, {})
1115
1123
  ]);
1124
+ absent.forEach((name) => instance.absentRelationships.add(name));
1116
1125
  instance.currentChangeSet.persist();
1117
1126
  return instance;
1118
1127
  });
@@ -1124,6 +1133,17 @@ function createRunner(target, opts) {
1124
1133
  const { resolve: resolve2, reject } = opts;
1125
1134
  let didRun = false;
1126
1135
  RUNNERS.set(target, async () => {
1136
+ if (didRun) {
1137
+ return;
1138
+ }
1139
+ didRun = true;
1140
+ try {
1141
+ await run();
1142
+ } catch (err) {
1143
+ reject(err);
1144
+ }
1145
+ });
1146
+ async function run() {
1127
1147
  let results;
1128
1148
  const {
1129
1149
  model,
@@ -1133,10 +1153,6 @@ function createRunner(target, opts) {
1133
1153
  shouldCount,
1134
1154
  relationships
1135
1155
  } = target;
1136
- if (didRun) {
1137
- return;
1138
- }
1139
- didRun = true;
1140
1156
  if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
1141
1157
  target.select(...target.model.attributeNames);
1142
1158
  }
@@ -1177,7 +1193,7 @@ function createRunner(target, opts) {
1177
1193
  resolve2(result);
1178
1194
  }
1179
1195
  }
1180
- });
1196
+ }
1181
1197
  }
1182
1198
  }
1183
1199
  function runQuery(target) {
@@ -1188,6 +1204,17 @@ function runQuery(target) {
1188
1204
  }
1189
1205
 
1190
1206
  // src/packages/database/query/index.ts
1207
+ var CONDITIONS = /* @__PURE__ */ new Set([
1208
+ "where",
1209
+ "whereNot",
1210
+ "whereIn",
1211
+ "whereNotIn",
1212
+ "whereNull",
1213
+ "whereNotNull",
1214
+ "whereBetween",
1215
+ "whereNotBetween",
1216
+ "whereRaw"
1217
+ ]);
1191
1218
  var Query = class extends Promise {
1192
1219
  constructor(model) {
1193
1220
  let resolve2;
@@ -1380,12 +1407,11 @@ var Query = class extends Promise {
1380
1407
  return this;
1381
1408
  }
1382
1409
  count() {
1383
- const validName = /^(where(((Not)?(In)?)|(Raw)|(Between)))$/g;
1384
1410
  Object.assign(this, {
1385
1411
  shouldCount: true,
1386
1412
  snapshots: [
1387
1413
  ["count", "* as countAll"],
1388
- ...this.snapshots.filter(([name]) => validName.test(name))
1414
+ ...this.snapshots.filter(([name]) => CONDITIONS.has(name))
1389
1415
  ]
1390
1416
  });
1391
1417
  return this;
@@ -1882,7 +1908,9 @@ async function get(owner, key) {
1882
1908
  let { foreignKey } = opts;
1883
1909
  value = owner.currentChangeSet.get(key);
1884
1910
  foreignKey = (0, import_inflection7.camelize)(foreignKey, true);
1885
- if (!value) {
1911
+ if (!value && owner.absentRelationships.has(key)) {
1912
+ value = null;
1913
+ } else if (!value) {
1886
1914
  switch (type) {
1887
1915
  case "hasOne":
1888
1916
  value = await getHasOne(owner, {
@@ -2544,13 +2572,37 @@ async function initializeClass({
2544
2572
  return model;
2545
2573
  }
2546
2574
 
2575
+ // src/packages/server/utils/source-for.ts
2576
+ var import_inflection9 = require("inflection");
2577
+ function memberName(key) {
2578
+ return (0, import_inflection9.dasherize)(underscore(key));
2579
+ }
2580
+ function escapePointer(segment) {
2581
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
2582
+ }
2583
+ function sourceFor(path) {
2584
+ const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
2585
+ if (!root) {
2586
+ return {};
2587
+ } else if (root === "data") {
2588
+ return {
2589
+ pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
2590
+ };
2591
+ }
2592
+ return {
2593
+ parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
2594
+ };
2595
+ }
2596
+
2547
2597
  // src/packages/database/validation/errors/validation-error.ts
2548
2598
  var ValidationError = class extends Error {
2549
- constructor(key, value) {
2550
- super(`Validation failed for ${key}: ${value}`);
2599
+ constructor(key) {
2600
+ super(`Validation failed for ${key}.`);
2601
+ this.key = key;
2602
+ this.source = sourceFor(`data.attributes.${key}`);
2551
2603
  }
2552
2604
  };
2553
- var validation_error_default = ValidationError;
2605
+ var validation_error_default = createServerError(ValidationError, 422);
2554
2606
 
2555
2607
  // src/packages/database/validation/index.ts
2556
2608
  var Validation = class {
@@ -2590,12 +2642,32 @@ function validate(instance) {
2590
2642
  validator: Reflect.get(instance.constructor.validates, key)
2591
2643
  })).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
2592
2644
  if (!validation.isValid()) {
2593
- throw new validation_error_default(validation.key, String(validation.value));
2645
+ throw new validation_error_default(validation.key);
2594
2646
  }
2595
2647
  return result;
2596
2648
  }, true);
2597
2649
  }
2598
2650
 
2651
+ // src/packages/database/model/utils/process-write-error.ts
2652
+ function driverMessage(message) {
2653
+ const index = message.lastIndexOf(" - ");
2654
+ return index >= 0 ? message.slice(index + 3) : message;
2655
+ }
2656
+ function processWriteError(err) {
2657
+ if (!(err instanceof Error)) {
2658
+ return err;
2659
+ }
2660
+ const { code } = err;
2661
+ const { message } = err;
2662
+ if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
2663
+ return new unique_constraint_error_default(driverMessage(message));
2664
+ }
2665
+ return err;
2666
+ }
2667
+ function rethrowWriteError(err) {
2668
+ throw processWriteError(err);
2669
+ }
2670
+
2599
2671
  // src/packages/database/model/utils/run-hooks.ts
2600
2672
  function runHooks(record, trx, ...hooks) {
2601
2673
  return hooks.filter((hook) => Boolean(hook)).reduce(
@@ -2635,6 +2707,12 @@ var Model = class {
2635
2707
  writable: false,
2636
2708
  enumerable: false,
2637
2709
  configurable: false
2710
+ },
2711
+ absentRelationships: {
2712
+ value: /* @__PURE__ */ new Set(),
2713
+ writable: false,
2714
+ enumerable: false,
2715
+ configurable: false
2638
2716
  }
2639
2717
  });
2640
2718
  const {
@@ -2981,9 +3059,9 @@ var Model = class {
2981
3059
  return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
2982
3060
  };
2983
3061
  if (transaction) {
2984
- return run(transaction);
3062
+ return run(transaction).catch(rethrowWriteError);
2985
3063
  }
2986
- return this.transaction(run);
3064
+ return this.transaction(run).catch(rethrowWriteError);
2987
3065
  }
2988
3066
  /**
2989
3067
  * Permanently delete the instance from the database.
@@ -3103,9 +3181,9 @@ var Model = class {
3103
3181
  return createTransactionResultProxy(instance, true);
3104
3182
  };
3105
3183
  if (transaction) {
3106
- return run(transaction);
3184
+ return run(transaction).catch(rethrowWriteError);
3107
3185
  }
3108
- return this.transaction(run);
3186
+ return this.transaction(run).catch(rethrowWriteError);
3109
3187
  }
3110
3188
  /**
3111
3189
  * Specify the transaction object to use for following save, update, or
@@ -3283,7 +3361,7 @@ var Model = class {
3283
3361
  return Promise.resolve(this);
3284
3362
  }
3285
3363
  if (!this.tableName) {
3286
- const getTableName = compose(import_inflection9.pluralize, underscore);
3364
+ const getTableName = compose(import_inflection10.pluralize, underscore);
3287
3365
  const tableName = getTableName(this.name);
3288
3366
  Reflect.defineProperty(this, "tableName", {
3289
3367
  value: tableName,
@@ -3481,7 +3559,7 @@ function parseNestedObject(source) {
3481
3559
  }
3482
3560
 
3483
3561
  // src/packages/server/request/parser/utils/format.ts
3484
- var import_inflection11 = require("inflection");
3562
+ var import_inflection12 = require("inflection");
3485
3563
 
3486
3564
  // src/packages/server/request/parser/constants.ts
3487
3565
  var INT = /^\d+$/;
@@ -3492,7 +3570,7 @@ var TRUE = /^true$/;
3492
3570
  var BRACKETS = /(?:\[])/g;
3493
3571
 
3494
3572
  // src/utils/transform-keys.ts
3495
- var import_inflection10 = require("inflection");
3573
+ var import_inflection11 = require("inflection");
3496
3574
  function transformKeys(source, transformer, deep = false) {
3497
3575
  if (Array.isArray(source)) {
3498
3576
  return source.slice(0);
@@ -3516,14 +3594,14 @@ function transformKeys(source, transformer, deep = false) {
3516
3594
  function camelizeKeys(source, deep) {
3517
3595
  return transformKeys(
3518
3596
  source,
3519
- (key) => (0, import_inflection10.camelize)(underscore(key), true),
3597
+ (key) => (0, import_inflection11.camelize)(underscore(key), true),
3520
3598
  deep
3521
3599
  );
3522
3600
  }
3523
3601
  function dasherizeKeys(source, deep) {
3524
3602
  return transformKeys(
3525
3603
  source,
3526
- (key) => (0, import_inflection10.dasherize)(underscore(key)),
3604
+ (key) => (0, import_inflection11.dasherize)(underscore(key)),
3527
3605
  deep
3528
3606
  );
3529
3607
  }
@@ -3538,7 +3616,7 @@ function makeArray(source) {
3538
3616
  function formatString(source, method) {
3539
3617
  if (method === "GET") {
3540
3618
  if (source.indexOf(",") >= 0) {
3541
- return source.split(",").map((str) => (0, import_inflection11.camelize)(underscore(str), true));
3619
+ return source.split(",").map((str) => (0, import_inflection12.camelize)(underscore(str), true));
3542
3620
  } else if (INT.test(source)) {
3543
3621
  return Number.parseInt(source, 10);
3544
3622
  } else if (BOOL.test(source)) {
@@ -3565,9 +3643,9 @@ function formatObject(source, method, formatter) {
3565
3643
  }
3566
3644
  function formatSort(sort) {
3567
3645
  if (sort.startsWith("-")) {
3568
- return `-${(0, import_inflection11.camelize)(underscore(sort.substr(1)), true)}`;
3646
+ return `-${(0, import_inflection12.camelize)(underscore(sort.substr(1)), true)}`;
3569
3647
  }
3570
- return (0, import_inflection11.camelize)(underscore(sort), true);
3648
+ return (0, import_inflection12.camelize)(underscore(sort), true);
3571
3649
  }
3572
3650
  function formatFields(fields) {
3573
3651
  return entries(fields).reduce(
@@ -3832,6 +3910,10 @@ function dataFor(status, err) {
3832
3910
  if (title) {
3833
3911
  errData.title = title;
3834
3912
  }
3913
+ const source = err?.source;
3914
+ if (source && (source.pointer || source.parameter)) {
3915
+ errData.source = source;
3916
+ }
3835
3917
  if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
3836
3918
  errData.detail = err.message.replace(/^\[public\]/gi, "");
3837
3919
  }
@@ -4054,9 +4136,10 @@ function merge(dest, source) {
4054
4136
  }
4055
4137
 
4056
4138
  // src/packages/controller/utils/params-to-query.ts
4057
- function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4139
+ function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
4058
4140
  const relationships = entries(model.relationships);
4059
4141
  const includedFields = omit(fields, model.resourceName);
4142
+ const included = include && include.map((path) => path.split(".")[0]);
4060
4143
  let query = {
4061
4144
  id,
4062
4145
  filter,
@@ -4099,9 +4182,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4099
4182
  if (!value.includes(relationship.model.primaryKey)) {
4100
4183
  value = [relationship.model.primaryKey, ...value];
4101
4184
  }
4102
- if (include && value.length === 1 && include.includes(name)) {
4103
- value = [...value, ...relationship.model.serializer.attributes];
4104
- } else if (!include && value.length > 1) {
4185
+ if (included && value.length === 1 && included.includes(name)) {
4186
+ value = [...value, ...serializerFor(relationship.model).attributes];
4187
+ } else if (!included && value.length > 1) {
4105
4188
  value = value.slice(0, 1);
4106
4189
  }
4107
4190
  return {
@@ -4116,18 +4199,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4116
4199
  }
4117
4200
 
4118
4201
  // src/packages/controller/utils/find-one.ts
4119
- function findOne(model, req) {
4202
+ function findOne(model, req, serializerFor) {
4120
4203
  const params = merge(req.defaultParams, req.params);
4121
- const { id, select, include } = paramsToQuery(model, params);
4204
+ const { id, select, include } = paramsToQuery(model, params, serializerFor);
4122
4205
  return model.find(id).select(...select).include(include);
4123
4206
  }
4124
4207
 
4125
4208
  // src/packages/controller/utils/find-many.ts
4126
- function findMany(model, req) {
4209
+ function findMany(model, req, serializerFor) {
4127
4210
  const params = merge(req.defaultParams, req.params);
4128
4211
  const { sort, page, limit, select, filter, include } = paramsToQuery(
4129
4212
  model,
4130
- params
4213
+ params,
4214
+ serializerFor
4131
4215
  );
4132
4216
  return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
4133
4217
  }
@@ -4157,6 +4241,53 @@ function resolveRelationships(model, relationships = {}) {
4157
4241
  );
4158
4242
  }
4159
4243
 
4244
+ // src/packages/controller/errors/related-record-not-found-error.ts
4245
+ var RelatedRecordNotFoundError = class extends Error {
4246
+ constructor({ name, primaryKey }, primaryKeyValue, path) {
4247
+ super(
4248
+ `Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
4249
+ );
4250
+ this.source = sourceFor(path);
4251
+ }
4252
+ };
4253
+ var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
4254
+
4255
+ // src/packages/controller/utils/validate-relationships.ts
4256
+ async function validateRelationships(model, relationships = {}) {
4257
+ await Promise.all(
4258
+ entries(relationships).map(async ([key, value]) => {
4259
+ const opts = model.relationshipFor(key);
4260
+ const { data = null } = value || {};
4261
+ if (!opts || !data) {
4262
+ return;
4263
+ }
4264
+ const { model: related } = opts;
4265
+ const isMany = Array.isArray(data);
4266
+ const ids = (isMany ? data : [data]).map(
4267
+ (item) => Reflect.get(Object(item), "id")
4268
+ );
4269
+ if (!ids.length) {
4270
+ return;
4271
+ }
4272
+ const column = related.columnNameFor(related.primaryKey) || "id";
4273
+ const rows = await related.table().whereIn(
4274
+ `${related.tableName}.${column}`,
4275
+ ids.filter((id) => id != null)
4276
+ ).select(`${related.tableName}.${column}`);
4277
+ const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
4278
+ const missing = ids.findIndex((id) => !found.has(String(id)));
4279
+ if (missing >= 0) {
4280
+ const path = `data.relationships.${key}.data`;
4281
+ throw new related_record_not_found_error_default(
4282
+ related,
4283
+ ids[missing],
4284
+ isMany ? `${path}.${missing}` : path
4285
+ );
4286
+ }
4287
+ })
4288
+ );
4289
+ }
4290
+
4160
4291
  // src/packages/controller/constants.ts
4161
4292
  var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
4162
4293
 
@@ -4342,6 +4473,74 @@ var Controller = class {
4342
4473
  * @public
4343
4474
  */
4344
4475
  defaultPerPage = 25;
4476
+ /**
4477
+ * How many relationships deep an `?include` path may go on this
4478
+ * controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
4479
+ * only direct relationships (`comments`) can be included. Paths deeper than
4480
+ * this are rejected with `400 Bad Request`.
4481
+ *
4482
+ * Set it on `ApplicationController` to change it for the whole app, or on a
4483
+ * single controller to override it there.
4484
+ *
4485
+ * ```javascript
4486
+ * class ApplicationController extends Controller {
4487
+ * maxIncludeDepth = 2;
4488
+ * }
4489
+ * ```
4490
+ *
4491
+ * Every allowed path is enumerated up front from the serializers'
4492
+ * relationships, and each nested level costs its own queries per request, so
4493
+ * keep this small.
4494
+ *
4495
+ * @property maxIncludeDepth
4496
+ * @type {Number}
4497
+ * @default 3
4498
+ * @public
4499
+ */
4500
+ maxIncludeDepth = 3;
4501
+ /**
4502
+ * Whether a namespace may fall back to the root Serializer of a type it has
4503
+ * no Serializer for. Read from a namespace's `ApplicationController` and
4504
+ * applies to the whole namespace.
4505
+ *
4506
+ * By default `app/controllers/admin/comments.js` without an
4507
+ * `app/serializers/admin/comments.js` — or an included type without one —
4508
+ * is serialized by the root Serializer, with every attribute and
4509
+ * relationship it declares. For a namespace that must only expose what it
4510
+ * declares itself, turn the fallback off:
4511
+ *
4512
+ * ```javascript
4513
+ * // app/controllers/admin/application.js
4514
+ * class AdminApplicationController extends ApplicationController {
4515
+ * serializerFallback = false;
4516
+ * }
4517
+ * ```
4518
+ *
4519
+ * The application then refuses to boot while any type the namespace can
4520
+ * serialize or `include` (down to each controller's `maxIncludeDepth`) has
4521
+ * no Serializer in that namespace, listing each missing one.
4522
+ *
4523
+ * @property serializerFallback
4524
+ * @type {Boolean}
4525
+ * @default true
4526
+ * @public
4527
+ */
4528
+ serializerFallback = true;
4529
+ /**
4530
+ * The Serializer to serialize (and validate, and load) related resources of
4531
+ * this Controller's responses with: the related model's Serializer in this
4532
+ * Controller's namespace, falling back to the root one.
4533
+ *
4534
+ * Always this Controller's namespace — not its Serializer's, which is the
4535
+ * root one when the namespace has no Serializer for this resource.
4536
+ *
4537
+ * @method serializerFor
4538
+ * @private
4539
+ */
4540
+ serializerFor(model) {
4541
+ const { serializer, namespace } = this;
4542
+ return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
4543
+ }
4345
4544
  constructor({ model, namespace, serializer }) {
4346
4545
  Object.assign(this, {
4347
4546
  model,
@@ -4367,7 +4566,7 @@ var Controller = class {
4367
4566
  * @public
4368
4567
  */
4369
4568
  index(req) {
4370
- return findMany(this.model, req);
4569
+ return findMany(this.model, req, (related) => this.serializerFor(related));
4371
4570
  }
4372
4571
  /**
4373
4572
  * This method supports including relationships, and sparse fieldsets via
@@ -4382,7 +4581,7 @@ var Controller = class {
4382
4581
  * @public
4383
4582
  */
4384
4583
  show(req) {
4385
- return findOne(this.model, req);
4584
+ return findOne(this.model, req, (related) => this.serializerFor(related));
4386
4585
  }
4387
4586
  /**
4388
4587
  * Create and return a single Model instance that the Controller instance
@@ -4403,6 +4602,7 @@ var Controller = class {
4403
4602
  data: { attributes, relationships }
4404
4603
  }
4405
4604
  } = req;
4605
+ await validateRelationships(model, relationships);
4406
4606
  const record = await model.create({
4407
4607
  ...attributes,
4408
4608
  ...resolveRelationships(model, relationships)
@@ -4428,12 +4628,13 @@ var Controller = class {
4428
4628
  */
4429
4629
  update(req) {
4430
4630
  const { model } = this;
4431
- return findOne(model, req).then((record) => {
4631
+ return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
4432
4632
  const {
4433
4633
  params: {
4434
4634
  data: { attributes, relationships }
4435
4635
  }
4436
4636
  } = req;
4637
+ await validateRelationships(model, relationships);
4437
4638
  return record.update({
4438
4639
  ...attributes,
4439
4640
  ...resolveRelationships(model, relationships)
@@ -4457,7 +4658,7 @@ var Controller = class {
4457
4658
  * @public
4458
4659
  */
4459
4660
  destroy(req) {
4460
- return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
4661
+ return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
4461
4662
  }
4462
4663
  /**
4463
4664
  * Respond to HEAD or OPTIONS requests.
@@ -4475,7 +4676,194 @@ var Controller = class {
4475
4676
  var controller_default = Controller;
4476
4677
 
4477
4678
  // src/packages/serializer/index.ts
4478
- var import_inflection12 = require("inflection");
4679
+ var import_path3 = require("path");
4680
+ var import_inflection14 = require("inflection");
4681
+
4682
+ // src/packages/loader/resolver/utils/closest-ancestor.ts
4683
+ var import_path2 = require("path");
4684
+ function closestAncestor(source, key) {
4685
+ const name = import_path2.posix.basename(key);
4686
+ let namespace = import_path2.posix.dirname(key);
4687
+ if (namespace === ".") {
4688
+ return source.get(name);
4689
+ }
4690
+ namespace = import_path2.posix.dirname(namespace);
4691
+ const ancestor = source.get(import_path2.posix.join(namespace, name));
4692
+ if (ancestor) {
4693
+ return ancestor;
4694
+ }
4695
+ return closestAncestor(source, import_path2.posix.join(import_path2.posix.dirname(namespace), name));
4696
+ }
4697
+
4698
+ // src/packages/serializer/utils/load-linkage.ts
4699
+ var import_inflection13 = require("inflection");
4700
+ var valueOf = (record, key) => Reflect.get(record, key);
4701
+ var toId = (value) => value == null ? null : String(value);
4702
+ async function loadLinkage(model, records, names) {
4703
+ const ids = Array.from(
4704
+ new Set(records.map((record) => record.getPrimaryKey()))
4705
+ );
4706
+ const linkage = /* @__PURE__ */ new Map();
4707
+ const belongsTo = [];
4708
+ const queries = [];
4709
+ ids.forEach((id) => {
4710
+ linkage.set(
4711
+ String(id),
4712
+ names.reduce((result, name) => {
4713
+ const opts = model.relationshipFor(name);
4714
+ result[name] = opts && opts.type === "hasMany" ? [] : null;
4715
+ return result;
4716
+ }, {})
4717
+ );
4718
+ });
4719
+ if (!ids.length) {
4720
+ return linkage;
4721
+ }
4722
+ const link = (ownerId, name, relatedId) => {
4723
+ const owner = linkage.get(String(ownerId));
4724
+ const id = toId(relatedId);
4725
+ if (!owner || id === null) {
4726
+ return;
4727
+ }
4728
+ const current = owner[name];
4729
+ if (Array.isArray(current)) {
4730
+ if (!current.includes(id)) {
4731
+ current.push(id);
4732
+ }
4733
+ } else if (current === null) {
4734
+ owner[name] = id;
4735
+ }
4736
+ };
4737
+ names.forEach((name) => {
4738
+ const opts = model.relationshipFor(name);
4739
+ if (!opts) {
4740
+ return;
4741
+ }
4742
+ const { type, through, model: related } = opts;
4743
+ const foreignKey = (0, import_inflection13.camelize)(opts.foreignKey, true);
4744
+ if (type === "belongsTo") {
4745
+ belongsTo.push([name, foreignKey, related]);
4746
+ } else if (through) {
4747
+ const inverse = related.relationshipFor(opts.inverse);
4748
+ if (!inverse) {
4749
+ return;
4750
+ }
4751
+ const relatedKey = (0, import_inflection13.camelize)(inverse.foreignKey, true);
4752
+ queries.push(
4753
+ (async () => {
4754
+ const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
4755
+ rows.forEach((row) => {
4756
+ link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
4757
+ });
4758
+ })()
4759
+ );
4760
+ } else {
4761
+ queries.push(
4762
+ (async () => {
4763
+ const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
4764
+ rows.forEach((row) => {
4765
+ link(valueOf(row, foreignKey), name, row.getPrimaryKey());
4766
+ });
4767
+ })()
4768
+ );
4769
+ }
4770
+ });
4771
+ if (belongsTo.length) {
4772
+ queries.push(
4773
+ (async () => {
4774
+ const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
4775
+ rows.forEach((row) => {
4776
+ belongsTo.forEach(([name, key]) => {
4777
+ link(row.getPrimaryKey(), name, valueOf(row, key));
4778
+ });
4779
+ });
4780
+ await dropDangling(linkage, belongsTo);
4781
+ })()
4782
+ );
4783
+ }
4784
+ await Promise.all(queries);
4785
+ return linkage;
4786
+ }
4787
+ async function dropDangling(linkage, belongsTo) {
4788
+ const wanted = /* @__PURE__ */ new Map();
4789
+ belongsTo.forEach(([name, , related]) => {
4790
+ const ids = wanted.get(related) || /* @__PURE__ */ new Set();
4791
+ linkage.forEach(({ [name]: id }) => {
4792
+ if (typeof id === "string") {
4793
+ ids.add(id);
4794
+ }
4795
+ });
4796
+ wanted.set(related, ids);
4797
+ });
4798
+ const existing = /* @__PURE__ */ new Map();
4799
+ await Promise.all(
4800
+ Array.from(wanted, async ([related, ids]) => {
4801
+ if (!ids.size) {
4802
+ existing.set(related, ids);
4803
+ return;
4804
+ }
4805
+ const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
4806
+ existing.set(
4807
+ related,
4808
+ new Set(rows.map((row) => String(row.getPrimaryKey())))
4809
+ );
4810
+ })
4811
+ );
4812
+ belongsTo.forEach(([name, , related]) => {
4813
+ const found = existing.get(related);
4814
+ linkage.forEach((owner) => {
4815
+ const id = owner[name];
4816
+ if (typeof id === "string" && !found?.has(id)) {
4817
+ owner[name] = null;
4818
+ }
4819
+ });
4820
+ });
4821
+ }
4822
+
4823
+ // src/packages/serializer/utils/include-tree.ts
4824
+ function createIncludeTree(paths = []) {
4825
+ const tree = /* @__PURE__ */ new Map();
4826
+ paths.forEach((path) => {
4827
+ path.split(".").filter(Boolean).reduce((node, name) => {
4828
+ let child = node.get(name);
4829
+ if (!child) {
4830
+ child = /* @__PURE__ */ new Map();
4831
+ node.set(name, child);
4832
+ }
4833
+ return child;
4834
+ }, tree);
4835
+ });
4836
+ return tree;
4837
+ }
4838
+ function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
4839
+ if (depth < 1) {
4840
+ return [];
4841
+ }
4842
+ return names.reduce((paths, name) => {
4843
+ const opts = model.relationshipFor(name);
4844
+ if (!opts) {
4845
+ return paths;
4846
+ }
4847
+ const { model: related } = opts;
4848
+ const serializer = serializerFor(related);
4849
+ if (!serializer) {
4850
+ return [...paths, name];
4851
+ }
4852
+ const { hasOne, hasMany } = serializer;
4853
+ return [
4854
+ ...paths,
4855
+ name,
4856
+ ...enumerateIncludePaths(
4857
+ related,
4858
+ [...hasOne, ...hasMany],
4859
+ depth - 1,
4860
+ serializerFor
4861
+ ).map((path) => `${name}.${path}`)
4862
+ ];
4863
+ }, []);
4864
+ }
4865
+
4866
+ // src/packages/serializer/index.ts
4479
4867
  var Serializer = class {
4480
4868
  /**
4481
4869
  * An Array of the `hasOne` or `belongsTo` relationships on a Serializer
@@ -4562,9 +4950,17 @@ var Serializer = class {
4562
4950
  * the resource and relationship objects in the returned [JSON API](
4563
4951
  * http://jsonapi.org) document object.
4564
4952
  *
4565
- * @param {Array} options.include - An array of strings containing the
4566
- * relationship keys that should be added to the top level included object of
4567
- * the returned [JSON API](http://jsonapi.org) document object.
4953
+ * @param {Array} options.include - An array of relationship paths (e.g.
4954
+ * `'comments'` or `'comments.user'`) whose resources should be added to the
4955
+ * top level included object of the returned [JSON API](http://jsonapi.org)
4956
+ * document object. Intermediate resources of a nested path are included too.
4957
+ *
4958
+ * @param {String} options.namespace - The namespace of the request, i.e. of
4959
+ * the Controller handling it. Every link in the document is built in it, and
4960
+ * included resources are serialized by their Serializer in it (falling back
4961
+ * to the root). Defaults to this Serializer's namespace — which is the root
4962
+ * one when a namespaced Controller has no Serializer of its own, so the
4963
+ * Controller passes its namespace explicitly.
4568
4964
  *
4569
4965
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
4570
4966
  * object.
@@ -4575,38 +4971,61 @@ var Serializer = class {
4575
4971
  data,
4576
4972
  links,
4577
4973
  domain,
4578
- include
4974
+ include,
4975
+ namespace = this.namespace
4579
4976
  }) {
4580
- let serialized = {};
4581
- const included = [];
4977
+ const tree = createIncludeTree(include);
4978
+ const related = /* @__PURE__ */ new Map();
4979
+ const included = /* @__PURE__ */ new Map();
4980
+ const names = Array.from(tree.keys());
4981
+ let primary;
4982
+ let serialized;
4582
4983
  if (Array.isArray(data)) {
4583
- serialized = {
4584
- data: await Promise.all(
4585
- data.map(
4586
- (item) => this.formatOne({
4587
- item,
4588
- domain,
4589
- include,
4590
- included
4591
- })
4592
- )
4984
+ primary = await Promise.all(
4985
+ data.map(
4986
+ (item) => this.formatOne({
4987
+ item,
4988
+ domain,
4989
+ related,
4990
+ namespace,
4991
+ include: names
4992
+ })
4593
4993
  )
4594
- };
4994
+ );
4995
+ serialized = { data: primary };
4595
4996
  } else {
4596
- serialized = {
4597
- data: await this.formatOne({
4997
+ const resource2 = await this.formatOne({
4998
+ domain,
4999
+ related,
5000
+ namespace,
5001
+ item: data,
5002
+ include: names,
5003
+ links: false
5004
+ });
5005
+ primary = [resource2];
5006
+ serialized = { data: resource2 };
5007
+ }
5008
+ for (const [name, children] of tree) {
5009
+ const opts = this.model.relationshipFor(name);
5010
+ const records = related.get(name);
5011
+ if (opts && records) {
5012
+ await this.addIncluded({
4598
5013
  domain,
4599
- include,
5014
+ records,
4600
5015
  included,
4601
- item: data,
4602
- links: false
4603
- })
4604
- };
5016
+ namespace,
5017
+ model: opts.model,
5018
+ tree: children
5019
+ });
5020
+ }
4605
5021
  }
4606
- if (included.length) {
5022
+ primary.forEach((resource2) => {
5023
+ included.delete(resourceKey(resource2));
5024
+ });
5025
+ if (included.size) {
4607
5026
  serialized = {
4608
5027
  ...serialized,
4609
- included: uniq(included, "id", "type")
5028
+ included: Array.from(included.values())
4610
5029
  };
4611
5030
  }
4612
5031
  return {
@@ -4621,6 +5040,12 @@ var Serializer = class {
4621
5040
  * Transform a single Model instance into a [JSON API](http://jsonapi.org)
4622
5041
  * resource object.
4623
5042
  *
5043
+ * Relationships are serialized in one of two ways. By default each one is
5044
+ * read from the Model instance (for primary data the query has already
5045
+ * loaded them). When `linkage` is given — as it is for included resources,
5046
+ * whose relationships are batch-loaded by `addIncluded()` — the resource
5047
+ * linkage is built from it instead, without touching the database.
5048
+ *
4624
5049
  * @method formatOne
4625
5050
  *
4626
5051
  * @param {Object} options - An options object used for building the returned
@@ -4637,17 +5062,19 @@ var Serializer = class {
4637
5062
  * the top level links object or relationship links objects in the returned
4638
5063
  * [JSON API](http://jsonapi.org) resource object.
4639
5064
  *
4640
- * @param {Array} options.include - An array of strings containing the
4641
- * relationship keys that should be added to the top level included object of
4642
- * a [JSON API](http://jsonapi.org) document object.
5065
+ * @param {Array} options.include - An array of the relationship keys whose
5066
+ * related records should be collected into `options.related`.
4643
5067
  *
4644
- * @param {Array} options.included - An array of [JSON API](
4645
- * http://jsonapi.org) resource objects that will be added to the top level
4646
- * included array of a [JSON API](http://jsonapi.org) document object.
5068
+ * @param {Map} options.related - Collects, per relationship key in
5069
+ * `options.include`, the related Model instances that belong in the top
5070
+ * level included object of a [JSON API](http://jsonapi.org) document object.
4647
5071
  *
4648
- * @param {Boolean} options.formatRelationships - Wether or not
4649
- * relationships should be formatted and included in the returned
4650
- * [JSON API](http://jsonapi.org) resource object.
5072
+ * @param {Object} options.linkage - Pre-loaded resource linkage (related
5073
+ * primary keys per relationship key) to serialize relationships from.
5074
+ *
5075
+ * @param {String} options.namespace - The namespace to build links in.
5076
+ * Defaults to this Serializer's; included resources pass the namespace of
5077
+ * the request, so every link in a document points into the same namespace.
4651
5078
  *
4652
5079
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
4653
5080
  * object.
@@ -4658,13 +5085,15 @@ var Serializer = class {
4658
5085
  item,
4659
5086
  links,
4660
5087
  domain,
4661
- include,
4662
- included,
4663
- formatRelationships = true
5088
+ include = [],
5089
+ related,
5090
+ linkage,
5091
+ namespace = this.namespace
4664
5092
  }) {
4665
5093
  const { resourceName: type } = item;
4666
5094
  const id = String(item.getPrimaryKey());
4667
- let relationships = {};
5095
+ const names = [...this.hasOne, ...this.hasMany];
5096
+ let relationships;
4668
5097
  const attributes = dasherizeKeys(
4669
5098
  item.getAttributes(
4670
5099
  ...Object.keys(item.rawColumnData).filter(
@@ -4677,34 +5106,41 @@ var Serializer = class {
4677
5106
  type,
4678
5107
  attributes
4679
5108
  };
4680
- if (formatRelationships) {
5109
+ if (linkage) {
5110
+ relationships = names.reduce(
5111
+ (hash, name) => ({
5112
+ ...hash,
5113
+ [(0, import_inflection14.dasherize)(underscore(name))]: this.formatLinkage(
5114
+ domain,
5115
+ this.model.relationshipFor(name)?.model.resourceName,
5116
+ linkage[name],
5117
+ namespace
5118
+ )
5119
+ }),
5120
+ {}
5121
+ );
5122
+ } else {
5123
+ const collect = (name, records) => {
5124
+ if (related && include.includes(name)) {
5125
+ related.set(name, [...related.get(name) || [], ...records]);
5126
+ }
5127
+ };
4681
5128
  relationships = await promiseHash(
4682
- [...this.hasOne, ...this.hasMany].reduce(
5129
+ names.reduce(
4683
5130
  (hash, name) => ({
4684
5131
  ...hash,
4685
- [(0, import_inflection12.dasherize)(underscore(name))]: (async () => {
4686
- const related = await Reflect.get(item, name);
4687
- if (Array.isArray(related)) {
5132
+ [(0, import_inflection14.dasherize)(underscore(name))]: (async () => {
5133
+ const value = await Reflect.get(item, name);
5134
+ if (Array.isArray(value)) {
5135
+ collect(name, value);
4688
5136
  return {
4689
- data: await Promise.all(
4690
- related.map(async (relatedItem) => {
4691
- const { data: relatedData } = await this.formatRelationship({
4692
- domain,
4693
- included,
4694
- item: relatedItem,
4695
- include: include.includes(name)
4696
- });
4697
- return relatedData;
4698
- })
5137
+ data: value.map(
5138
+ (relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
4699
5139
  )
4700
5140
  };
4701
- } else if (related && related.id != null) {
4702
- return this.formatRelationship({
4703
- domain,
4704
- included,
4705
- item: related,
4706
- include: include.includes(name)
4707
- });
5141
+ } else if (value && value.id != null) {
5142
+ collect(name, [value]);
5143
+ return this.formatRelationship(value, domain, namespace);
4708
5144
  }
4709
5145
  return {
4710
5146
  data: null
@@ -4719,16 +5155,9 @@ var Serializer = class {
4719
5155
  serialized.relationships = relationships;
4720
5156
  }
4721
5157
  if (links || typeof links !== "boolean") {
4722
- const { namespace } = this;
4723
- if (namespace) {
4724
- serialized.links = {
4725
- self: `${domain}/${namespace}/${type}/${id}`
4726
- };
4727
- } else {
4728
- serialized.links = {
4729
- self: `${domain}/${type}/${id}`
4730
- };
4731
- }
5158
+ serialized.links = {
5159
+ self: this.linkFor(domain, type, id, namespace)
5160
+ };
4732
5161
  }
4733
5162
  return serialized;
4734
5163
  }
@@ -4738,70 +5167,167 @@ var Serializer = class {
4738
5167
  *
4739
5168
  * @method formatRelationship
4740
5169
  *
4741
- * @param {Object} options - An options object used for building the returned
5170
+ * @param {Model} item - The Model instance to transform into the returned
4742
5171
  * [JSON API](http://jsonapi.org) relationship object.
4743
5172
  *
4744
- * @param {Model} options.item - The Model instance to transform into the
5173
+ * @param {String} domain - A string used to build links included in the
4745
5174
  * returned [JSON API](http://jsonapi.org) relationship object.
4746
5175
  *
4747
- * @param {String} options.domain - A string used to build links included in
4748
- * the returned [JSON API](http://jsonapi.org) relationship object.
4749
- *
4750
- * @param {Array} options.include - An array of strings containing the
4751
- * relationship keys that should be added to the top level included object of
4752
- * a [JSON API](http://jsonapi.org) document object.
4753
- *
4754
- * @param {Array} options.included - An array of [JSON API](
4755
- * http://jsonapi.org) resource objects that will be added to the top level
4756
- * included array of a [JSON API](http://jsonapi.org) document object.
5176
+ * @return {Object} A [JSON API](http://jsonapi.org) relationship object.
4757
5177
  *
4758
- * @return {Promise} Resolves with a [JSON API](http://jsonapi.org)
4759
- * relationship object.
5178
+ * @private
5179
+ */
5180
+ formatRelationship(item, domain, namespace = this.namespace) {
5181
+ return this.formatLinkage(
5182
+ domain,
5183
+ item.resourceName,
5184
+ String(item.getPrimaryKey()),
5185
+ namespace
5186
+ );
5187
+ }
5188
+ /**
5189
+ * Build a [JSON API](http://jsonapi.org) relationship object from resource
5190
+ * linkage, in the same shape `formatRelationship()` produces from Model
5191
+ * instances: to-one relationships carry a `links` object, to-many ones only
5192
+ * `data`, and a missing to-one relationship is `{ data: null }`.
4760
5193
  *
5194
+ * @method formatLinkage
4761
5195
  * @private
4762
5196
  */
4763
- async formatRelationship({
4764
- item,
4765
- domain,
4766
- include,
4767
- included
4768
- }) {
4769
- const { namespace } = this;
4770
- const {
4771
- resourceName: type,
4772
- constructor: { serializer }
4773
- } = item;
4774
- const id = String(item.getPrimaryKey());
4775
- let links;
4776
- if (namespace) {
4777
- links = {
4778
- self: `${domain}/${namespace}/${type}/${id}`
4779
- };
4780
- } else {
4781
- links = {
4782
- self: `${domain}/${type}/${id}`
5197
+ formatLinkage(domain, type, linkage, namespace = this.namespace) {
5198
+ if (Array.isArray(linkage)) {
5199
+ return {
5200
+ data: type ? linkage.map((id) => ({ id, type })) : []
4783
5201
  };
4784
5202
  }
4785
- if (include) {
4786
- included.push(
4787
- await serializer.formatOne({
4788
- item,
4789
- domain,
4790
- include: [],
4791
- included: [],
4792
- formatRelationships: false
4793
- })
4794
- );
5203
+ if (linkage == null || !type) {
5204
+ return {
5205
+ data: null
5206
+ };
4795
5207
  }
4796
5208
  return {
4797
5209
  data: {
4798
- id,
5210
+ id: linkage,
4799
5211
  type
4800
5212
  },
4801
- links
5213
+ links: {
5214
+ self: this.linkFor(domain, type, linkage, namespace)
5215
+ }
4802
5216
  };
4803
5217
  }
5218
+ /**
5219
+ * Add `records` (instances of `model`) to `included` as resource objects,
5220
+ * then recurse into the relationships named in `tree`. Each is serialized by
5221
+ * `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
5222
+ * `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
5223
+ * is one and `CommentsSerializer` otherwise. The relationships of every level
5224
+ * are batch-loaded with one query per relationship, not one per record.
5225
+ *
5226
+ * @method addIncluded
5227
+ * @private
5228
+ */
5229
+ async addIncluded({
5230
+ model,
5231
+ records,
5232
+ tree,
5233
+ domain,
5234
+ included,
5235
+ namespace
5236
+ }) {
5237
+ const serializer = this.serializerFor(model, namespace);
5238
+ const unique = Array.from(
5239
+ new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
5240
+ );
5241
+ if (!unique.length) {
5242
+ return;
5243
+ }
5244
+ const names = [...serializer.hasOne, ...serializer.hasMany];
5245
+ const linkage = await loadLinkage(model, unique, names);
5246
+ for (const item of unique) {
5247
+ const id = String(item.getPrimaryKey());
5248
+ const key = resourceKey({ id, type: item.resourceName });
5249
+ if (!included.has(key)) {
5250
+ included.set(
5251
+ key,
5252
+ await serializer.formatOne({
5253
+ item,
5254
+ domain,
5255
+ linkage: linkage.get(id),
5256
+ namespace
5257
+ })
5258
+ );
5259
+ }
5260
+ }
5261
+ for (const [name, children] of tree) {
5262
+ const opts = model.relationshipFor(name);
5263
+ if (!opts || !names.includes(name)) {
5264
+ continue;
5265
+ }
5266
+ const { model: next } = opts;
5267
+ const ids = /* @__PURE__ */ new Set();
5268
+ linkage.forEach(({ [name]: value }) => {
5269
+ (Array.isArray(value) ? value : [value]).forEach((relatedId) => {
5270
+ if (relatedId != null) {
5271
+ ids.add(relatedId);
5272
+ }
5273
+ });
5274
+ });
5275
+ if (ids.size) {
5276
+ const attributes = this.serializerFor(
5277
+ next,
5278
+ namespace
5279
+ ).attributes.filter((attr) => next.attributeNames.includes(attr));
5280
+ const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
5281
+ await this.addIncluded({
5282
+ domain,
5283
+ included,
5284
+ namespace,
5285
+ model: next,
5286
+ records: nextRecords,
5287
+ tree: children
5288
+ });
5289
+ }
5290
+ }
5291
+ }
5292
+ /**
5293
+ * Resolve the Serializer for `model` in `namespace` (this Serializer's by
5294
+ * default), the way a namespaced Controller resolves its own:
5295
+ * `admin/comments` if it exists, otherwise the closest ancestor namespace's,
5296
+ * down to the root `comments` Serializer. Falls back to `model.serializer`
5297
+ * when this Serializer was not created by an application (e.g. in
5298
+ * isolation).
5299
+ *
5300
+ * Pass the request's namespace when there is one: a Serializer's own
5301
+ * namespace is the root one whenever it is a namespaced Controller's
5302
+ * fallback.
5303
+ *
5304
+ * @method serializerFor
5305
+ * @private
5306
+ */
5307
+ serializerFor(model, namespace = this.namespace) {
5308
+ const { serializers } = this;
5309
+ if (serializers) {
5310
+ const key = import_path3.posix.join(namespace || ".", model.resourceName);
5311
+ const serializer = serializers.get(key) || closestAncestor(serializers, key);
5312
+ if (serializer) {
5313
+ return serializer;
5314
+ }
5315
+ }
5316
+ return model.serializer;
5317
+ }
5318
+ /**
5319
+ * @private
5320
+ */
5321
+ linkFor(domain, type, id, namespace = this.namespace) {
5322
+ if (namespace) {
5323
+ return `${domain}/${namespace}/${type}/${id}`;
5324
+ }
5325
+ return `${domain}/${type}/${id}`;
5326
+ }
4804
5327
  };
5328
+ function resourceKey({ id, type }) {
5329
+ return `${type}:${id}`;
5330
+ }
4805
5331
  var serializer_default = Serializer;
4806
5332
 
4807
5333
  // src/packages/config/index.ts
@@ -5000,7 +5526,10 @@ function resource(action) {
5000
5526
  data,
5001
5527
  links,
5002
5528
  domain,
5003
- include
5529
+ include,
5530
+ // The request's namespace, not the serializer's: a namespaced
5531
+ // controller without its own serializer is given the root one.
5532
+ namespace
5004
5533
  });
5005
5534
  }
5006
5535
  return data;
@@ -5081,6 +5610,7 @@ var ParameterTypeError = class extends TypeError {
5081
5610
  Expected type '${type || "undefined"}' for parameter '${path}' but got
5082
5611
  '${actual}'.
5083
5612
  `);
5613
+ this.source = sourceFor(path);
5084
5614
  }
5085
5615
  };
5086
5616
  var parameter_type_error_default = createServerError(ParameterTypeError, 400);
@@ -5093,6 +5623,7 @@ var ParameterValueError = class extends TypeError {
5093
5623
  [${param.size ? Array.from(param.values()).join(", ") : ""}] but got
5094
5624
  ${String(actual)}.
5095
5625
  `);
5626
+ this.source = sourceFor(param.path);
5096
5627
  }
5097
5628
  };
5098
5629
  var parameter_value_error_default = createServerError(ParameterValueError, 400);
@@ -5101,6 +5632,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
5101
5632
  var InvalidParameterError = class extends TypeError {
5102
5633
  constructor(path) {
5103
5634
  super(`'${path}' is not a valid parameter for this resource.`);
5635
+ this.source = sourceFor(path);
5104
5636
  }
5105
5637
  };
5106
5638
  var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
@@ -5116,6 +5648,7 @@ var ResourceMismatchError = class extends TypeError {
5116
5648
  Expected '${String(expected)}' for parameter '${path}' but got
5117
5649
  ${String(normalized)}.
5118
5650
  `);
5651
+ this.source = sourceFor(path);
5119
5652
  }
5120
5653
  };
5121
5654
  var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
@@ -5124,14 +5657,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
5124
5657
  var ParameterRequiredError = class extends TypeError {
5125
5658
  constructor(path) {
5126
5659
  super(`Missing required parameter '${path}'.`);
5660
+ this.source = sourceFor(path);
5127
5661
  }
5128
5662
  };
5129
5663
  var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
5130
5664
 
5665
+ // src/packages/router/route/params/errors/client-generated-id-error.ts
5666
+ var ClientGeneratedIdError = class extends TypeError {
5667
+ constructor() {
5668
+ super("Client-generated IDs are not supported for this resource.");
5669
+ this.source = sourceFor("data.id");
5670
+ }
5671
+ };
5672
+ var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
5673
+
5674
+ // src/packages/router/route/params/errors/forbidden-parameter-error.ts
5675
+ var ForbiddenParameterError = class extends TypeError {
5676
+ constructor(path) {
5677
+ super(`Setting '${path}' is not supported for this resource.`);
5678
+ this.source = sourceFor(path);
5679
+ }
5680
+ };
5681
+ var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
5682
+
5131
5683
  // src/packages/router/route/params/errors/parameter-not-nullable-error.ts
5132
5684
  var ParameterNotNullableError = class extends TypeError {
5133
5685
  constructor({ path }) {
5134
5686
  super(`Parameter '${path}' is not nullable.`);
5687
+ this.source = sourceFor(path);
5135
5688
  }
5136
5689
  };
5137
5690
  var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
@@ -5260,13 +5813,14 @@ var Parameter = class extends set_default {
5260
5813
  path,
5261
5814
  type,
5262
5815
  required: Boolean(required),
5263
- sanitize: Boolean(sanitize)
5816
+ sanitize: Boolean(sanitize),
5817
+ restricted: values !== void 0
5264
5818
  });
5265
5819
  this.freeze();
5266
5820
  }
5267
5821
  validate(value) {
5268
5822
  validateType2(this, value);
5269
- if (this.size > 0) {
5823
+ if (this.restricted) {
5270
5824
  return validateValue(this, value);
5271
5825
  }
5272
5826
  return value;
@@ -5285,6 +5839,17 @@ function getURLParams(dynamicSegments) {
5285
5839
  ]);
5286
5840
  }
5287
5841
 
5842
+ // src/packages/router/route/params/parameter/forbidden-parameter.ts
5843
+ var ForbiddenParameter = class extends parameter_default {
5844
+ constructor(path) {
5845
+ super({ path });
5846
+ }
5847
+ validate() {
5848
+ throw new forbidden_parameter_error_default(this.path);
5849
+ }
5850
+ };
5851
+ var forbidden_parameter_default = ForbiddenParameter;
5852
+
5288
5853
  // src/packages/router/route/params/utils/get-data-params.ts
5289
5854
  function getIDParam({ model }) {
5290
5855
  const primaryKeyColumn = model.columnFor(model.primaryKey);
@@ -5337,16 +5902,48 @@ function getRelationshipsParam({
5337
5902
  model,
5338
5903
  params
5339
5904
  }) {
5905
+ const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
5906
+ key,
5907
+ new forbidden_parameter_default(`data.relationships.${key}`)
5908
+ ]);
5340
5909
  return [
5341
5910
  "relationships",
5342
5911
  new parameter_group_default(
5343
- params.reduce((group, param) => {
5344
- const path = `data.relationships.${param}`;
5345
- const opts = model.relationshipFor(param);
5346
- if (!opts) {
5347
- return group;
5348
- }
5349
- if (opts.type === "hasMany") {
5912
+ [
5913
+ ...params.reduce((group, param) => {
5914
+ const path = `data.relationships.${param}`;
5915
+ const opts = model.relationshipFor(param);
5916
+ if (!opts) {
5917
+ return group;
5918
+ }
5919
+ if (opts.type === "hasMany") {
5920
+ return [
5921
+ ...group,
5922
+ [
5923
+ param,
5924
+ new parameter_group_default(
5925
+ [
5926
+ [
5927
+ "data",
5928
+ new parameter_default({
5929
+ type: "array",
5930
+ path: `${path}.data`,
5931
+ required: true
5932
+ })
5933
+ ]
5934
+ ],
5935
+ {
5936
+ path
5937
+ }
5938
+ )
5939
+ ]
5940
+ ];
5941
+ }
5942
+ const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5943
+ let primaryKeyType = "number";
5944
+ if (primaryKeyColumn) {
5945
+ primaryKeyType = typeForColumn(primaryKeyColumn);
5946
+ }
5350
5947
  return [
5351
5948
  ...group,
5352
5949
  [
@@ -5355,11 +5952,32 @@ function getRelationshipsParam({
5355
5952
  [
5356
5953
  [
5357
5954
  "data",
5358
- new parameter_default({
5359
- type: "array",
5360
- path: `${path}.data`,
5361
- required: true
5362
- })
5955
+ new parameter_group_default(
5956
+ [
5957
+ [
5958
+ "id",
5959
+ new parameter_default({
5960
+ type: primaryKeyType,
5961
+ path: `${path}.data.id`,
5962
+ required: true
5963
+ })
5964
+ ],
5965
+ [
5966
+ "type",
5967
+ new parameter_default({
5968
+ type: "string",
5969
+ path: `${path}.data.type`,
5970
+ values: [opts.model.resourceName],
5971
+ required: true
5972
+ })
5973
+ ]
5974
+ ],
5975
+ {
5976
+ type: "array",
5977
+ path: `${path}.data`,
5978
+ required: true
5979
+ }
5980
+ )
5363
5981
  ]
5364
5982
  ],
5365
5983
  {
@@ -5368,55 +5986,9 @@ function getRelationshipsParam({
5368
5986
  )
5369
5987
  ]
5370
5988
  ];
5371
- }
5372
- const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5373
- let primaryKeyType = "number";
5374
- if (primaryKeyColumn) {
5375
- primaryKeyType = typeForColumn(primaryKeyColumn);
5376
- }
5377
- return [
5378
- ...group,
5379
- [
5380
- param,
5381
- new parameter_group_default(
5382
- [
5383
- [
5384
- "data",
5385
- new parameter_group_default(
5386
- [
5387
- [
5388
- "id",
5389
- new parameter_default({
5390
- type: primaryKeyType,
5391
- path: `${path}.data.id`,
5392
- required: true
5393
- })
5394
- ],
5395
- [
5396
- "type",
5397
- new parameter_default({
5398
- type: "string",
5399
- path: `${path}.data.type`,
5400
- values: [opts.model.resourceName],
5401
- required: true
5402
- })
5403
- ]
5404
- ],
5405
- {
5406
- type: "array",
5407
- path: `${path}.data`,
5408
- required: true
5409
- }
5410
- )
5411
- ]
5412
- ],
5413
- {
5414
- path
5415
- }
5416
- )
5417
- ]
5418
- ];
5419
- }, []),
5989
+ }, []),
5990
+ ...forbidden
5991
+ ],
5420
5992
  {
5421
5993
  path: "data.relationships"
5422
5994
  }
@@ -5524,10 +6096,9 @@ function getFilterParam({ filter }) {
5524
6096
  )
5525
6097
  ];
5526
6098
  }
5527
- function getFieldsParam({
5528
- model,
5529
- serializer: { hasOne, hasMany, attributes }
5530
- }) {
6099
+ function getFieldsParam(controller) {
6100
+ const { model, serializer } = controller;
6101
+ const { hasOne, hasMany, attributes } = serializer;
5531
6102
  const relationships = [...hasOne, ...hasMany];
5532
6103
  return [
5533
6104
  "fields",
@@ -5556,7 +6127,7 @@ function getFieldsParam({
5556
6127
  sanitize: true,
5557
6128
  values: [
5558
6129
  opts.model.primaryKey,
5559
- ...opts.model.serializer.attributes
6130
+ ...controller.serializerFor(opts.model).attributes
5560
6131
  ]
5561
6132
  })
5562
6133
  ]
@@ -5574,16 +6145,30 @@ function getFieldsParam({
5574
6145
  )
5575
6146
  ];
5576
6147
  }
5577
- function getIncludeParam({
5578
- serializer: { hasOne, hasMany }
5579
- }) {
6148
+ function getIncludeParam(controller) {
6149
+ const { model, maxIncludeDepth, serializer } = controller;
6150
+ const { hasOne, hasMany } = serializer;
5580
6151
  const relationships = [...hasOne, ...hasMany];
5581
6152
  return [
5582
6153
  "include",
5583
6154
  new parameter_default({
5584
6155
  path: "include",
5585
6156
  type: "array",
5586
- values: relationships
6157
+ // Every top level name stays allowed (as before), plus the nested paths
6158
+ // (`comments.user`) reachable through each related serializer in the
6159
+ // controller's namespace (at every level, even below a root fallback
6160
+ // serializer), down to the controller's `maxIncludeDepth`.
6161
+ values: Array.from(
6162
+ /* @__PURE__ */ new Set([
6163
+ ...relationships,
6164
+ ...enumerateIncludePaths(
6165
+ model,
6166
+ relationships,
6167
+ maxIncludeDepth,
6168
+ (related) => controller.serializerFor(related)
6169
+ )
6170
+ ])
6171
+ )
5587
6172
  })
5588
6173
  ];
5589
6174
  }
@@ -5621,6 +6206,15 @@ function getCollectionQueryParams(controller) {
5621
6206
  return getCustomParams(controller);
5622
6207
  }
5623
6208
 
6209
+ // src/packages/router/route/params/utils/validate-client-id.ts
6210
+ function validateClientId(params) {
6211
+ const { data } = params;
6212
+ if (data && typeof data === "object" && Reflect.has(data, "id")) {
6213
+ throw new client_generated_id_error_default();
6214
+ }
6215
+ return true;
6216
+ }
6217
+
5624
6218
  // src/packages/router/route/params/utils/validate-resource-id.ts
5625
6219
  function validateResourceId({
5626
6220
  params: {
@@ -5786,6 +6380,9 @@ var Route = class extends set_default {
5786
6380
  ...req.params,
5787
6381
  ...this.parseParams(req.url.params)
5788
6382
  };
6383
+ if (this.action === "create" && req.method === "POST") {
6384
+ validateClientId(params);
6385
+ }
5789
6386
  if (req.method !== "OPTIONS") {
5790
6387
  params = this.params.validate(params);
5791
6388
  }
@@ -6098,10 +6695,10 @@ var Router = class extends map_default {
6098
6695
  var router_default = Router;
6099
6696
 
6100
6697
  // src/packages/loader/utils/bundle-for.ts
6101
- var import_path2 = require("path");
6698
+ var import_path4 = require("path");
6102
6699
 
6103
6700
  // src/packages/loader/utils/format-key.ts
6104
- var import_inflection13 = require("inflection");
6701
+ var import_inflection15 = require("inflection");
6105
6702
  var NAMESPACE_DELIMITER = /\$-/g;
6106
6703
  function formatKey(key, formatter) {
6107
6704
  return chain(key).pipe((str) => {
@@ -6109,7 +6706,7 @@ function formatKey(key, formatter) {
6109
6706
  return formatter(str);
6110
6707
  }
6111
6708
  return str;
6112
- }).pipe(underscore).pipe(import_inflection13.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6709
+ }).pipe(underscore).pipe(import_inflection15.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6113
6710
  }
6114
6711
 
6115
6712
  // src/packages/loader/utils/bundle-for.ts
@@ -6176,31 +6773,15 @@ function normalize2(manifest) {
6176
6773
  }
6177
6774
  function bundleFor(path) {
6178
6775
  const manifest = Reflect.apply(require, null, [
6179
- (0, import_path2.join)(path, "dist", "bundle")
6776
+ (0, import_path4.join)(path, "dist", "bundle")
6180
6777
  ]);
6181
6778
  return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
6182
6779
  }
6183
6780
 
6184
- // src/packages/loader/resolver/utils/closest-ancestor.ts
6185
- var import_path3 = require("path");
6186
- function closestAncestor(source, key) {
6187
- const name = import_path3.posix.basename(key);
6188
- let namespace = import_path3.posix.dirname(key);
6189
- if (namespace === ".") {
6190
- return source.get(name);
6191
- }
6192
- namespace = import_path3.posix.dirname(namespace);
6193
- const ancestor = source.get(import_path3.posix.join(namespace, name));
6194
- if (ancestor) {
6195
- return ancestor;
6196
- }
6197
- return closestAncestor(source, import_path3.posix.join(import_path3.posix.dirname(namespace), name));
6198
- }
6199
-
6200
6781
  // src/packages/loader/resolver/utils/closest-child.ts
6201
- var import_path4 = require("path");
6782
+ var import_path5 = require("path");
6202
6783
  function closestChild(source, key) {
6203
- const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path4.posix.basename(path), value]).filter(([resource2]) => key === resource2);
6784
+ const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path5.posix.basename(path), value]).filter(([resource2]) => key === resource2);
6204
6785
  return result;
6205
6786
  }
6206
6787
 
@@ -6223,7 +6804,7 @@ function resolve(group) {
6223
6804
  }
6224
6805
 
6225
6806
  // src/packages/loader/builder/utils/create-parent-builder.ts
6226
- var import_path5 = require("path");
6807
+ var import_path6 = require("path");
6227
6808
 
6228
6809
  // src/packages/loader/builder/utils/sort-by-namespace.ts
6229
6810
  function sortByNamespace([a], [b]) {
@@ -6244,7 +6825,7 @@ function createParentBuilder(construct) {
6244
6825
  let grandparent = null;
6245
6826
  if (key !== "root") {
6246
6827
  const found = result.find((namespace) => {
6247
- const dirname = import_path5.posix.dirname(key);
6828
+ const dirname = import_path6.posix.dirname(key);
6248
6829
  if (namespace.key === "root") {
6249
6830
  return dirname === ".";
6250
6831
  }
@@ -6314,13 +6895,13 @@ function normalizePort(port) {
6314
6895
  }
6315
6896
 
6316
6897
  // src/packages/application/utils/create-controller.ts
6317
- var import_path6 = require("path");
6898
+ var import_path7 = require("path");
6318
6899
  function createController(constructor, opts) {
6319
6900
  const { key, store, serializers } = opts;
6320
- const namespace = import_path6.posix.dirname(key).replace(".", "");
6901
+ const namespace = import_path7.posix.dirname(key).replace(".", "");
6321
6902
  let { parent } = opts;
6322
6903
  let model = tryCatchSync(
6323
- () => store.modelFor(import_path6.posix.basename(key))
6904
+ () => store.modelFor(import_path7.posix.basename(key))
6324
6905
  );
6325
6906
  let serializer = serializers.get(key);
6326
6907
  if (!model) {
@@ -6376,13 +6957,13 @@ function createController(constructor, opts) {
6376
6957
  }
6377
6958
 
6378
6959
  // src/packages/application/utils/create-serializer.ts
6379
- var import_path7 = require("path");
6960
+ var import_path8 = require("path");
6380
6961
  function createSerializer(constructor, opts) {
6381
6962
  const { key, store } = opts;
6382
- const namespace = import_path7.posix.dirname(key).replace(".", "");
6963
+ const namespace = import_path8.posix.dirname(key).replace(".", "");
6383
6964
  let { parent } = opts;
6384
6965
  let model = tryCatchSync(
6385
- () => store.modelFor(import_path7.posix.basename(key))
6966
+ () => store.modelFor(import_path8.posix.basename(key))
6386
6967
  );
6387
6968
  if (!model) {
6388
6969
  model = null;
@@ -6406,6 +6987,85 @@ function createSerializer(constructor, opts) {
6406
6987
  return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
6407
6988
  }
6408
6989
 
6990
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6991
+ var import_path9 = require("path");
6992
+
6993
+ // src/errors/namespaced-serializer-missing-error.ts
6994
+ var NamespacedSerializerMissingError = class extends ReferenceError {
6995
+ /**
6996
+ * @param missing - Each missing serializer key (`admin/comments`) with an
6997
+ * example of how it is reached (`admin/posts?include=comments`).
6998
+ */
6999
+ constructor(missing) {
7000
+ const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
7001
+ super(
7002
+ [
7003
+ "Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
7004
+ ...lines
7005
+ ].join("\n")
7006
+ );
7007
+ }
7008
+ };
7009
+ var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
7010
+
7011
+ // src/packages/application/utils/validate-namespaced-serializers.ts
7012
+ var distance = (via) => {
7013
+ const [, include] = via.split("?include=");
7014
+ return include ? include.split(".").length : 0;
7015
+ };
7016
+ function validateNamespacedSerializers(controllers, serializers) {
7017
+ const missing = /* @__PURE__ */ new Map();
7018
+ controllers.forEach((controller, key) => {
7019
+ const { namespace, model } = controller;
7020
+ if (!namespace || !model) {
7021
+ return;
7022
+ }
7023
+ const application = controllers.get(import_path9.posix.join(namespace, "application"));
7024
+ if (!application || application.serializerFallback !== false) {
7025
+ return;
7026
+ }
7027
+ const require2 = (type, via) => {
7028
+ const serializerKey = import_path9.posix.join(namespace, type);
7029
+ const serializer = serializers.get(serializerKey);
7030
+ if (!serializer) {
7031
+ const known = missing.get(serializerKey);
7032
+ if (known === void 0 || distance(via) < distance(known)) {
7033
+ missing.set(serializerKey, via);
7034
+ }
7035
+ }
7036
+ return serializer;
7037
+ };
7038
+ const own = require2(model.resourceName, key);
7039
+ if (!own) {
7040
+ return;
7041
+ }
7042
+ const depth = Math.max(controller.maxIncludeDepth, 1);
7043
+ let level = [
7044
+ [model, own, []]
7045
+ ];
7046
+ for (let current = 1; current <= depth && level.length; current += 1) {
7047
+ const next = [];
7048
+ level.forEach(([parent, serializer, path]) => {
7049
+ [...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
7050
+ const opts = parent.relationshipFor(name);
7051
+ if (!opts) {
7052
+ return;
7053
+ }
7054
+ const nextPath = [...path, name];
7055
+ const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
7056
+ if (related) {
7057
+ next.push([opts.model, related, nextPath]);
7058
+ }
7059
+ });
7060
+ });
7061
+ level = next;
7062
+ }
7063
+ });
7064
+ if (missing.size) {
7065
+ throw new namespaced_serializer_missing_error_default(missing);
7066
+ }
7067
+ }
7068
+
6409
7069
  // src/packages/application/initialize.ts
6410
7070
  async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
6411
7071
  const load = createLoader(path);
@@ -6428,6 +7088,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6428
7088
  parent
6429
7089
  })
6430
7090
  );
7091
+ serializers.forEach((serializer) => {
7092
+ Reflect.defineProperty(serializer, "serializers", {
7093
+ value: serializers,
7094
+ writable: false,
7095
+ enumerable: false,
7096
+ configurable: false
7097
+ });
7098
+ });
6431
7099
  models.forEach((model) => {
6432
7100
  Reflect.defineProperty(model, "serializer", {
6433
7101
  value: closestChild(serializers, model.resourceName),
@@ -6453,6 +7121,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6453
7121
  configurable: false
6454
7122
  });
6455
7123
  });
7124
+ validateNamespacedSerializers(controllers, serializers);
6456
7125
  const ApplicationController = controllers.get("application");
6457
7126
  if (!ApplicationController) {
6458
7127
  throw new controller_missing_error_default("application");