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.mjs CHANGED
@@ -9,6 +9,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  import { green, yellow as yellow3 } from "chalk";
10
10
 
11
11
  // src/packages/database/constants.ts
12
+ var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
13
+ var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
12
14
  var VALID_DRIVERS = [
13
15
  "pg",
14
16
  "sqlite3",
@@ -1056,10 +1058,16 @@ async function buildResults({
1056
1058
  }
1057
1059
  );
1058
1060
  }
1061
+ const missing = new Set(
1062
+ entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
1063
+ );
1064
+ const absent = Array.from(missing).filter(
1065
+ (name) => model.relationshipFor(name)?.type === "hasOne"
1066
+ );
1059
1067
  const instance = Reflect.construct(model, [
1060
1068
  entries(record).reduce((r, entry) => {
1061
1069
  let [key, value] = entry;
1062
- if (value == null && pkPattern.test(key)) {
1070
+ if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
1063
1071
  return r;
1064
1072
  } else if (key.indexOf(".") >= 0) {
1065
1073
  const [a, b] = key.split(".");
@@ -1079,6 +1087,7 @@ async function buildResults({
1079
1087
  };
1080
1088
  }, {})
1081
1089
  ]);
1090
+ absent.forEach((name) => instance.absentRelationships.add(name));
1082
1091
  instance.currentChangeSet.persist();
1083
1092
  return instance;
1084
1093
  });
@@ -1090,6 +1099,17 @@ function createRunner(target, opts) {
1090
1099
  const { resolve: resolve2, reject } = opts;
1091
1100
  let didRun = false;
1092
1101
  RUNNERS.set(target, async () => {
1102
+ if (didRun) {
1103
+ return;
1104
+ }
1105
+ didRun = true;
1106
+ try {
1107
+ await run();
1108
+ } catch (err) {
1109
+ reject(err);
1110
+ }
1111
+ });
1112
+ async function run() {
1093
1113
  let results;
1094
1114
  const {
1095
1115
  model,
@@ -1099,10 +1119,6 @@ function createRunner(target, opts) {
1099
1119
  shouldCount,
1100
1120
  relationships
1101
1121
  } = target;
1102
- if (didRun) {
1103
- return;
1104
- }
1105
- didRun = true;
1106
1122
  if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
1107
1123
  target.select(...target.model.attributeNames);
1108
1124
  }
@@ -1143,7 +1159,7 @@ function createRunner(target, opts) {
1143
1159
  resolve2(result);
1144
1160
  }
1145
1161
  }
1146
- });
1162
+ }
1147
1163
  }
1148
1164
  }
