lumen-framework 3.0.3 → 3.1.0

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