1149
1165
  function runQuery(target) {
@@ -1848,7 +1864,9 @@ async function get(owner, key) {
1848
1864
  let { foreignKey } = opts;
1849
1865
  value = owner.currentChangeSet.get(key);
1850
1866
  foreignKey = camelize5(foreignKey, true);
1851
- if (!value) {
1867
+ if (!value && owner.absentRelationships.has(key)) {
1868
+ value = null;
1869
+ } else if (!value) {
1852
1870
  switch (type) {
1853
1871
  case "hasOne":
1854
1872
  value = await getHasOne(owner, {
@@ -2510,13 +2528,37 @@ async function initializeClass({
2510
2528
  return model;
2511
2529
  }
2512
2530
 
2531
+ // src/packages/server/utils/source-for.ts
2532
+ import { dasherize as dasherize3 } from "inflection";
2533
+ function memberName(key) {
2534
+ return dasherize3(underscore(key));
2535
+ }
2536
+ function escapePointer(segment) {
2537
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
2538
+ }
2539
+ function sourceFor(path) {
2540
+ const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
2541
+ if (!root) {
2542
+ return {};
2543
+ } else if (root === "data") {
2544
+ return {
2545
+ pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
2546
+ };
2547
+ }
2548
+ return {
2549
+ parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
2550
+ };
2551
+ }
2552
+
2513
2553
  // src/packages/database/validation/errors/validation-error.ts
2514
2554
  var ValidationError = class extends Error {
2515
- constructor(key, value) {
2516
- super(`Validation failed for ${key}: ${value}`);
2555
+ constructor(key) {
2556
+ super(`Validation failed for ${key}.`);
2557
+ this.key = key;
2558
+ this.source = sourceFor(`data.attributes.${key}`);
2517
2559
  }
2518
2560
  };
2519
- var validation_error_default = ValidationError;
2561
+ var validation_error_default = createServerError(ValidationError, 422);
2520
2562
 
2521
2563
  // src/packages/database/validation/index.ts
2522
2564
  var Validation = class {
@@ -2556,12 +2598,32 @@ function validate(instance) {
2556
2598
  validator: Reflect.get(instance.constructor.validates, key)
2557
2599
  })).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
2558
2600
  if (!validation.isValid()) {
2559
- throw new validation_error_default(validation.key, String(validation.value));
2601
+ throw new validation_error_default(validation.key);
2560
2602
  }
2561
2603
  return result;
2562
2604
  }, true);
2563
2605
  }
2564
2606
 
2607
+ // src/packages/database/model/utils/process-write-error.ts
2608
+ function driverMessage(message) {
2609
+ const index = message.lastIndexOf(" - ");
2610
+ return index >= 0 ? message.slice(index + 3) : message;
2611
+ }
2612
+ function processWriteError(err) {
2613
+ if (!(err instanceof Error)) {
2614
+ return err;
2615
+ }
2616
+ const { code } = err;
2617
+ const { message } = err;
2618
+ if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
2619
+ return new unique_constraint_error_default(driverMessage(message));
2620
+ }
2621
+ return err;
2622
+ }
2623
+ function rethrowWriteError(err) {
2624
+ throw processWriteError(err);
2625
+ }
2626
+
2565
2627
  // src/packages/database/model/utils/run-hooks.ts
2566
2628
  function runHooks(record, trx, ...hooks) {
2567
2629
  return hooks.filter((hook) => Boolean(hook)).reduce(
@@ -2601,6 +2663,12 @@ var Model = class {
2601
2663
  writable: false,
2602
2664
  enumerable: false,
2603
2665
  configurable: false
2666
+ },
2667
+ absentRelationships: {
2668
+ value: /* @__PURE__ */ new Set(),
2669
+ writable: false,
2670
+ enumerable: false,
2671
+ configurable: false
2604
2672
  }
2605
2673
  });
2606
2674
  const {
@@ -2947,9 +3015,9 @@ var Model = class {
2947
3015
  return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
2948
3016
  };
2949
3017
  if (transaction) {
2950
- return run(transaction);
3018
+ return run(transaction).catch(rethrowWriteError);
2951
3019
  }
2952
- return this.transaction(run);
3020
+ return this.transaction(run).catch(rethrowWriteError);
2953
3021
  }
2954
3022
  /**
2955
3023
  * Permanently delete the instance from the database.
@@ -3069,9 +3137,9 @@ var Model = class {
3069
3137
  return createTransactionResultProxy(instance, true);
3070
3138
  };
3071
3139
  if (transaction) {
3072
- return run(transaction);
3140
+ return run(transaction).catch(rethrowWriteError);
3073
3141
  }
3074
- return this.transaction(run);
3142
+ return this.transaction(run).catch(rethrowWriteError);
3075
3143
  }
3076
3144
  /**
3077
3145
  * Specify the transaction object to use for following save, update, or
@@ -3458,7 +3526,7 @@ var TRUE = /^true$/;
3458
3526
  var BRACKETS = /(?:\[])/g;
3459
3527
 
3460
3528
  // src/utils/transform-keys.ts
3461
- import { camelize as camelize7, dasherize as dasherize3 } from "inflection";
3529
+ import { camelize as camelize7, dasherize as dasherize4 } from "inflection";
3462
3530
  function transformKeys(source, transformer, deep = false) {
3463
3531
  if (Array.isArray(source)) {
3464
3532
  return source.slice(0);
@@ -3489,7 +3557,7 @@ function camelizeKeys(source, deep) {
3489
3557
  function dasherizeKeys(source, deep) {
3490
3558
  return transformKeys(
3491
3559
  source,
3492
- (key) => dasherize3(underscore(key)),
3560
+ (key) => dasherize4(underscore(key)),
3493
3561
  deep
3494
3562
  );
3495
3563
  }
@@ -3798,6 +3866,10 @@ function dataFor(status, err) {
3798
3866
  if (title) {
3799
3867
  errData.title = title;
3800
3868
  }
3869
+ const source = err?.source;
3870
+ if (source && (source.pointer || source.parameter)) {
3871
+ errData.source = source;
3872
+ }
3801
3873
  if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
3802
3874
  errData.detail = err.message.replace(/^\[public\]/gi, "");
3803
3875
  }
@@ -4020,9 +4092,10 @@ function merge(dest, source) {
4020
4092
  }
4021
4093
 
4022
4094
  // src/packages/controller/utils/params-to-query.ts
4023
- function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4095
+ function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
4024
4096
  const relationships = entries(model.relationships);
4025
4097
  const includedFields = omit(fields, model.resourceName);
4098
+ const included = include && include.map((path) => path.split(".")[0]);
4026
4099
  let query = {
4027
4100
  id,
4028
4101
  filter,
@@ -4065,9 +4138,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4065
4138
  if (!value.includes(relationship.model.primaryKey)) {
4066
4139
  value = [relationship.model.primaryKey, ...value];
4067
4140
  }
4068
- if (include && value.length === 1 && include.includes(name)) {
4069
- value = [...value, ...relationship.model.serializer.attributes];
4070
- } else if (!include && value.length > 1) {
4141
+ if (included && value.length === 1 && included.includes(name)) {
4142
+ value = [...value, ...serializerFor(relationship.model).attributes];
4143
+ } else if (!included && value.length > 1) {
4071
4144
  value = value.slice(0, 1);
4072
4145
  }
4073
4146
  return {
@@ -4082,18 +4155,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4082
4155
  }
4083
4156
 
4084
4157
  // src/packages/controller/utils/find-one.ts
4085
- function findOne(model, req) {
4158
+ function findOne(model, req, serializerFor) {
4086
4159
  const params = merge(req.defaultParams, req.params);
4087
- const { id, select, include } = paramsToQuery(model, params);
4160
+ const { id, select, include } = paramsToQuery(model, params, serializerFor);
4088
4161
  return model.find(id).select(...select).include(include);
4089
4162
  }
4090
4163
 
4091
4164
  // src/packages/controller/utils/find-many.ts
4092
- function findMany(model, req) {
4165
+ function findMany(model, req, serializerFor) {
4093
4166
  const params = merge(req.defaultParams, req.params);
4094
4167
  const { sort, page, limit, select, filter, include } = paramsToQuery(
4095
4168
  model,
4096
- params
4169
+ params,
4170
+ serializerFor
4097
4171
  );
4098
4172
  return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
4099
4173
  }
@@ -4123,6 +4197,53 @@ function resolveRelationships(model, relationships = {}) {
4123
4197
  );
4124
4198
  }
4125
4199
 
4200
+ // src/packages/controller/errors/related-record-not-found-error.ts
4201
+ var RelatedRecordNotFoundError = class extends Error {
4202
+ constructor({ name, primaryKey }, primaryKeyValue, path) {
4203
+ super(
4204
+ `Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
4205
+ );
4206
+ this.source = sourceFor(path);
4207
+ }
4208
+ };
4209
+ var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
4210
+
4211
+ // src/packages/controller/utils/validate-relationships.ts
4212
+ async function validateRelationships(model, relationships = {}) {
4213
+ await Promise.all(
4214
+ entries(relationships).map(async ([key, value]) => {
4215
+ const opts = model.relationshipFor(key);
4216
+ const { data = null } = value || {};
4217
+ if (!opts || !data) {
4218
+ return;
4219
+ }
4220
+ const { model: related } = opts;
4221
+ const isMany = Array.isArray(data);
4222
+ const ids = (isMany ? data : [data]).map(
4223
+ (item) => Reflect.get(Object(item), "id")
4224
+ );
4225
+ if (!ids.length) {
4226
+ return;
4227
+ }
4228
+ const column = related.columnNameFor(related.primaryKey) || "id";
4229
+ const rows = await related.table().whereIn(
4230
+ `${related.tableName}.${column}`,
4231
+ ids.filter((id) => id != null)
4232
+ ).select(`${related.tableName}.${column}`);
4233
+ const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
4234
+ const missing = ids.findIndex((id) => !found.has(String(id)));
4235
+ if (missing >= 0) {
4236
+ const path = `data.relationships.${key}.data`;
4237
+ throw new related_record_not_found_error_default(
4238
+ related,
4239
+ ids[missing],
4240
+ isMany ? `${path}.${missing}` : path
4241
+ );
4242
+ }
4243
+ })
4244
+ );
4245
+ }
4246
+
4126
4247
  // src/packages/controller/constants.ts
4127
4248
  var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
4128
4249
 
@@ -4308,6 +4429,74 @@ var Controller = class {
4308
4429
  * @public
4309
4430
  */
4310
4431
  defaultPerPage = 25;
4432
+ /**
4433
+ * How many relationships deep an `?include` path may go on this
4434
+ * controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
4435
+ * only direct relationships (`comments`) can be included. Paths deeper than
4436
+ * this are rejected with `400 Bad Request`.
4437
+ *
4438
+ * Set it on `ApplicationController` to change it for the whole app, or on a
4439
+ * single controller to override it there.
4440
+ *
4441
+ * ```javascript
4442
+ * class ApplicationController extends Controller {
4443
+ * maxIncludeDepth = 2;
4444
+ * }
4445
+ * ```
4446
+ *
4447
+ * Every allowed path is enumerated up front from the serializers'
4448
+ * relationships, and each nested level costs its own queries per request, so
4449
+ * keep this small.
4450
+ *
4451
+ * @property maxIncludeDepth
4452
+ * @type {Number}
4453
+ * @default 3
4454
+ * @public
4455
+ */
4456
+ maxIncludeDepth = 3;
4457
+ /**
4458
+ * Whether a namespace may fall back to the root Serializer of a type it has
4459
+ * no Serializer for. Read from a namespace's `ApplicationController` and
4460
+ * applies to the whole namespace.
4461
+ *
4462
+ * By default `app/controllers/admin/comments.js` without an
4463
+ * `app/serializers/admin/comments.js` — or an included type without one —
4464
+ * is serialized by the root Serializer, with every attribute and
4465
+ * relationship it declares. For a namespace that must only expose what it
4466
+ * declares itself, turn the fallback off:
4467
+ *
4468
+ * ```javascript
4469
+ * // app/controllers/admin/application.js
4470
+ * class AdminApplicationController extends ApplicationController {
4471
+ * serializerFallback = false;
4472
+ * }
4473
+ * ```
4474
+ *
4475
+ * The application then refuses to boot while any type the namespace can
4476
+ * serialize or `include` (down to each controller's `maxIncludeDepth`) has
4477
+ * no Serializer in that namespace, listing each missing one.
4478
+ *
4479
+ * @property serializerFallback
4480
+ * @type {Boolean}
4481
+ * @default true
4482
+ * @public
4483
+ */
4484
+ serializerFallback = true;
4485
+ /**
4486
+ * The Serializer to serialize (and validate, and load) related resources of
4487
+ * this Controller's responses with: the related model's Serializer in this
4488
+ * Controller's namespace, falling back to the root one.
4489
+ *
4490
+ * Always this Controller's namespace — not its Serializer's, which is the
4491
+ * root one when the namespace has no Serializer for this resource.
4492
+ *
4493
+ * @method serializerFor
4494
+ * @private
4495
+ */
4496
+ serializerFor(model) {
4497
+ const { serializer, namespace } = this;
4498
+ return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
4499
+ }
4311
4500
  constructor({ model, namespace, serializer }) {
4312
4501
  Object.assign(this, {
4313
4502
  model,
@@ -4333,7 +4522,7 @@ var Controller = class {
4333
4522
  * @public
4334
4523
  */
4335
4524
  index(req) {
4336
- return findMany(this.model, req);
4525
+ return findMany(this.model, req, (related) => this.serializerFor(related));
4337
4526
  }
4338
4527
  /**
4339
4528
  * This method supports including relationships, and sparse fieldsets via
@@ -4348,7 +4537,7 @@ var Controller = class {
4348
4537
  * @public
4349
4538
  */
4350
4539
  show(req) {
4351
- return findOne(this.model, req);
4540
+ return findOne(this.model, req, (related) => this.serializerFor(related));
4352
4541
  }
4353
4542
  /**
4354
4543
  * Create and return a single Model instance that the Controller instance
@@ -4369,6 +4558,7 @@ var Controller = class {
4369
4558
  data: { attributes, relationships }
4370
4559
  }
4371
4560
  } = req;
4561
+ await validateRelationships(model, relationships);
4372
4562
  const record = await model.create({
4373
4563
  ...attributes,
4374
4564
  ...resolveRelationships(model, relationships)
@@ -4394,12 +4584,13 @@ var Controller = class {
4394
4584
  */
4395
4585
  update(req) {
4396
4586
  const { model } = this;
4397
- return findOne(model, req).then((record) => {
4587
+ return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
4398
4588
  const {
4399
4589
  params: {
4400
4590
  data: { attributes, relationships }
4401
4591
  }
4402
4592
  } = req;
4593
+ await validateRelationships(model, relationships);
4403
4594
  return record.update({
4404
4595
  ...attributes,
4405
4596
  ...resolveRelationships(model, relationships)
@@ -4423,7 +4614,7 @@ var Controller = class {
4423
4614
  * @public
4424
4615
  */
4425
4616
  destroy(req) {
4426
- return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
4617
+ return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
4427
4618
  }
4428
4619
  /**
4429
4620
  * Respond to HEAD or OPTIONS requests.
@@ -4441,7 +4632,194 @@ var Controller = class {
4441
4632
  var controller_default = Controller;
4442
4633
 
4443
4634
  // src/packages/serializer/index.ts
4444
- import { dasherize as dasherize4 } from "inflection";
4635
+ import { posix as posix2 } from "path";
4636
+ import { dasherize as dasherize5 } from "inflection";
4637
+
4638
+ // src/packages/loader/resolver/utils/closest-ancestor.ts
4639
+ import { posix } from "path";
4640
+ function closestAncestor(source, key) {
4641
+ const name = posix.basename(key);
4642
+ let namespace = posix.dirname(key);
4643
+ if (namespace === ".") {
4644
+ return source.get(name);
4645
+ }
4646
+ namespace = posix.dirname(namespace);
4647
+ const ancestor = source.get(posix.join(namespace, name));
4648
+ if (ancestor) {
4649
+ return ancestor;
4650
+ }
4651
+ return closestAncestor(source, posix.join(posix.dirname(namespace), name));
4652
+ }
4653
+
4654
+ // src/packages/serializer/utils/load-linkage.ts
4655
+ import { camelize as camelize9 } from "inflection";
4656
+ var valueOf = (record, key) => Reflect.get(record, key);
4657
+ var toId = (value) => value == null ? null : String(value);
4658
+ async function loadLinkage(model, records, names) {
4659
+ const ids = Array.from(
4660
+ new Set(records.map((record) => record.getPrimaryKey()))
4661
+ );
4662
+ const linkage = /* @__PURE__ */ new Map();
4663
+ const belongsTo = [];
4664
+ const queries = [];
4665
+ ids.forEach((id) => {
4666
+ linkage.set(
4667
+ String(id),
4668
+ names.reduce((result, name) => {
4669
+ const opts = model.relationshipFor(name);
4670
+ result[name] = opts && opts.type === "hasMany" ? [] : null;
4671
+ return result;
4672
+ }, {})
4673
+ );
4674
+ });
4675
+ if (!ids.length) {
4676
+ return linkage;
4677
+ }
4678
+ const link = (ownerId, name, relatedId) => {
4679
+ const owner = linkage.get(String(ownerId));
4680
+ const id = toId(relatedId);
4681
+ if (!owner || id === null) {
4682
+ return;
4683
+ }
4684
+ const current = owner[name];
4685
+ if (Array.isArray(current)) {
4686
+ if (!current.includes(id)) {
4687
+ current.push(id);
4688
+ }
4689
+ } else if (current === null) {
4690
+ owner[name] = id;
4691
+ }
4692
+ };
4693
+ names.forEach((name) => {
4694
+ const opts = model.relationshipFor(name);
4695
+ if (!opts) {
4696
+ return;
4697
+ }
4698
+ const { type, through, model: related } = opts;
4699
+ const foreignKey = camelize9(opts.foreignKey, true);
4700
+ if (type === "belongsTo") {
4701
+ belongsTo.push([name, foreignKey, related]);
4702
+ } else if (through) {
4703
+ const inverse = related.relationshipFor(opts.inverse);
4704
+ if (!inverse) {
4705
+ return;
4706
+ }
4707
+ const relatedKey = camelize9(inverse.foreignKey, true);
4708
+ queries.push(
4709
+ (async () => {
4710
+ const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
4711
+ rows.forEach((row) => {
4712
+ link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
4713
+ });
4714
+ })()
4715
+ );
4716
+ } else {
4717
+ queries.push(
4718
+ (async () => {
4719
+ const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
4720
+ rows.forEach((row) => {
4721
+ link(valueOf(row, foreignKey), name, row.getPrimaryKey());
4722
+ });
4723
+ })()
4724
+ );
4725
+ }
4726
+ });
4727
+ if (belongsTo.length) {
4728
+ queries.push(
4729
+ (async () => {
4730
+ const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
4731
+ rows.forEach((row) => {
4732
+ belongsTo.forEach(([name, key]) => {
4733
+ link(row.getPrimaryKey(), name, valueOf(row, key));
4734
+ });
4735
+ });
4736
+ await dropDangling(linkage, belongsTo);
4737
+ })()
4738
+ );
4739
+ }
4740
+ await Promise.all(queries);
4741
+ return linkage;
4742
+ }
4743
+ async function dropDangling(linkage, belongsTo) {
4744
+ const wanted = /* @__PURE__ */ new Map();
4745
+ belongsTo.forEach(([name, , related]) => {
4746
+ const ids = wanted.get(related) || /* @__PURE__ */ new Set();
4747
+ linkage.forEach(({ [name]: id }) => {
4748
+ if (typeof id === "string") {
4749
+ ids.add(id);
4750
+ }
4751
+ });
4752
+ wanted.set(related, ids);
4753
+ });
4754
+ const existing = /* @__PURE__ */ new Map();
4755
+ await Promise.all(
4756
+ Array.from(wanted, async ([related, ids]) => {
4757
+ if (!ids.size) {
4758
+ existing.set(related, ids);
4759
+ return;
4760
+ }
4761
+ const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
4762
+ existing.set(
4763
+ related,
4764
+ new Set(rows.map((row) => String(row.getPrimaryKey())))
4765
+ );
4766
+ })
4767
+ );
4768
+ belongsTo.forEach(([name, , related]) => {
4769
+ const found = existing.get(related);
4770
+ linkage.forEach((owner) => {
4771
+ const id = owner[name];
4772
+ if (typeof id === "string" && !found?.has(id)) {
4773
+ owner[name] = null;
4774
+ }
4775
+ });
4776
+ });
4777
+ }
4778
+
4779
+ // src/packages/serializer/utils/include-tree.ts
4780
+ function createIncludeTree(paths = []) {
4781
+ const tree = /* @__PURE__ */ new Map();
4782
+ paths.forEach((path) => {
4783
+ path.split(".").filter(Boolean).reduce((node, name) => {
4784
+ let child = node.get(name);
4785
+ if (!child) {
4786
+ child = /* @__PURE__ */ new Map();
4787
+ node.set(name, child);
4788
+ }
4789
+ return child;
4790
+ }, tree);
4791
+ });
4792
+ return tree;
4793
+ }
4794
+ function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
4795
+ if (depth < 1) {
4796
+ return [];
4797
+ }
4798
+ return names.reduce((paths, name) => {
4799
+ const opts = model.relationshipFor(name);
4800
+ if (!opts) {
4801
+ return paths;
4802
+ }
4803
+ const { model: related } = opts;
4804
+ const serializer = serializerFor(related);
4805
+ if (!serializer) {
4806
+ return [...paths, name];
4807
+ }
4808
+ const { hasOne, hasMany } = serializer;
4809
+ return [
4810
+ ...paths,
4811
+ name,
4812
+ ...enumerateIncludePaths(
4813
+ related,
4814
+ [...hasOne, ...hasMany],
4815
+ depth - 1,
4816
+ serializerFor
4817
+ ).map((path) => `${name}.${path}`)
4818
+ ];
4819
+ }, []);
4820
+ }
4821
+
4822
+ // src/packages/serializer/index.ts
4445
4823
  var Serializer = class {
4446
4824
  /**
4447
4825
  * An Array of the `hasOne` or `belongsTo` relationships on a Serializer
@@ -4528,9 +4906,17 @@ var Serializer = class {
4528
4906
  * the resource and relationship objects in the returned [JSON API](
4529
4907
  * http://jsonapi.org) document object.
4530
4908
  *
4531
- * @param {Array} options.include - An array of strings containing the
4532
- * relationship keys that should be added to the top level included object of
4533
- * the returned [JSON API](http://jsonapi.org) document object.
4909
+ * @param {Array} options.include - An array of relationship paths (e.g.
4910
+ * `'comments'` or `'comments.user'`) whose resources should be added to the
4911
+ * top level included object of the returned [JSON API](http://jsonapi.org)
4912
+ * document object. Intermediate resources of a nested path are included too.
4913
+ *
4914
+ * @param {String} options.namespace - The namespace of the request, i.e. of
4915
+ * the Controller handling it. Every link in the document is built in it, and
4916
+ * included resources are serialized by their Serializer in it (falling back
4917
+ * to the root). Defaults to this Serializer's namespace — which is the root
4918
+ * one when a namespaced Controller has no Serializer of its own, so the
4919
+ * Controller passes its namespace explicitly.
4534
4920
  *
4535
4921
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
4536
4922
  * object.
@@ -4541,38 +4927,61 @@ var Serializer = class {
4541
4927
  data,
4542
4928
  links,
4543
4929
  domain,
4544
- include
4930
+ include,
4931
+ namespace = this.namespace
4545
4932
  }) {
4546
- let serialized = {};
4547
- const included = [];
4933
+ const tree = createIncludeTree(include);
4934
+ const related = /* @__PURE__ */ new Map();
4935
+ const included = /* @__PURE__ */ new Map();
4936
+ const names = Array.from(tree.keys());
4937
+ let primary;
4938
+ let serialized;
4548
4939
  if (Array.isArray(data)) {
4549
- serialized = {
4550
- data: await Promise.all(
4551
- data.map(
4552
- (item) => this.formatOne({
4553
- item,
4554
- domain,
4555
- include,
4556
- included
4557
- })
4558
- )
4940
+ primary = await Promise.all(
4941
+ data.map(
4942
+ (item) => this.formatOne({
4943
+ item,
4944
+ domain,
4945
+ related,
4946
+ namespace,
4947
+ include: names
4948
+ })
4559
4949
  )
4560
- };
4950
+ );
4951
+ serialized = { data: primary };
4561
4952
  } else {
4562
- serialized = {
4563
- data: await this.formatOne({
4953
+ const resource2 = await this.formatOne({
4954
+ domain,
4955
+ related,
4956
+ namespace,
4957
+ item: data,
4958
+ include: names,
4959
+ links: false
4960
+ });
4961
+ primary = [resource2];
4962
+ serialized = { data: resource2 };
4963
+ }
4964
+ for (const [name, children] of tree) {
4965
+ const opts = this.model.relationshipFor(name);
4966
+ const records = related.get(name);
4967
+ if (opts && records) {
4968
+ await this.addIncluded({
4564
4969
  domain,
4565
- include,
4970
+ records,
4566
4971
  included,
4567
- item: data,
4568
- links: false
4569
- })
4570
- };
4972
+ namespace,
4973
+ model: opts.model,
4974
+ tree: children
4975
+ });
4976
+ }
4571
4977
  }
4572
- if (included.length) {
4978
+ primary.forEach((resource2) => {
4979
+ included.delete(resourceKey(resource2));
4980
+ });
4981
+ if (included.size) {
4573
4982
  serialized = {
4574
4983
  ...serialized,
4575
- included: uniq(included, "id", "type")
4984
+ included: Array.from(included.values())
4576
4985
  };
4577
4986
  }
4578
4987
  return {
@@ -4587,6 +4996,12 @@ var Serializer = class {
4587
4996
  * Transform a single Model instance into a [JSON API](http://jsonapi.org)
4588
4997
  * resource object.
4589
4998
  *
4999
+ * Relationships are serialized in one of two ways. By default each one is
5000
+ * read from the Model instance (for primary data the query has already
5001
+ * loaded them). When `linkage` is given — as it is for included resources,
5002
+ * whose relationships are batch-loaded by `addIncluded()` — the resource
5003
+ * linkage is built from it instead, without touching the database.
5004
+ *
4590
5005
  * @method formatOne
4591
5006
  *
4592
5007
  * @param {Object} options - An options object used for building the returned
@@ -4603,17 +5018,19 @@ var Serializer = class {
4603
5018
  * the top level links object or relationship links objects in the returned
4604
5019
  * [JSON API](http://jsonapi.org) resource object.
4605
5020
  *
4606
- * @param {Array} options.include - An array of strings containing the
4607
- * relationship keys that should be added to the top level included object of
4608
- * a [JSON API](http://jsonapi.org) document object.
5021
+ * @param {Array} options.include - An array of the relationship keys whose
5022
+ * related records should be collected into `options.related`.
4609
5023
  *
4610
- * @param {Array} options.included - An array of [JSON API](
4611
- * http://jsonapi.org) resource objects that will be added to the top level
4612
- * included array of a [JSON API](http://jsonapi.org) document object.
5024
+ * @param {Map} options.related - Collects, per relationship key in
5025
+ * `options.include`, the related Model instances that belong in the top
5026
+ * level included object of a [JSON API](http://jsonapi.org) document object.
4613
5027
  *
4614
- * @param {Boolean} options.formatRelationships - Wether or not
4615
- * relationships should be formatted and included in the returned
4616
- * [JSON API](http://jsonapi.org) resource object.
5028
+ * @param {Object} options.linkage - Pre-loaded resource linkage (related
5029
+ * primary keys per relationship key) to serialize relationships from.
5030
+ *
5031
+ * @param {String} options.namespace - The namespace to build links in.
5032
+ * Defaults to this Serializer's; included resources pass the namespace of
5033
+ * the request, so every link in a document points into the same namespace.
4617
5034
  *
4618
5035
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
4619
5036
  * object.
@@ -4624,13 +5041,15 @@ var Serializer = class {
4624
5041
  item,
4625
5042
  links,
4626
5043
  domain,
4627
- include,
4628
- included,
4629
- formatRelationships = true
5044
+ include = [],
5045
+ related,
5046
+ linkage,
5047
+ namespace = this.namespace
4630
5048
  }) {
4631
5049
  const { resourceName: type } = item;
4632
5050
  const id = String(item.getPrimaryKey());
4633
- let relationships = {};
5051
+ const names = [...this.hasOne, ...this.hasMany];
5052
+ let relationships;
4634
5053
  const attributes = dasherizeKeys(
4635
5054
  item.getAttributes(
4636
5055
  ...Object.keys(item.rawColumnData).filter(
@@ -4643,34 +5062,41 @@ var Serializer = class {
4643
5062
  type,
4644
5063
  attributes
4645
5064
  };
4646
- if (formatRelationships) {
5065
+ if (linkage) {
5066
+ relationships = names.reduce(
5067
+ (hash, name) => ({
5068
+ ...hash,
5069
+ [dasherize5(underscore(name))]: this.formatLinkage(
5070
+ domain,
5071
+ this.model.relationshipFor(name)?.model.resourceName,
5072
+ linkage[name],
5073
+ namespace
5074
+ )
5075
+ }),
5076
+ {}
5077
+ );
5078
+ } else {
5079
+ const collect = (name, records) => {
5080
+ if (related && include.includes(name)) {
5081
+ related.set(name, [...related.get(name) || [], ...records]);
5082
+ }
5083
+ };
4647
5084
  relationships = await promiseHash(
4648
- [...this.hasOne, ...this.hasMany].reduce(
5085
+ names.reduce(
4649
5086
  (hash, name) => ({
4650
5087
  ...hash,
4651
- [dasherize4(underscore(name))]: (async () => {
4652
- const related = await Reflect.get(item, name);
4653
- if (Array.isArray(related)) {
5088
+ [dasherize5(underscore(name))]: (async () => {
5089
+ const value = await Reflect.get(item, name);
5090
+ if (Array.isArray(value)) {
5091
+ collect(name, value);
4654
5092
  return {
4655
- data: await Promise.all(
4656
- related.map(async (relatedItem) => {
4657
- const { data: relatedData } = await this.formatRelationship({
4658
- domain,
4659
- included,
4660
- item: relatedItem,
4661
- include: include.includes(name)
4662
- });
4663
- return relatedData;
4664
- })
5093
+ data: value.map(
5094
+ (relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
4665
5095
  )
4666
5096
  };
4667
- } else if (related && related.id != null) {
4668
- return this.formatRelationship({
4669
- domain,
4670
- included,
4671
- item: related,
4672
- include: include.includes(name)
4673
- });
5097
+ } else if (value && value.id != null) {
5098
+ collect(name, [value]);
5099
+ return this.formatRelationship(value, domain, namespace);
4674
5100
  }
4675
5101
  return {
4676
5102
  data: null
@@ -4685,16 +5111,9 @@ var Serializer = class {
4685
5111
  serialized.relationships = relationships;
4686
5112
  }
4687
5113
  if (links || typeof links !== "boolean") {
4688
- const { namespace } = this;
4689
- if (namespace) {
4690
- serialized.links = {
4691
- self: `${domain}/${namespace}/${type}/${id}`
4692
- };
4693
- } else {
4694
- serialized.links = {
4695
- self: `${domain}/${type}/${id}`
4696
- };
4697
- }
5114
+ serialized.links = {
5115
+ self: this.linkFor(domain, type, id, namespace)
5116
+ };
4698
5117
  }
4699
5118
  return serialized;
4700
5119
  }
@@ -4704,70 +5123,167 @@ var Serializer = class {
4704
5123
  *
4705
5124
  * @method formatRelationship
4706
5125
  *
4707
- * @param {Object} options - An options object used for building the returned
5126
+ * @param {Model} item - The Model instance to transform into the returned
4708
5127
  * [JSON API](http://jsonapi.org) relationship object.
4709
5128
  *
4710
- * @param {Model} options.item - The Model instance to transform into the
5129
+ * @param {String} domain - A string used to build links included in the
4711
5130
  * returned [JSON API](http://jsonapi.org) relationship object.
4712
5131
  *
4713
- * @param {String} options.domain - A string used to build links included in
4714
- * the returned [JSON API](http://jsonapi.org) relationship object.
4715
- *
4716
- * @param {Array} options.include - An array of strings containing the
4717
- * relationship keys that should be added to the top level included object of
4718
- * a [JSON API](http://jsonapi.org) document object.
5132
+ * @return {Object} A [JSON API](http://jsonapi.org) relationship object.
4719
5133
  *
4720
- * @param {Array} options.included - An array of [JSON API](
4721
- * http://jsonapi.org) resource objects that will be added to the top level
4722
- * included array of a [JSON API](http://jsonapi.org) document object.
4723
- *
4724
- * @return {Promise} Resolves with a [JSON API](http://jsonapi.org)
4725
- * relationship object.
5134
+ * @private
5135
+ */
5136
+ formatRelationship(item, domain, namespace = this.namespace) {
5137
+ return this.formatLinkage(
5138
+ domain,
5139
+ item.resourceName,
5140
+ String(item.getPrimaryKey()),
5141
+ namespace
5142
+ );
5143
+ }
5144
+ /**
5145
+ * Build a [JSON API](http://jsonapi.org) relationship object from resource
5146
+ * linkage, in the same shape `formatRelationship()` produces from Model
5147
+ * instances: to-one relationships carry a `links` object, to-many ones only
5148
+ * `data`, and a missing to-one relationship is `{ data: null }`.
4726
5149
  *
5150
+ * @method formatLinkage
4727
5151
  * @private
4728
5152
  */
4729
- async formatRelationship({
4730
- item,
4731
- domain,
4732
- include,
4733
- included
4734
- }) {
4735
- const { namespace } = this;
4736
- const {
4737
- resourceName: type,
4738
- constructor: { serializer }
4739
- } = item;
4740
- const id = String(item.getPrimaryKey());
4741
- let links;
4742
- if (namespace) {
4743
- links = {
4744
- self: `${domain}/${namespace}/${type}/${id}`
4745
- };
4746
- } else {
4747
- links = {
4748
- self: `${domain}/${type}/${id}`
5153
+ formatLinkage(domain, type, linkage, namespace = this.namespace) {
5154
+ if (Array.isArray(linkage)) {
5155
+ return {
5156
+ data: type ? linkage.map((id) => ({ id, type })) : []
4749
5157
  };
4750
5158
  }
4751
- if (include) {
4752
- included.push(
4753
- await serializer.formatOne({
4754
- item,
4755
- domain,
4756
- include: [],
4757
- included: [],
4758
- formatRelationships: false
4759
- })
4760
- );
5159
+ if (linkage == null || !type) {
5160
+ return {
5161
+ data: null
5162
+ };
4761
5163
  }
4762
5164
  return {
4763
5165
  data: {
4764
- id,
5166
+ id: linkage,
4765
5167
  type
4766
5168
  },
4767
- links
5169
+ links: {
5170
+ self: this.linkFor(domain, type, linkage, namespace)
5171
+ }
4768
5172
  };
4769
5173
  }
5174
+ /**
5175
+ * Add `records` (instances of `model`) to `included` as resource objects,
5176
+ * then recurse into the relationships named in `tree`. Each is serialized by
5177
+ * `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
5178
+ * `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
5179
+ * is one and `CommentsSerializer` otherwise. The relationships of every level
5180
+ * are batch-loaded with one query per relationship, not one per record.
5181
+ *
5182
+ * @method addIncluded
5183
+ * @private
5184
+ */
5185
+ async addIncluded({
5186
+ model,
5187
+ records,
5188
+ tree,
5189
+ domain,
5190
+ included,
5191
+ namespace
5192
+ }) {
5193
+ const serializer = this.serializerFor(model, namespace);
5194
+ const unique = Array.from(
5195
+ new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
5196
+ );
5197
+ if (!unique.length) {
5198
+ return;
5199
+ }
5200
+ const names = [...serializer.hasOne, ...serializer.hasMany];
5201
+ const linkage = await loadLinkage(model, unique, names);
5202
+ for (const item of unique) {
5203
+ const id = String(item.getPrimaryKey());
5204
+ const key = resourceKey({ id, type: item.resourceName });
5205
+ if (!included.has(key)) {
5206
+ included.set(
5207
+ key,
5208
+ await serializer.formatOne({
5209
+ item,
5210
+ domain,
5211
+ linkage: linkage.get(id),
5212
+ namespace
5213
+ })
5214
+ );
5215
+ }
5216
+ }
5217
+ for (const [name, children] of tree) {
5218
+ const opts = model.relationshipFor(name);
5219
+ if (!opts || !names.includes(name)) {
5220
+ continue;
5221
+ }
5222
+ const { model: next } = opts;
5223
+ const ids = /* @__PURE__ */ new Set();
5224
+ linkage.forEach(({ [name]: value }) => {
5225
+ (Array.isArray(value) ? value : [value]).forEach((relatedId) => {
5226
+ if (relatedId != null) {
5227
+ ids.add(relatedId);
5228
+ }
5229
+ });
5230
+ });
5231
+ if (ids.size) {
5232
+ const attributes = this.serializerFor(
5233
+ next,
5234
+ namespace
5235
+ ).attributes.filter((attr) => next.attributeNames.includes(attr));
5236
+ const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
5237
+ await this.addIncluded({
5238
+ domain,
5239
+ included,
5240
+ namespace,
5241
+ model: next,
5242
+ records: nextRecords,
5243
+ tree: children
5244
+ });
5245
+ }
5246
+ }
5247
+ }
5248
+ /**
5249
+ * Resolve the Serializer for `model` in `namespace` (this Serializer's by
5250
+ * default), the way a namespaced Controller resolves its own:
5251
+ * `admin/comments` if it exists, otherwise the closest ancestor namespace's,
5252
+ * down to the root `comments` Serializer. Falls back to `model.serializer`
5253
+ * when this Serializer was not created by an application (e.g. in
5254
+ * isolation).
5255
+ *
5256
+ * Pass the request's namespace when there is one: a Serializer's own
5257
+ * namespace is the root one whenever it is a namespaced Controller's
5258
+ * fallback.
5259
+ *
5260
+ * @method serializerFor
5261
+ * @private
5262
+ */
5263
+ serializerFor(model, namespace = this.namespace) {
5264
+ const { serializers } = this;
5265
+ if (serializers) {
5266
+ const key = posix2.join(namespace || ".", model.resourceName);
5267
+ const serializer = serializers.get(key) || closestAncestor(serializers, key);
5268
+ if (serializer) {
5269
+ return serializer;
5270
+ }
5271
+ }
5272
+ return model.serializer;
5273
+ }
5274
+ /**
5275
+ * @private
5276
+ */
5277
+ linkFor(domain, type, id, namespace = this.namespace) {
5278
+ if (namespace) {
5279
+ return `${domain}/${namespace}/${type}/${id}`;
5280
+ }
5281
+ return `${domain}/${type}/${id}`;
5282
+ }
4770
5283
  };
5284
+ function resourceKey({ id, type }) {
5285
+ return `${type}:${id}`;
5286
+ }
4771
5287
  var serializer_default = Serializer;
4772
5288
 
4773
5289
  // src/packages/config/index.ts
@@ -4966,7 +5482,10 @@ function resource(action) {
4966
5482
  data,
4967
5483
  links,
4968
5484
  domain,
4969
- include
5485
+ include,
5486
+ // The request's namespace, not the serializer's: a namespaced
5487
+ // controller without its own serializer is given the root one.
5488
+ namespace
4970
5489
  });
4971
5490
  }
4972
5491
  return data;
@@ -5047,6 +5566,7 @@ var ParameterTypeError = class extends TypeError {
5047
5566
  Expected type '${type || "undefined"}' for parameter '${path}' but got
5048
5567
  '${actual}'.
5049
5568
  `);
5569
+ this.source = sourceFor(path);
5050
5570
  }
5051
5571
  };
5052
5572
  var parameter_type_error_default = createServerError(ParameterTypeError, 400);
@@ -5059,6 +5579,7 @@ var ParameterValueError = class extends TypeError {
5059
5579
  [${param.size ? Array.from(param.values()).join(", ") : ""}] but got
5060
5580
  ${String(actual)}.
5061
5581
  `);
5582
+ this.source = sourceFor(param.path);
5062
5583
  }
5063
5584
  };
5064
5585
  var parameter_value_error_default = createServerError(ParameterValueError, 400);
@@ -5067,6 +5588,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
5067
5588
  var InvalidParameterError = class extends TypeError {
5068
5589
  constructor(path) {
5069
5590
  super(`'${path}' is not a valid parameter for this resource.`);
5591
+ this.source = sourceFor(path);
5070
5592
  }
5071
5593
  };
5072
5594
  var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
@@ -5082,6 +5604,7 @@ var ResourceMismatchError = class extends TypeError {
5082
5604
  Expected '${String(expected)}' for parameter '${path}' but got
5083
5605
  ${String(normalized)}.
5084
5606
  `);
5607
+ this.source = sourceFor(path);
5085
5608
  }
5086
5609
  };
5087
5610
  var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
@@ -5090,14 +5613,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
5090
5613
  var ParameterRequiredError = class extends TypeError {
5091
5614
  constructor(path) {
5092
5615
  super(`Missing required parameter '${path}'.`);
5616
+ this.source = sourceFor(path);
5093
5617
  }
5094
5618
  };
5095
5619
  var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
5096
5620
 
5621
+ // src/packages/router/route/params/errors/client-generated-id-error.ts
5622
+ var ClientGeneratedIdError = class extends TypeError {
5623
+ constructor() {
5624
+ super("Client-generated IDs are not supported for this resource.");
5625
+ this.source = sourceFor("data.id");
5626
+ }
5627
+ };
5628
+ var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
5629
+
5630
+ // src/packages/router/route/params/errors/forbidden-parameter-error.ts
5631
+ var ForbiddenParameterError = class extends TypeError {
5632
+ constructor(path) {
5633
+ super(`Setting '${path}' is not supported for this resource.`);
5634
+ this.source = sourceFor(path);
5635
+ }
5636
+ };
5637
+ var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
5638
+
5097
5639
  // src/packages/router/route/params/errors/parameter-not-nullable-error.ts
5098
5640
  var ParameterNotNullableError = class extends TypeError {
5099
5641
  constructor({ path }) {
5100
5642
  super(`Parameter '${path}' is not nullable.`);
5643
+ this.source = sourceFor(path);
5101
5644
  }
5102
5645
  };
5103
5646
  var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
@@ -5226,13 +5769,14 @@ var Parameter = class extends set_default {
5226
5769
  path,
5227
5770
  type,
5228
5771
  required: Boolean(required),
5229
- sanitize: Boolean(sanitize)
5772
+ sanitize: Boolean(sanitize),
5773
+ restricted: values !== void 0
5230
5774
  });
5231
5775
  this.freeze();
5232
5776
  }
5233
5777
  validate(value) {
5234
5778
  validateType2(this, value);
5235
- if (this.size > 0) {
5779
+ if (this.restricted) {
5236
5780
  return validateValue(this, value);
5237
5781
  }
5238
5782
  return value;
@@ -5251,6 +5795,17 @@ function getURLParams(dynamicSegments) {
5251
5795
  ]);
5252
5796
  }
5253
5797
 
5798
+ // src/packages/router/route/params/parameter/forbidden-parameter.ts
5799
+ var ForbiddenParameter = class extends parameter_default {
5800
+ constructor(path) {
5801
+ super({ path });
5802
+ }
5803
+ validate() {
5804
+ throw new forbidden_parameter_error_default(this.path);
5805
+ }
5806
+ };
5807
+ var forbidden_parameter_default = ForbiddenParameter;
5808
+
5254
5809
  // src/packages/router/route/params/utils/get-data-params.ts
5255
5810
  function getIDParam({ model }) {
5256
5811
  const primaryKeyColumn = model.columnFor(model.primaryKey);
@@ -5303,16 +5858,48 @@ function getRelationshipsParam({
5303
5858
  model,
5304
5859
  params
5305
5860
  }) {
5861
+ const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
5862
+ key,
5863
+ new forbidden_parameter_default(`data.relationships.${key}`)
5864
+ ]);
5306
5865
  return [
5307
5866
  "relationships",
5308
5867
  new parameter_group_default(
5309
- params.reduce((group, param) => {
5310
- const path = `data.relationships.${param}`;
5311
- const opts = model.relationshipFor(param);
5312
- if (!opts) {
5313
- return group;
5314
- }
5315
- if (opts.type === "hasMany") {
5868
+ [
5869
+ ...params.reduce((group, param) => {
5870
+ const path = `data.relationships.${param}`;
5871
+ const opts = model.relationshipFor(param);
5872
+ if (!opts) {
5873
+ return group;
5874
+ }
5875
+ if (opts.type === "hasMany") {
5876
+ return [
5877
+ ...group,
5878
+ [
5879
+ param,
5880
+ new parameter_group_default(
5881
+ [
5882
+ [
5883
+ "data",
5884
+ new parameter_default({
5885
+ type: "array",
5886
+ path: `${path}.data`,
5887
+ required: true
5888
+ })
5889
+ ]
5890
+ ],
5891
+ {
5892
+ path
5893
+ }
5894
+ )
5895
+ ]
5896
+ ];
5897
+ }
5898
+ const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5899
+ let primaryKeyType = "number";
5900
+ if (primaryKeyColumn) {
5901
+ primaryKeyType = typeForColumn(primaryKeyColumn);
5902
+ }
5316
5903
  return [
5317
5904
  ...group,
5318
5905
  [
@@ -5321,11 +5908,32 @@ function getRelationshipsParam({
5321
5908
  [
5322
5909
  [
5323
5910
  "data",
5324
- new parameter_default({
5325
- type: "array",
5326
- path: `${path}.data`,
5327
- required: true
5328
- })
5911
+ new parameter_group_default(
5912
+ [
5913
+ [
5914
+ "id",
5915
+ new parameter_default({
5916
+ type: primaryKeyType,
5917
+ path: `${path}.data.id`,
5918
+ required: true
5919
+ })
5920
+ ],
5921
+ [
5922
+ "type",
5923
+ new parameter_default({
5924
+ type: "string",
5925
+ path: `${path}.data.type`,
5926
+ values: [opts.model.resourceName],
5927
+ required: true
5928
+ })
5929
+ ]
5930
+ ],
5931
+ {
5932
+ type: "array",
5933
+ path: `${path}.data`,
5934
+ required: true
5935
+ }
5936
+ )
5329
5937
  ]
5330
5938
  ],
5331
5939
  {
@@ -5334,55 +5942,9 @@ function getRelationshipsParam({
5334
5942
  )
5335
5943
  ]
5336
5944
  ];
5337
- }
5338
- const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5339
- let primaryKeyType = "number";
5340
- if (primaryKeyColumn) {
5341
- primaryKeyType = typeForColumn(primaryKeyColumn);
5342
- }
5343
- return [
5344
- ...group,
5345
- [
5346
- param,
5347
- new parameter_group_default(
5348
- [
5349
- [
5350
- "data",
5351
- new parameter_group_default(
5352
- [
5353
- [
5354
- "id",
5355
- new parameter_default({
5356
- type: primaryKeyType,
5357
- path: `${path}.data.id`,
5358
- required: true
5359
- })
5360
- ],
5361
- [
5362
- "type",
5363
- new parameter_default({
5364
- type: "string",
5365
- path: `${path}.data.type`,
5366
- values: [opts.model.resourceName],
5367
- required: true
5368
- })
5369
- ]
5370
- ],
5371
- {
5372
- type: "array",
5373
- path: `${path}.data`,
5374
- required: true
5375
- }
5376
- )
5377
- ]
5378
- ],
5379
- {
5380
- path
5381
- }
5382
- )
5383
- ]
5384
- ];
5385
- }, []),
5945
+ }, []),
5946
+ ...forbidden
5947
+ ],
5386
5948
  {
5387
5949
  path: "data.relationships"
5388
5950
  }
@@ -5490,10 +6052,9 @@ function getFilterParam({ filter }) {
5490
6052
  )
5491
6053
  ];
5492
6054
  }
5493
- function getFieldsParam({
5494
- model,
5495
- serializer: { hasOne, hasMany, attributes }
5496
- }) {
6055
+ function getFieldsParam(controller) {
6056
+ const { model, serializer } = controller;
6057
+ const { hasOne, hasMany, attributes } = serializer;
5497
6058
  const relationships = [...hasOne, ...hasMany];
5498
6059
  return [
5499
6060
  "fields",
@@ -5522,7 +6083,7 @@ function getFieldsParam({
5522
6083
  sanitize: true,
5523
6084
  values: [
5524
6085
  opts.model.primaryKey,
5525
- ...opts.model.serializer.attributes
6086
+ ...controller.serializerFor(opts.model).attributes
5526
6087
  ]
5527
6088
  })
5528
6089
  ]
@@ -5540,16 +6101,30 @@ function getFieldsParam({
5540
6101
  )
5541
6102
  ];
5542
6103
  }
5543
- function getIncludeParam({
5544
- serializer: { hasOne, hasMany }
5545
- }) {
6104
+ function getIncludeParam(controller) {
6105
+ const { model, maxIncludeDepth, serializer } = controller;
6106
+ const { hasOne, hasMany } = serializer;
5546
6107
  const relationships = [...hasOne, ...hasMany];
5547
6108
  return [
5548
6109
  "include",
5549
6110
  new parameter_default({
5550
6111
  path: "include",
5551
6112
  type: "array",
5552
- values: relationships
6113
+ // Every top level name stays allowed (as before), plus the nested paths
6114
+ // (`comments.user`) reachable through each related serializer in the
6115
+ // controller's namespace (at every level, even below a root fallback
6116
+ // serializer), down to the controller's `maxIncludeDepth`.
6117
+ values: Array.from(
6118
+ /* @__PURE__ */ new Set([
6119
+ ...relationships,
6120
+ ...enumerateIncludePaths(
6121
+ model,
6122
+ relationships,
6123
+ maxIncludeDepth,
6124
+ (related) => controller.serializerFor(related)
6125
+ )
6126
+ ])
6127
+ )
5553
6128
  })
5554
6129
  ];
5555
6130
  }
@@ -5587,6 +6162,15 @@ function getCollectionQueryParams(controller) {
5587
6162
  return getCustomParams(controller);
5588
6163
  }
5589
6164
 
6165
+ // src/packages/router/route/params/utils/validate-client-id.ts
6166
+ function validateClientId(params) {
6167
+ const { data } = params;
6168
+ if (data && typeof data === "object" && Reflect.has(data, "id")) {
6169
+ throw new client_generated_id_error_default();
6170
+ }
6171
+ return true;
6172
+ }
6173
+
5590
6174
  // src/packages/router/route/params/utils/validate-resource-id.ts
5591
6175
  function validateResourceId({
5592
6176
  params: {
@@ -5752,6 +6336,9 @@ var Route = class extends set_default {
5752
6336
  ...req.params,
5753
6337
  ...this.parseParams(req.url.params)
5754
6338
  };
6339
+ if (this.action === "create" && req.method === "POST") {
6340
+ validateClientId(params);
6341
+ }
5755
6342
  if (req.method !== "OPTIONS") {
5756
6343
  params = this.params.validate(params);
5757
6344
  }
@@ -6067,7 +6654,7 @@ var router_default = Router;
6067
6654
  import { join as joinPath2 } from "path";
6068
6655
 
6069
6656
  // src/packages/loader/utils/format-key.ts
6070
- import { dasherize as dasherize5 } from "inflection";
6657
+ import { dasherize as dasherize6 } from "inflection";
6071
6658
  var NAMESPACE_DELIMITER = /\$-/g;
6072
6659
  function formatKey(key, formatter) {
6073
6660
  return chain(key).pipe((str) => {
@@ -6075,7 +6662,7 @@ function formatKey(key, formatter) {
6075
6662
  return formatter(str);
6076
6663
  }
6077
6664
  return str;
6078
- }).pipe(underscore).pipe(dasherize5).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6665
+ }).pipe(underscore).pipe(dasherize6).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6079
6666
  }
6080
6667
 
6081
6668
  // src/packages/loader/utils/bundle-for.ts
@@ -6147,26 +6734,10 @@ function bundleFor(path) {
6147
6734
  return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
6148
6735
  }
6149
6736
 
6150
- // src/packages/loader/resolver/utils/closest-ancestor.ts
6151
- import { posix } from "path";
6152
- function closestAncestor(source, key) {
6153
- const name = posix.basename(key);
6154
- let namespace = posix.dirname(key);
6155
- if (namespace === ".") {
6156
- return source.get(name);
6157
- }
6158
- namespace = posix.dirname(namespace);
6159
- const ancestor = source.get(posix.join(namespace, name));
6160
- if (ancestor) {
6161
- return ancestor;
6162
- }
6163
- return closestAncestor(source, posix.join(posix.dirname(namespace), name));
6164
- }
6165
-
6166
6737
  // src/packages/loader/resolver/utils/closest-child.ts
6167
- import { posix as posix2 } from "path";
6738
+ import { posix as posix3 } from "path";
6168
6739
  function closestChild(source, key) {
6169
- const [[, result] = []] = Array.from(source).map(([path, value]) => [posix2.basename(path), value]).filter(([resource2]) => key === resource2);
6740
+ const [[, result] = []] = Array.from(source).map(([path, value]) => [posix3.basename(path), value]).filter(([resource2]) => key === resource2);
6170
6741
  return result;
6171
6742
  }
6172
6743
 
@@ -6189,7 +6760,7 @@ function resolve(group) {
6189
6760
  }
6190
6761
 
6191
6762
  // src/packages/loader/builder/utils/create-parent-builder.ts
6192
- import { posix as posix3 } from "path";
6763
+ import { posix as posix4 } from "path";
6193
6764
 
6194
6765
  // src/packages/loader/builder/utils/sort-by-namespace.ts
6195
6766
  function sortByNamespace([a], [b]) {
@@ -6210,7 +6781,7 @@ function createParentBuilder(construct) {
6210
6781
  let grandparent = null;
6211
6782
  if (key !== "root") {
6212
6783
  const found = result.find((namespace) => {
6213
- const dirname = posix3.dirname(key);
6784
+ const dirname = posix4.dirname(key);
6214
6785
  if (namespace.key === "root") {
6215
6786
  return dirname === ".";
6216
6787
  }
@@ -6280,13 +6851,13 @@ function normalizePort(port) {
6280
6851
  }
6281
6852
 
6282
6853
  // src/packages/application/utils/create-controller.ts
6283
- import { posix as posix4 } from "path";
6854
+ import { posix as posix5 } from "path";
6284
6855
  function createController(constructor, opts) {
6285
6856
  const { key, store, serializers } = opts;
6286
- const namespace = posix4.dirname(key).replace(".", "");
6857
+ const namespace = posix5.dirname(key).replace(".", "");
6287
6858
  let { parent } = opts;
6288
6859
  let model = tryCatchSync(
6289
- () => store.modelFor(posix4.basename(key))
6860
+ () => store.modelFor(posix5.basename(key))
6290
6861
  );
6291
6862
  let serializer = serializers.get(key);
6292
6863
  if (!model) {
@@ -6342,13 +6913,13 @@ function createController(constructor, opts) {
6342
6913
  }
6343
6914
 
6344
6915
  // src/packages/application/utils/create-serializer.ts
6345
- import { posix as posix5 } from "path";
6916
+ import { posix as posix6 } from "path";
6346
6917
  function createSerializer(constructor, opts) {
6347
6918
  const { key, store } = opts;
6348
- const namespace = posix5.dirname(key).replace(".", "");
6919
+ const namespace = posix6.dirname(key).replace(".", "");
6349
6920
  let { parent } = opts;
6350
6921
  let model = tryCatchSync(
6351
- () => store.modelFor(posix5.basename(key))
6922
+ () => store.modelFor(posix6.basename(key))
6352
6923
  );
6353
6924
  if (!model) {
6354
6925
  model = null;
@@ -6372,6 +6943,85 @@ function createSerializer(constructor, opts) {
6372
6943
  return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
6373
6944
  }
6374
6945
 
6946
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6947
+ import { posix as posix7 } from "path";
6948
+
6949
+ // src/errors/namespaced-serializer-missing-error.ts
6950
+ var NamespacedSerializerMissingError = class extends ReferenceError {
6951
+ /**
6952
+ * @param missing - Each missing serializer key (`admin/comments`) with an
6953
+ * example of how it is reached (`admin/posts?include=comments`).
6954
+ */
6955
+ constructor(missing) {
6956
+ const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
6957
+ super(
6958
+ [
6959
+ "Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
6960
+ ...lines
6961
+ ].join("\n")
6962
+ );
6963
+ }
6964
+ };
6965
+ var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
6966
+
6967
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6968
+ var distance = (via) => {
6969
+ const [, include] = via.split("?include=");
6970
+ return include ? include.split(".").length : 0;
6971
+ };
6972
+ function validateNamespacedSerializers(controllers, serializers) {
6973
+ const missing = /* @__PURE__ */ new Map();
6974
+ controllers.forEach((controller, key) => {
6975
+ const { namespace, model } = controller;
6976
+ if (!namespace || !model) {
6977
+ return;
6978
+ }
6979
+ const application = controllers.get(posix7.join(namespace, "application"));
6980
+ if (!application || application.serializerFallback !== false) {
6981
+ return;
6982
+ }
6983
+ const require2 = (type, via) => {
6984
+ const serializerKey = posix7.join(namespace, type);
6985
+ const serializer = serializers.get(serializerKey);
6986
+ if (!serializer) {
6987
+ const known = missing.get(serializerKey);
6988
+ if (known === void 0 || distance(via) < distance(known)) {
6989
+ missing.set(serializerKey, via);
6990
+ }
6991
+ }
6992
+ return serializer;
6993
+ };
6994
+ const own = require2(model.resourceName, key);
6995
+ if (!own) {
6996
+ return;
6997
+ }
6998
+ const depth = Math.max(controller.maxIncludeDepth, 1);
6999
+ let level = [
7000
+ [model, own, []]
7001
+ ];
7002
+ for (let current = 1; current <= depth && level.length; current += 1) {
7003
+ const next = [];
7004
+ level.forEach(([parent, serializer, path]) => {
7005
+ [...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
7006
+ const opts = parent.relationshipFor(name);
7007
+ if (!opts) {
7008
+ return;
7009
+ }
7010
+ const nextPath = [...path, name];
7011
+ const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
7012
+ if (related) {
7013
+ next.push([opts.model, related, nextPath]);
7014
+ }
7015
+ });
7016
+ });
7017
+ level = next;
7018
+ }
7019
+ });
7020
+ if (missing.size) {
7021
+ throw new namespaced_serializer_missing_error_default(missing);
7022
+ }
7023
+ }
7024
+
6375
7025
  // src/packages/application/initialize.ts
6376
7026
  async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
6377
7027
  const load = createLoader(path);
@@ -6394,6 +7044,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6394
7044
  parent
6395
7045
  })
6396
7046
  );
7047
+ serializers.forEach((serializer) => {
7048
+ Reflect.defineProperty(serializer, "serializers", {
7049
+ value: serializers,
7050
+ writable: false,
7051
+ enumerable: false,
7052
+ configurable: false
7053
+ });
7054
+ });
6397
7055
  models.forEach((model) => {
6398
7056
  Reflect.defineProperty(model, "serializer", {
6399
7057
  value: closestChild(serializers, model.resourceName),
@@ -6419,6 +7077,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6419
7077
  configurable: false
6420
7078
  });
6421
7079
  });
7080
+ validateNamespacedSerializers(controllers, serializers);
6422
7081
  const ApplicationController = controllers.get("application");
6423
7082
  if (!ApplicationController) {
6424
7083
  throw new controller_missing_error_default("application");