lumen-framework 3.0.3 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli.cjs +70 -38
  2. package/dist/cli.cjs.map +4 -4
  3. package/dist/index.js +928 -259
  4. package/dist/index.js.map +4 -4
  5. package/dist/index.mjs +919 -250
  6. package/dist/index.mjs.map +4 -4
  7. package/dist/types/errors/namespaced-serializer-missing-error.d.ts +15 -0
  8. package/dist/types/packages/application/utils/validate-namespaced-serializers.d.ts +15 -0
  9. package/dist/types/packages/controller/errors/index.d.ts +1 -0
  10. package/dist/types/packages/controller/errors/related-record-not-found-error.d.ts +14 -0
  11. package/dist/types/packages/controller/index.d.ts +65 -0
  12. package/dist/types/packages/controller/utils/find-many.d.ts +2 -1
  13. package/dist/types/packages/controller/utils/find-one.d.ts +2 -1
  14. package/dist/types/packages/controller/utils/params-to-query.d.ts +7 -2
  15. package/dist/types/packages/controller/utils/validate-relationships.d.ts +8 -0
  16. package/dist/types/packages/database/constants.d.ts +1 -0
  17. package/dist/types/packages/database/model/index.d.ts +11 -0
  18. package/dist/types/packages/database/model/utils/process-write-error.d.ts +9 -1
  19. package/dist/types/packages/database/validation/errors/validation-error.d.ts +11 -2
  20. package/dist/types/packages/jsonapi/interfaces.d.ts +1 -1
  21. package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +13 -0
  22. package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +14 -0
  23. package/dist/types/packages/router/route/params/errors/index.d.ts +2 -0
  24. package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +2 -0
  25. package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +2 -0
  26. package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +2 -0
  27. package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +2 -0
  28. package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +2 -0
  29. package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +2 -0
  30. package/dist/types/packages/router/route/params/index.d.ts +1 -0
  31. package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +12 -0
  32. package/dist/types/packages/router/route/params/parameter/index.d.ts +8 -0
  33. package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +8 -0
  34. package/dist/types/packages/serializer/index.d.ts +111 -36
  35. package/dist/types/packages/serializer/utils/include-tree.d.ts +29 -0
  36. package/dist/types/packages/serializer/utils/load-linkage.d.ts +22 -0
  37. package/dist/types/packages/server/index.d.ts +2 -1
  38. package/dist/types/packages/server/interfaces.d.ts +5 -0
  39. package/dist/types/packages/server/utils/source-for.d.ts +10 -0
  40. package/package.json +1 -1
package/dist/index.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) {
@@ -1154,6 +1170,17 @@ function runQuery(target) {
1154
1170
  }
1155
1171
 
1156
1172
  // src/packages/database/query/index.ts
1173
+ var CONDITIONS = /* @__PURE__ */ new Set([
1174
+ "where",
1175
+ "whereNot",
1176
+ "whereIn",
1177
+ "whereNotIn",
1178
+ "whereNull",
1179
+ "whereNotNull",
1180
+ "whereBetween",
1181
+ "whereNotBetween",
1182
+ "whereRaw"
1183
+ ]);
1157
1184
  var Query = class extends Promise {
1158
1185
  constructor(model) {
1159
1186
  let resolve2;
@@ -1346,12 +1373,11 @@ var Query = class extends Promise {
1346
1373
  return this;
1347
1374
  }
1348
1375
  count() {
1349
- const validName = /^(where(((Not)?(In)?)|(Raw)|(Between)))$/g;
1350
1376
  Object.assign(this, {
1351
1377
  shouldCount: true,
1352
1378
  snapshots: [
1353
1379
  ["count", "* as countAll"],
1354
- ...this.snapshots.filter(([name]) => validName.test(name))
1380
+ ...this.snapshots.filter(([name]) => CONDITIONS.has(name))
1355
1381
  ]
1356
1382
  });
1357
1383
  return this;
@@ -1848,7 +1874,9 @@ async function get(owner, key) {
1848
1874
  let { foreignKey } = opts;
1849
1875
  value = owner.currentChangeSet.get(key);
1850
1876
  foreignKey = camelize5(foreignKey, true);
1851
- if (!value) {
1877
+ if (!value && owner.absentRelationships.has(key)) {
1878
+ value = null;
1879
+ } else if (!value) {
1852
1880
  switch (type) {
1853
1881
  case "hasOne":
1854
1882
  value = await getHasOne(owner, {
@@ -2510,13 +2538,37 @@ async function initializeClass({
2510
2538
  return model;
2511
2539
  }
2512
2540
 
2541
+ // src/packages/server/utils/source-for.ts
2542
+ import { dasherize as dasherize3 } from "inflection";
2543
+ function memberName(key) {
2544
+ return dasherize3(underscore(key));
2545
+ }
2546
+ function escapePointer(segment) {
2547
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
2548
+ }
2549
+ function sourceFor(path) {
2550
+ const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
2551
+ if (!root) {
2552
+ return {};
2553
+ } else if (root === "data") {
2554
+ return {
2555
+ pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
2556
+ };
2557
+ }
2558
+ return {
2559
+ parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
2560
+ };
2561
+ }
2562
+
2513
2563
  // src/packages/database/validation/errors/validation-error.ts
2514
2564
  var ValidationError = class extends Error {
2515
- constructor(key, value) {
2516
- super(`Validation failed for ${key}: ${value}`);
2565
+ constructor(key) {
2566
+ super(`Validation failed for ${key}.`);
2567
+ this.key = key;
2568
+ this.source = sourceFor(`data.attributes.${key}`);
2517
2569
  }
2518
2570
  };
2519
- var validation_error_default = ValidationError;
2571
+ var validation_error_default = createServerError(ValidationError, 422);
2520
2572
 
2521
2573
  // src/packages/database/validation/index.ts
2522
2574
  var Validation = class {
@@ -2556,12 +2608,32 @@ function validate(instance) {
2556
2608
  validator: Reflect.get(instance.constructor.validates, key)
2557
2609
  })).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
2558
2610
  if (!validation.isValid()) {
2559
- throw new validation_error_default(validation.key, String(validation.value));
2611
+ throw new validation_error_default(validation.key);
2560
2612
  }
2561
2613
  return result;
2562
2614
  }, true);
2563
2615
  }
2564
2616
 
2617
+ // src/packages/database/model/utils/process-write-error.ts
2618
+ function driverMessage(message) {
2619
+ const index = message.lastIndexOf(" - ");
2620
+ return index >= 0 ? message.slice(index + 3) : message;
2621
+ }
2622
+ function processWriteError(err) {
2623
+ if (!(err instanceof Error)) {
2624
+ return err;
2625
+ }
2626
+ const { code } = err;
2627
+ const { message } = err;
2628
+ if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
2629
+ return new unique_constraint_error_default(driverMessage(message));
2630
+ }
2631
+ return err;
2632
+ }
2633
+ function rethrowWriteError(err) {
2634
+ throw processWriteError(err);
2635
+ }
2636
+
2565
2637
  // src/packages/database/model/utils/run-hooks.ts
2566
2638
  function runHooks(record, trx, ...hooks) {
2567
2639
  return hooks.filter((hook) => Boolean(hook)).reduce(
@@ -2601,6 +2673,12 @@ var Model = class {
2601
2673
  writable: false,
2602
2674
  enumerable: false,
2603
2675
  configurable: false
2676
+ },
2677
+ absentRelationships: {
2678
+ value: /* @__PURE__ */ new Set(),
2679
+ writable: false,
2680
+ enumerable: false,
2681
+ configurable: false
2604
2682
  }
2605
2683
  });
2606
2684
  const {
@@ -2947,9 +3025,9 @@ var Model = class {
2947
3025
  return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
2948
3026
  };
2949
3027
  if (transaction) {
2950
- return run(transaction);
3028
+ return run(transaction).catch(rethrowWriteError);
2951
3029
  }
2952
- return this.transaction(run);
3030
+ return this.transaction(run).catch(rethrowWriteError);
2953
3031
  }
2954
3032
  /**
2955
3033
  * Permanently delete the instance from the database.
@@ -3069,9 +3147,9 @@ var Model = class {
3069
3147
  return createTransactionResultProxy(instance, true);
3070
3148
  };
3071
3149
  if (transaction) {
3072
- return run(transaction);
3150
+ return run(transaction).catch(rethrowWriteError);
3073
3151
  }
3074
- return this.transaction(run);
3152
+ return this.transaction(run).catch(rethrowWriteError);
3075
3153
  }
3076
3154
  /**
3077
3155
  * Specify the transaction object to use for following save, update, or
@@ -3458,7 +3536,7 @@ var TRUE = /^true$/;
3458
3536
  var BRACKETS = /(?:\[])/g;
3459
3537
 
3460
3538
  // src/utils/transform-keys.ts
3461
- import { camelize as camelize7, dasherize as dasherize3 } from "inflection";
3539
+ import { camelize as camelize7, dasherize as dasherize4 } from "inflection";
3462
3540
  function transformKeys(source, transformer, deep = false) {
3463
3541
  if (Array.isArray(source)) {
3464
3542
  return source.slice(0);
@@ -3489,7 +3567,7 @@ function camelizeKeys(source, deep) {
3489
3567
  function dasherizeKeys(source, deep) {
3490
3568
  return transformKeys(
3491
3569
  source,
3492
- (key) => dasherize3(underscore(key)),
3570
+ (key) => dasherize4(underscore(key)),
3493
3571
  deep
3494
3572
  );
3495
3573
  }
@@ -3798,6 +3876,10 @@ function dataFor(status, err) {
3798
3876
  if (title) {
3799
3877
  errData.title = title;
3800
3878
  }
3879
+ const source = err?.source;
3880
+ if (source && (source.pointer || source.parameter)) {
3881
+ errData.source = source;
3882
+ }
3801
3883
  if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
3802
3884
  errData.detail = err.message.replace(/^\[public\]/gi, "");
3803
3885
  }
@@ -4020,9 +4102,10 @@ function merge(dest, source) {
4020
4102
  }
4021
4103
 
4022
4104
  // src/packages/controller/utils/params-to-query.ts
4023
- function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4105
+ function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
4024
4106
  const relationships = entries(model.relationships);
4025
4107
  const includedFields = omit(fields, model.resourceName);
4108
+ const included = include && include.map((path) => path.split(".")[0]);
4026
4109
  let query = {
4027
4110
  id,
4028
4111
  filter,
@@ -4065,9 +4148,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4065
4148
  if (!value.includes(relationship.model.primaryKey)) {
4066
4149
  value = [relationship.model.primaryKey, ...value];
4067
4150
  }
4068
- if (include && value.length === 1 && include.includes(name)) {
4069
- value = [...value, ...relationship.model.serializer.attributes];
4070
- } else if (!include && value.length > 1) {
4151
+ if (included && value.length === 1 && included.includes(name)) {
4152
+ value = [...value, ...serializerFor(relationship.model).attributes];
4153
+ } else if (!included && value.length > 1) {
4071
4154
  value = value.slice(0, 1);
4072
4155
  }
4073
4156
  return {
@@ -4082,18 +4165,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4082
4165
  }
4083
4166
 
4084
4167
  // src/packages/controller/utils/find-one.ts
4085
- function findOne(model, req) {
4168
+ function findOne(model, req, serializerFor) {
4086
4169
  const params = merge(req.defaultParams, req.params);
4087
- const { id, select, include } = paramsToQuery(model, params);
4170
+ const { id, select, include } = paramsToQuery(model, params, serializerFor);
4088
4171
  return model.find(id).select(...select).include(include);
4089
4172
  }
4090
4173
 
4091
4174
  // src/packages/controller/utils/find-many.ts
4092
- function findMany(model, req) {
4175
+ function findMany(model, req, serializerFor) {
4093
4176
  const params = merge(req.defaultParams, req.params);
4094
4177
  const { sort, page, limit, select, filter, include } = paramsToQuery(
4095
4178
  model,
4096
- params
4179
+ params,
4180
+ serializerFor
4097
4181
  );
4098
4182
  return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
4099
4183
  }
@@ -4123,6 +4207,53 @@ function resolveRelationships(model, relationships = {}) {
4123
4207
  );
4124
4208
  }
4125
4209
 
4210
+ // src/packages/controller/errors/related-record-not-found-error.ts
4211
+ var RelatedRecordNotFoundError = class extends Error {
4212
+ constructor({ name, primaryKey }, primaryKeyValue, path) {
4213
+ super(
4214
+ `Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
4215
+ );
4216
+ this.source = sourceFor(path);
4217
+ }
4218
+ };
4219
+ var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
4220
+
4221
+ // src/packages/controller/utils/validate-relationships.ts
4222
+ async function validateRelationships(model, relationships = {}) {
4223
+ await Promise.all(
4224
+ entries(relationships).map(async ([key, value]) => {
4225
+ const opts = model.relationshipFor(key);
4226
+ const { data = null } = value || {};
4227
+ if (!opts || !data) {
4228
+ return;
4229
+ }
4230
+ const { model: related } = opts;
4231
+ const isMany = Array.isArray(data);
4232
+ const ids = (isMany ? data : [data]).map(
4233
+ (item) => Reflect.get(Object(item), "id")
4234
+ );
4235
+ if (!ids.length) {
4236
+ return;
4237
+ }
4238
+ const column = related.columnNameFor(related.primaryKey) || "id";
4239
+ const rows = await related.table().whereIn(
4240
+ `${related.tableName}.${column}`,
4241
+ ids.filter((id) => id != null)
4242
+ ).select(`${related.tableName}.${column}`);
4243
+ const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
4244
+ const missing = ids.findIndex((id) => !found.has(String(id)));
4245
+ if (missing >= 0) {
4246
+ const path = `data.relationships.${key}.data`;
4247
+ throw new related_record_not_found_error_default(
4248
+ related,
4249
+ ids[missing],
4250
+ isMany ? `${path}.${missing}` : path
4251
+ );
4252
+ }
4253
+ })
4254
+ );
4255
+ }
4256
+
4126
4257
  // src/packages/controller/constants.ts
4127
4258
  var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
4128
4259
 
@@ -4308,6 +4439,74 @@ var Controller = class {
4308
4439
  * @public
4309
4440
  */
4310
4441
  defaultPerPage = 25;
4442
+ /**
4443
+ * How many relationships deep an `?include` path may go on this
4444
+ * controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
4445
+ * only direct relationships (`comments`) can be included. Paths deeper than
4446
+ * this are rejected with `400 Bad Request`.
4447
+ *
4448
+ * Set it on `ApplicationController` to change it for the whole app, or on a
4449
+ * single controller to override it there.
4450
+ *
4451
+ * ```javascript
4452
+ * class ApplicationController extends Controller {
4453
+ * maxIncludeDepth = 2;
4454
+ * }
4455
+ * ```
4456
+ *
4457
+ * Every allowed path is enumerated up front from the serializers'
4458
+ * relationships, and each nested level costs its own queries per request, so
4459
+ * keep this small.
4460
+ *
4461
+ * @property maxIncludeDepth
4462
+ * @type {Number}
4463
+ * @default 3
4464
+ * @public
4465
+ */
4466
+ maxIncludeDepth = 3;
4467
+ /**
4468
+ * Whether a namespace may fall back to the root Serializer of a type it has
4469
+ * no Serializer for. Read from a namespace's `ApplicationController` and
4470
+ * applies to the whole namespace.
4471
+ *
4472
+ * By default `app/controllers/admin/comments.js` without an
4473
+ * `app/serializers/admin/comments.js` — or an included type without one —
4474
+ * is serialized by the root Serializer, with every attribute and
4475
+ * relationship it declares. For a namespace that must only expose what it
4476
+ * declares itself, turn the fallback off:
4477
+ *
4478
+ * ```javascript
4479
+ * // app/controllers/admin/application.js
4480
+ * class AdminApplicationController extends ApplicationController {
4481
+ * serializerFallback = false;
4482
+ * }
4483
+ * ```
4484
+ *
4485
+ * The application then refuses to boot while any type the namespace can
4486
+ * serialize or `include` (down to each controller's `maxIncludeDepth`) has
4487
+ * no Serializer in that namespace, listing each missing one.
4488
+ *
4489
+ * @property serializerFallback
4490
+ * @type {Boolean}
4491
+ * @default true
4492
+ * @public
4493
+ */
4494
+ serializerFallback = true;
4495
+ /**
4496
+ * The Serializer to serialize (and validate, and load) related resources of
4497
+ * this Controller's responses with: the related model's Serializer in this
4498
+ * Controller's namespace, falling back to the root one.
4499
+ *
4500
+ * Always this Controller's namespace — not its Serializer's, which is the
4501
+ * root one when the namespace has no Serializer for this resource.
4502
+ *
4503
+ * @method serializerFor
4504
+ * @private
4505
+ */
4506
+ serializerFor(model) {
4507
+ const { serializer, namespace } = this;
4508
+ return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
4509
+ }
4311
4510
  constructor({ model, namespace, serializer }) {
4312
4511
  Object.assign(this, {
4313
4512
  model,
@@ -4333,7 +4532,7 @@ var Controller = class {
4333
4532
  * @public
4334
4533
  */
4335
4534
  index(req) {
4336
- return findMany(this.model, req);
4535
+ return findMany(this.model, req, (related) => this.serializerFor(related));
4337
4536
  }
4338
4537
  /**
4339
4538
  * This method supports including relationships, and sparse fieldsets via
@@ -4348,7 +4547,7 @@ var Controller = class {
4348
4547
  * @public
4349
4548
  */
4350
4549
  show(req) {
4351
- return findOne(this.model, req);
4550
+ return findOne(this.model, req, (related) => this.serializerFor(related));
4352
4551
  }
4353
4552
  /**
4354
4553
  * Create and return a single Model instance that the Controller instance
@@ -4369,6 +4568,7 @@ var Controller = class {
4369
4568
  data: { attributes, relationships }
4370
4569
  }
4371
4570
  } = req;
4571
+ await validateRelationships(model, relationships);
4372
4572
  const record = await model.create({
4373
4573
  ...attributes,
4374
4574
  ...resolveRelationships(model, relationships)
@@ -4394,12 +4594,13 @@ var Controller = class {
4394
4594
  */
4395
4595
  update(req) {
4396
4596
  const { model } = this;
4397
- return findOne(model, req).then((record) => {
4597
+ return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
4398
4598
  const {
4399
4599
  params: {
4400
4600
  data: { attributes, relationships }
4401
4601
  }
4402
4602
  } = req;
4603
+ await validateRelationships(model, relationships);
4403
4604
  return record.update({
4404
4605
  ...attributes,
4405
4606
  ...resolveRelationships(model, relationships)
@@ -4423,7 +4624,7 @@ var Controller = class {
4423
4624
  * @public
4424
4625
  */
4425
4626
  destroy(req) {
4426
- return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
4627
+ return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
4427
4628
  }
4428
4629
  /**
4429
4630
  * Respond to HEAD or OPTIONS requests.
@@ -4441,7 +4642,194 @@ var Controller = class {
4441
4642
  var controller_default = Controller;
4442
4643
 
4443
4644
  // src/packages/serializer/index.ts
4444
- import { dasherize as dasherize4 } from "inflection";
4645
+ import { posix as posix2 } from "path";
4646
+ import { dasherize as dasherize5 } from "inflection";
4647
+
4648
+ // src/packages/loader/resolver/utils/closest-ancestor.ts
4649
+ import { posix } from "path";
4650
+ function closestAncestor(source, key) {
4651
+ const name = posix.basename(key);
4652
+ let namespace = posix.dirname(key);
4653
+ if (namespace === ".") {
4654
+ return source.get(name);
4655
+ }
4656
+ namespace = posix.dirname(namespace);
4657
+ const ancestor = source.get(posix.join(namespace, name));
4658
+ if (ancestor) {
4659
+ return ancestor;
4660
+ }
4661
+ return closestAncestor(source, posix.join(posix.dirname(namespace), name));
4662
+ }
4663
+
4664
+ // src/packages/serializer/utils/load-linkage.ts
4665
+ import { camelize as camelize9 } from "inflection";
4666
+ var valueOf = (record, key) => Reflect.get(record, key);
4667
+ var toId = (value) => value == null ? null : String(value);
4668
+ async function loadLinkage(model, records, names) {
4669
+ const ids = Array.from(
4670
+ new Set(records.map((record) => record.getPrimaryKey()))
4671
+ );
4672
+ const linkage = /* @__PURE__ */ new Map();
4673
+ const belongsTo = [];
4674
+ const queries = [];
4675
+ ids.forEach((id) => {
4676
+ linkage.set(
4677
+ String(id),
4678
+ names.reduce((result, name) => {
4679
+ const opts = model.relationshipFor(name);
4680
+ result[name] = opts && opts.type === "hasMany" ? [] : null;
4681
+ return result;
4682
+ }, {})
4683
+ );
4684
+ });
4685
+ if (!ids.length) {
4686
+ return linkage;
4687
+ }
4688
+ const link = (ownerId, name, relatedId) => {
4689
+ const owner = linkage.get(String(ownerId));
4690
+ const id = toId(relatedId);
4691
+ if (!owner || id === null) {
4692
+ return;
4693
+ }
4694
+ const current = owner[name];
4695
+ if (Array.isArray(current)) {
4696
+ if (!current.includes(id)) {
4697
+ current.push(id);
4698
+ }
4699
+ } else if (current === null) {
4700
+ owner[name] = id;
4701
+ }
4702
+ };
4703
+ names.forEach((name) => {
4704
+ const opts = model.relationshipFor(name);
4705
+ if (!opts) {
4706
+ return;
4707
+ }
4708
+ const { type, through, model: related } = opts;
4709
+ const foreignKey = camelize9(opts.foreignKey, true);
4710
+ if (type === "belongsTo") {
4711
+ belongsTo.push([name, foreignKey, related]);
4712
+ } else if (through) {
4713
+ const inverse = related.relationshipFor(opts.inverse);
4714
+ if (!inverse) {
4715
+ return;
4716
+ }
4717
+ const relatedKey = camelize9(inverse.foreignKey, true);
4718
+ queries.push(
4719
+ (async () => {
4720
+ const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
4721
+ rows.forEach((row) => {
4722
+ link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
4723
+ });
4724
+ })()
4725
+ );
4726
+ } else {
4727
+ queries.push(
4728
+ (async () => {
4729
+ const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
4730
+ rows.forEach((row) => {
4731
+ link(valueOf(row, foreignKey), name, row.getPrimaryKey());
4732
+ });
4733
+ })()
4734
+ );
4735
+ }
4736
+ });
4737
+ if (belongsTo.length) {
4738
+ queries.push(
4739
+ (async () => {
4740
+ const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
4741
+ rows.forEach((row) => {
4742
+ belongsTo.forEach(([name, key]) => {
4743
+ link(row.getPrimaryKey(), name, valueOf(row, key));
4744
+ });
4745
+ });
4746
+ await dropDangling(linkage, belongsTo);
4747
+ })()
4748
+ );
4749
+ }
4750
+ await Promise.all(queries);
4751
+ return linkage;
4752
+ }
4753
+ async function dropDangling(linkage, belongsTo) {
4754
+ const wanted = /* @__PURE__ */ new Map();
4755
+ belongsTo.forEach(([name, , related]) => {
4756
+ const ids = wanted.get(related) || /* @__PURE__ */ new Set();
4757
+ linkage.forEach(({ [name]: id }) => {
4758
+ if (typeof id === "string") {
4759
+ ids.add(id);
4760
+ }
4761
+ });
4762
+ wanted.set(related, ids);
4763
+ });
4764
+ const existing = /* @__PURE__ */ new Map();
4765
+ await Promise.all(
4766
+ Array.from(wanted, async ([related, ids]) => {
4767
+ if (!ids.size) {
4768
+ existing.set(related, ids);
4769
+ return;
4770
+ }
4771
+ const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
4772
+ existing.set(
4773
+ related,
4774
+ new Set(rows.map((row) => String(row.getPrimaryKey())))
4775
+ );
4776
+ })
4777
+ );
4778
+ belongsTo.forEach(([name, , related]) => {
4779
+ const found = existing.get(related);
4780
+ linkage.forEach((owner) => {
4781
+ const id = owner[name];
4782
+ if (typeof id === "string" && !found?.has(id)) {
4783
+ owner[name] = null;
4784
+ }
4785
+ });
4786
+ });
4787
+ }
4788
+
4789
+ // src/packages/serializer/utils/include-tree.ts
4790
+ function createIncludeTree(paths = []) {
4791
+ const tree = /* @__PURE__ */ new Map();
4792
+ paths.forEach((path) => {
4793
+ path.split(".").filter(Boolean).reduce((node, name) => {
4794
+ let child = node.get(name);
4795
+ if (!child) {
4796
+ child = /* @__PURE__ */ new Map();
4797
+ node.set(name, child);
4798
+ }
4799
+ return child;
4800
+ }, tree);
4801
+ });
4802
+ return tree;
4803
+ }
4804
+ function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
4805
+ if (depth < 1) {
4806
+ return [];
4807
+ }
4808
+ return names.reduce((paths, name) => {
4809
+ const opts = model.relationshipFor(name);
4810
+ if (!opts) {
4811
+ return paths;
4812
+ }
4813
+ const { model: related } = opts;
4814
+ const serializer = serializerFor(related);
4815
+ if (!serializer) {
4816
+ return [...paths, name];
4817
+ }
4818
+ const { hasOne, hasMany } = serializer;
4819
+ return [
4820
+ ...paths,
4821
+ name,
4822
+ ...enumerateIncludePaths(
4823
+ related,
4824
+ [...hasOne, ...hasMany],
4825
+ depth - 1,
4826
+ serializerFor
4827
+ ).map((path) => `${name}.${path}`)
4828
+ ];
4829
+ }, []);
4830
+ }
4831
+
4832
+ // src/packages/serializer/index.ts
4445
4833
  var Serializer = class {
4446
4834
  /**
4447
4835
  * An Array of the `hasOne` or `belongsTo` relationships on a Serializer
@@ -4528,9 +4916,17 @@ var Serializer = class {
4528
4916
  * the resource and relationship objects in the returned [JSON API](
4529
4917
  * http://jsonapi.org) document object.
4530
4918
  *
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.
4919
+ * @param {Array} options.include - An array of relationship paths (e.g.
4920
+ * `'comments'` or `'comments.user'`) whose resources should be added to the
4921
+ * top level included object of the returned [JSON API](http://jsonapi.org)
4922
+ * document object. Intermediate resources of a nested path are included too.
4923
+ *
4924
+ * @param {String} options.namespace - The namespace of the request, i.e. of
4925
+ * the Controller handling it. Every link in the document is built in it, and
4926
+ * included resources are serialized by their Serializer in it (falling back
4927
+ * to the root). Defaults to this Serializer's namespace — which is the root
4928
+ * one when a namespaced Controller has no Serializer of its own, so the
4929
+ * Controller passes its namespace explicitly.
4534
4930
  *
4535
4931
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
4536
4932
  * object.
@@ -4541,38 +4937,61 @@ var Serializer = class {
4541
4937
  data,
4542
4938
  links,
4543
4939
  domain,
4544
- include
4940
+ include,
4941
+ namespace = this.namespace
4545
4942
  }) {
4546
- let serialized = {};
4547
- const included = [];
4943
+ const tree = createIncludeTree(include);
4944
+ const related = /* @__PURE__ */ new Map();
4945
+ const included = /* @__PURE__ */ new Map();
4946
+ const names = Array.from(tree.keys());
4947
+ let primary;
4948
+ let serialized;
4548
4949
  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
- )
4950
+ primary = await Promise.all(
4951
+ data.map(
4952
+ (item) => this.formatOne({
4953
+ item,
4954
+ domain,
4955
+ related,
4956
+ namespace,
4957
+ include: names
4958
+ })
4559
4959
  )
4560
- };
4960
+ );
4961
+ serialized = { data: primary };
4561
4962
  } else {
4562
- serialized = {
4563
- data: await this.formatOne({
4963
+ const resource2 = await this.formatOne({
4964
+ domain,
4965
+ related,
4966
+ namespace,
4967
+ item: data,
4968
+ include: names,
4969
+ links: false
4970
+ });
4971
+ primary = [resource2];
4972
+ serialized = { data: resource2 };
4973
+ }
4974
+ for (const [name, children] of tree) {
4975
+ const opts = this.model.relationshipFor(name);
4976
+ const records = related.get(name);
4977
+ if (opts && records) {
4978
+ await this.addIncluded({
4564
4979
  domain,
4565
- include,
4980
+ records,
4566
4981
  included,
4567
- item: data,
4568
- links: false
4569
- })
4570
- };
4982
+ namespace,
4983
+ model: opts.model,
4984
+ tree: children
4985
+ });
4986
+ }
4571
4987
  }
4572
- if (included.length) {
4988
+ primary.forEach((resource2) => {
4989
+ included.delete(resourceKey(resource2));
4990
+ });
4991
+ if (included.size) {
4573
4992
  serialized = {
4574
4993
  ...serialized,
4575
- included: uniq(included, "id", "type")
4994
+ included: Array.from(included.values())
4576
4995
  };
4577
4996
  }
4578
4997
  return {
@@ -4587,6 +5006,12 @@ var Serializer = class {
4587
5006
  * Transform a single Model instance into a [JSON API](http://jsonapi.org)
4588
5007
  * resource object.
4589
5008
  *
5009
+ * Relationships are serialized in one of two ways. By default each one is
5010
+ * read from the Model instance (for primary data the query has already
5011
+ * loaded them). When `linkage` is given — as it is for included resources,
5012
+ * whose relationships are batch-loaded by `addIncluded()` — the resource
5013
+ * linkage is built from it instead, without touching the database.
5014
+ *
4590
5015
  * @method formatOne
4591
5016
  *
4592
5017
  * @param {Object} options - An options object used for building the returned
@@ -4603,17 +5028,19 @@ var Serializer = class {
4603
5028
  * the top level links object or relationship links objects in the returned
4604
5029
  * [JSON API](http://jsonapi.org) resource object.
4605
5030
  *
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.
5031
+ * @param {Array} options.include - An array of the relationship keys whose
5032
+ * related records should be collected into `options.related`.
4609
5033
  *
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.
5034
+ * @param {Map} options.related - Collects, per relationship key in
5035
+ * `options.include`, the related Model instances that belong in the top
5036
+ * level included object of a [JSON API](http://jsonapi.org) document object.
4613
5037
  *
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.
5038
+ * @param {Object} options.linkage - Pre-loaded resource linkage (related
5039
+ * primary keys per relationship key) to serialize relationships from.
5040
+ *
5041
+ * @param {String} options.namespace - The namespace to build links in.
5042
+ * Defaults to this Serializer's; included resources pass the namespace of
5043
+ * the request, so every link in a document points into the same namespace.
4617
5044
  *
4618
5045
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
4619
5046
  * object.
@@ -4624,13 +5051,15 @@ var Serializer = class {
4624
5051
  item,
4625
5052
  links,
4626
5053
  domain,
4627
- include,
4628
- included,
4629
- formatRelationships = true
5054
+ include = [],
5055
+ related,
5056
+ linkage,
5057
+ namespace = this.namespace
4630
5058
  }) {
4631
5059
  const { resourceName: type } = item;
4632
5060
  const id = String(item.getPrimaryKey());
4633
- let relationships = {};
5061
+ const names = [...this.hasOne, ...this.hasMany];
5062
+ let relationships;
4634
5063
  const attributes = dasherizeKeys(
4635
5064
  item.getAttributes(
4636
5065
  ...Object.keys(item.rawColumnData).filter(
@@ -4643,34 +5072,41 @@ var Serializer = class {
4643
5072
  type,
4644
5073
  attributes
4645
5074
  };
4646
- if (formatRelationships) {
5075
+ if (linkage) {
5076
+ relationships = names.reduce(
5077
+ (hash, name) => ({
5078
+ ...hash,
5079
+ [dasherize5(underscore(name))]: this.formatLinkage(
5080
+ domain,
5081
+ this.model.relationshipFor(name)?.model.resourceName,
5082
+ linkage[name],
5083
+ namespace
5084
+ )
5085
+ }),
5086
+ {}
5087
+ );
5088
+ } else {
5089
+ const collect = (name, records) => {
5090
+ if (related && include.includes(name)) {
5091
+ related.set(name, [...related.get(name) || [], ...records]);
5092
+ }
5093
+ };
4647
5094
  relationships = await promiseHash(
4648
- [...this.hasOne, ...this.hasMany].reduce(
5095
+ names.reduce(
4649
5096
  (hash, name) => ({
4650
5097
  ...hash,
4651
- [dasherize4(underscore(name))]: (async () => {
4652
- const related = await Reflect.get(item, name);
4653
- if (Array.isArray(related)) {
5098
+ [dasherize5(underscore(name))]: (async () => {
5099
+ const value = await Reflect.get(item, name);
5100
+ if (Array.isArray(value)) {
5101
+ collect(name, value);
4654
5102
  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
- })
5103
+ data: value.map(
5104
+ (relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
4665
5105
  )
4666
5106
  };
4667
- } else if (related && related.id != null) {
4668
- return this.formatRelationship({
4669
- domain,
4670
- included,
4671
- item: related,
4672
- include: include.includes(name)
4673
- });
5107
+ } else if (value && value.id != null) {
5108
+ collect(name, [value]);
5109
+ return this.formatRelationship(value, domain, namespace);
4674
5110
  }
4675
5111
  return {
4676
5112
  data: null
@@ -4685,16 +5121,9 @@ var Serializer = class {
4685
5121
  serialized.relationships = relationships;
4686
5122
  }
4687
5123
  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
- }
5124
+ serialized.links = {
5125
+ self: this.linkFor(domain, type, id, namespace)
5126
+ };
4698
5127
  }
4699
5128
  return serialized;
4700
5129
  }
@@ -4704,70 +5133,167 @@ var Serializer = class {
4704
5133
  *
4705
5134
  * @method formatRelationship
4706
5135
  *
4707
- * @param {Object} options - An options object used for building the returned
5136
+ * @param {Model} item - The Model instance to transform into the returned
4708
5137
  * [JSON API](http://jsonapi.org) relationship object.
4709
5138
  *
4710
- * @param {Model} options.item - The Model instance to transform into the
5139
+ * @param {String} domain - A string used to build links included in the
4711
5140
  * returned [JSON API](http://jsonapi.org) relationship object.
4712
5141
  *
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.
4719
- *
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.
5142
+ * @return {Object} A [JSON API](http://jsonapi.org) relationship object.
4723
5143
  *
4724
- * @return {Promise} Resolves with a [JSON API](http://jsonapi.org)
4725
- * relationship object.
5144
+ * @private
5145
+ */
5146
+ formatRelationship(item, domain, namespace = this.namespace) {
5147
+ return this.formatLinkage(
5148
+ domain,
5149
+ item.resourceName,
5150
+ String(item.getPrimaryKey()),
5151
+ namespace
5152
+ );
5153
+ }
5154
+ /**
5155
+ * Build a [JSON API](http://jsonapi.org) relationship object from resource
5156
+ * linkage, in the same shape `formatRelationship()` produces from Model
5157
+ * instances: to-one relationships carry a `links` object, to-many ones only
5158
+ * `data`, and a missing to-one relationship is `{ data: null }`.
4726
5159
  *
5160
+ * @method formatLinkage
4727
5161
  * @private
4728
5162
  */
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}`
5163
+ formatLinkage(domain, type, linkage, namespace = this.namespace) {
5164
+ if (Array.isArray(linkage)) {
5165
+ return {
5166
+ data: type ? linkage.map((id) => ({ id, type })) : []
4749
5167
  };
4750
5168
  }
4751
- if (include) {
4752
- included.push(
4753
- await serializer.formatOne({
4754
- item,
4755
- domain,
4756
- include: [],
4757
- included: [],
4758
- formatRelationships: false
4759
- })
4760
- );
5169
+ if (linkage == null || !type) {
5170
+ return {
5171
+ data: null
5172
+ };
4761
5173
  }
4762
5174
  return {
4763
5175
  data: {
4764
- id,
5176
+ id: linkage,
4765
5177
  type
4766
5178
  },
4767
- links
5179
+ links: {
5180
+ self: this.linkFor(domain, type, linkage, namespace)
5181
+ }
4768
5182
  };
4769
5183
  }
5184
+ /**
5185
+ * Add `records` (instances of `model`) to `included` as resource objects,
5186
+ * then recurse into the relationships named in `tree`. Each is serialized by
5187
+ * `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
5188
+ * `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
5189
+ * is one and `CommentsSerializer` otherwise. The relationships of every level
5190
+ * are batch-loaded with one query per relationship, not one per record.
5191
+ *
5192
+ * @method addIncluded
5193
+ * @private
5194
+ */
5195
+ async addIncluded({
5196
+ model,
5197
+ records,
5198
+ tree,
5199
+ domain,
5200
+ included,
5201
+ namespace
5202
+ }) {
5203
+ const serializer = this.serializerFor(model, namespace);
5204
+ const unique = Array.from(
5205
+ new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
5206
+ );
5207
+ if (!unique.length) {
5208
+ return;
5209
+ }
5210
+ const names = [...serializer.hasOne, ...serializer.hasMany];
5211
+ const linkage = await loadLinkage(model, unique, names);
5212
+ for (const item of unique) {
5213
+ const id = String(item.getPrimaryKey());
5214
+ const key = resourceKey({ id, type: item.resourceName });
5215
+ if (!included.has(key)) {
5216
+ included.set(
5217
+ key,
5218
+ await serializer.formatOne({
5219
+ item,
5220
+ domain,
5221
+ linkage: linkage.get(id),
5222
+ namespace
5223
+ })
5224
+ );
5225
+ }
5226
+ }
5227
+ for (const [name, children] of tree) {
5228
+ const opts = model.relationshipFor(name);
5229
+ if (!opts || !names.includes(name)) {
5230
+ continue;
5231
+ }
5232
+ const { model: next } = opts;
5233
+ const ids = /* @__PURE__ */ new Set();
5234
+ linkage.forEach(({ [name]: value }) => {
5235
+ (Array.isArray(value) ? value : [value]).forEach((relatedId) => {
5236
+ if (relatedId != null) {
5237
+ ids.add(relatedId);
5238
+ }
5239
+ });
5240
+ });
5241
+ if (ids.size) {
5242
+ const attributes = this.serializerFor(
5243
+ next,
5244
+ namespace
5245
+ ).attributes.filter((attr) => next.attributeNames.includes(attr));
5246
+ const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
5247
+ await this.addIncluded({
5248
+ domain,
5249
+ included,
5250
+ namespace,
5251
+ model: next,
5252
+ records: nextRecords,
5253
+ tree: children
5254
+ });
5255
+ }
5256
+ }
5257
+ }
5258
+ /**
5259
+ * Resolve the Serializer for `model` in `namespace` (this Serializer's by
5260
+ * default), the way a namespaced Controller resolves its own:
5261
+ * `admin/comments` if it exists, otherwise the closest ancestor namespace's,
5262
+ * down to the root `comments` Serializer. Falls back to `model.serializer`
5263
+ * when this Serializer was not created by an application (e.g. in
5264
+ * isolation).
5265
+ *
5266
+ * Pass the request's namespace when there is one: a Serializer's own
5267
+ * namespace is the root one whenever it is a namespaced Controller's
5268
+ * fallback.
5269
+ *
5270
+ * @method serializerFor
5271
+ * @private
5272
+ */
5273
+ serializerFor(model, namespace = this.namespace) {
5274
+ const { serializers } = this;
5275
+ if (serializers) {
5276
+ const key = posix2.join(namespace || ".", model.resourceName);
5277
+ const serializer = serializers.get(key) || closestAncestor(serializers, key);
5278
+ if (serializer) {
5279
+ return serializer;
5280
+ }
5281
+ }
5282
+ return model.serializer;
5283
+ }
5284
+ /**
5285
+ * @private
5286
+ */
5287
+ linkFor(domain, type, id, namespace = this.namespace) {
5288
+ if (namespace) {
5289
+ return `${domain}/${namespace}/${type}/${id}`;
5290
+ }
5291
+ return `${domain}/${type}/${id}`;
5292
+ }
4770
5293
  };
5294
+ function resourceKey({ id, type }) {
5295
+ return `${type}:${id}`;
5296
+ }
4771
5297
  var serializer_default = Serializer;
4772
5298
 
4773
5299
  // src/packages/config/index.ts
@@ -4966,7 +5492,10 @@ function resource(action) {
4966
5492
  data,
4967
5493
  links,
4968
5494
  domain,
4969
- include
5495
+ include,
5496
+ // The request's namespace, not the serializer's: a namespaced
5497
+ // controller without its own serializer is given the root one.
5498
+ namespace
4970
5499
  });
4971
5500
  }
4972
5501
  return data;
@@ -5047,6 +5576,7 @@ var ParameterTypeError = class extends TypeError {
5047
5576
  Expected type '${type || "undefined"}' for parameter '${path}' but got
5048
5577
  '${actual}'.
5049
5578
  `);
5579
+ this.source = sourceFor(path);
5050
5580
  }
5051
5581
  };
5052
5582
  var parameter_type_error_default = createServerError(ParameterTypeError, 400);
@@ -5059,6 +5589,7 @@ var ParameterValueError = class extends TypeError {
5059
5589
  [${param.size ? Array.from(param.values()).join(", ") : ""}] but got
5060
5590
  ${String(actual)}.
5061
5591
  `);
5592
+ this.source = sourceFor(param.path);
5062
5593
  }
5063
5594
  };
5064
5595
  var parameter_value_error_default = createServerError(ParameterValueError, 400);
@@ -5067,6 +5598,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
5067
5598
  var InvalidParameterError = class extends TypeError {
5068
5599
  constructor(path) {
5069
5600
  super(`'${path}' is not a valid parameter for this resource.`);
5601
+ this.source = sourceFor(path);
5070
5602
  }
5071
5603
  };
5072
5604
  var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
@@ -5082,6 +5614,7 @@ var ResourceMismatchError = class extends TypeError {
5082
5614
  Expected '${String(expected)}' for parameter '${path}' but got
5083
5615
  ${String(normalized)}.
5084
5616
  `);
5617
+ this.source = sourceFor(path);
5085
5618
  }
5086
5619
  };
5087
5620
  var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
@@ -5090,14 +5623,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
5090
5623
  var ParameterRequiredError = class extends TypeError {
5091
5624
  constructor(path) {
5092
5625
  super(`Missing required parameter '${path}'.`);
5626
+ this.source = sourceFor(path);
5093
5627
  }
5094
5628
  };
5095
5629
  var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
5096
5630
 
5631
+ // src/packages/router/route/params/errors/client-generated-id-error.ts
5632
+ var ClientGeneratedIdError = class extends TypeError {
5633
+ constructor() {
5634
+ super("Client-generated IDs are not supported for this resource.");
5635
+ this.source = sourceFor("data.id");
5636
+ }
5637
+ };
5638
+ var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
5639
+
5640
+ // src/packages/router/route/params/errors/forbidden-parameter-error.ts
5641
+ var ForbiddenParameterError = class extends TypeError {
5642
+ constructor(path) {
5643
+ super(`Setting '${path}' is not supported for this resource.`);
5644
+ this.source = sourceFor(path);
5645
+ }
5646
+ };
5647
+ var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
5648
+
5097
5649
  // src/packages/router/route/params/errors/parameter-not-nullable-error.ts
5098
5650
  var ParameterNotNullableError = class extends TypeError {
5099
5651
  constructor({ path }) {
5100
5652
  super(`Parameter '${path}' is not nullable.`);
5653
+ this.source = sourceFor(path);
5101
5654
  }
5102
5655
  };
5103
5656
  var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
@@ -5226,13 +5779,14 @@ var Parameter = class extends set_default {
5226
5779
  path,
5227
5780
  type,
5228
5781
  required: Boolean(required),
5229
- sanitize: Boolean(sanitize)
5782
+ sanitize: Boolean(sanitize),
5783
+ restricted: values !== void 0
5230
5784
  });
5231
5785
  this.freeze();
5232
5786
  }
5233
5787
  validate(value) {
5234
5788
  validateType2(this, value);
5235
- if (this.size > 0) {
5789
+ if (this.restricted) {
5236
5790
  return validateValue(this, value);
5237
5791
  }
5238
5792
  return value;
@@ -5251,6 +5805,17 @@ function getURLParams(dynamicSegments) {
5251
5805
  ]);
5252
5806
  }
5253
5807
 
5808
+ // src/packages/router/route/params/parameter/forbidden-parameter.ts
5809
+ var ForbiddenParameter = class extends parameter_default {
5810
+ constructor(path) {
5811
+ super({ path });
5812
+ }
5813
+ validate() {
5814
+ throw new forbidden_parameter_error_default(this.path);
5815
+ }
5816
+ };
5817
+ var forbidden_parameter_default = ForbiddenParameter;
5818
+
5254
5819
  // src/packages/router/route/params/utils/get-data-params.ts
5255
5820
  function getIDParam({ model }) {
5256
5821
  const primaryKeyColumn = model.columnFor(model.primaryKey);
@@ -5303,16 +5868,48 @@ function getRelationshipsParam({
5303
5868
  model,
5304
5869
  params
5305
5870
  }) {
5871
+ const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
5872
+ key,
5873
+ new forbidden_parameter_default(`data.relationships.${key}`)
5874
+ ]);
5306
5875
  return [
5307
5876
  "relationships",
5308
5877
  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") {
5878
+ [
5879
+ ...params.reduce((group, param) => {
5880
+ const path = `data.relationships.${param}`;
5881
+ const opts = model.relationshipFor(param);
5882
+ if (!opts) {
5883
+ return group;
5884
+ }
5885
+ if (opts.type === "hasMany") {
5886
+ return [
5887
+ ...group,
5888
+ [
5889
+ param,
5890
+ new parameter_group_default(
5891
+ [
5892
+ [
5893
+ "data",
5894
+ new parameter_default({
5895
+ type: "array",
5896
+ path: `${path}.data`,
5897
+ required: true
5898
+ })
5899
+ ]
5900
+ ],
5901
+ {
5902
+ path
5903
+ }
5904
+ )
5905
+ ]
5906
+ ];
5907
+ }
5908
+ const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5909
+ let primaryKeyType = "number";
5910
+ if (primaryKeyColumn) {
5911
+ primaryKeyType = typeForColumn(primaryKeyColumn);
5912
+ }
5316
5913
  return [
5317
5914
  ...group,
5318
5915
  [
@@ -5321,11 +5918,32 @@ function getRelationshipsParam({
5321
5918
  [
5322
5919
  [
5323
5920
  "data",
5324
- new parameter_default({
5325
- type: "array",
5326
- path: `${path}.data`,
5327
- required: true
5328
- })
5921
+ new parameter_group_default(
5922
+ [
5923
+ [
5924
+ "id",
5925
+ new parameter_default({
5926
+ type: primaryKeyType,
5927
+ path: `${path}.data.id`,
5928
+ required: true
5929
+ })
5930
+ ],
5931
+ [
5932
+ "type",
5933
+ new parameter_default({
5934
+ type: "string",
5935
+ path: `${path}.data.type`,
5936
+ values: [opts.model.resourceName],
5937
+ required: true
5938
+ })
5939
+ ]
5940
+ ],
5941
+ {
5942
+ type: "array",
5943
+ path: `${path}.data`,
5944
+ required: true
5945
+ }
5946
+ )
5329
5947
  ]
5330
5948
  ],
5331
5949
  {
@@ -5334,55 +5952,9 @@ function getRelationshipsParam({
5334
5952
  )
5335
5953
  ]
5336
5954
  ];
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
- }, []),
5955
+ }, []),
5956
+ ...forbidden
5957
+ ],
5386
5958
  {
5387
5959
  path: "data.relationships"
5388
5960
  }
@@ -5490,10 +6062,9 @@ function getFilterParam({ filter }) {
5490
6062
  )
5491
6063
  ];
5492
6064
  }
5493
- function getFieldsParam({
5494
- model,
5495
- serializer: { hasOne, hasMany, attributes }
5496
- }) {
6065
+ function getFieldsParam(controller) {
6066
+ const { model, serializer } = controller;
6067
+ const { hasOne, hasMany, attributes } = serializer;
5497
6068
  const relationships = [...hasOne, ...hasMany];
5498
6069
  return [
5499
6070
  "fields",
@@ -5522,7 +6093,7 @@ function getFieldsParam({
5522
6093
  sanitize: true,
5523
6094
  values: [
5524
6095
  opts.model.primaryKey,
5525
- ...opts.model.serializer.attributes
6096
+ ...controller.serializerFor(opts.model).attributes
5526
6097
  ]
5527
6098
  })
5528
6099
  ]
@@ -5540,16 +6111,30 @@ function getFieldsParam({
5540
6111
  )
5541
6112
  ];
5542
6113
  }
5543
- function getIncludeParam({
5544
- serializer: { hasOne, hasMany }
5545
- }) {
6114
+ function getIncludeParam(controller) {
6115
+ const { model, maxIncludeDepth, serializer } = controller;
6116
+ const { hasOne, hasMany } = serializer;
5546
6117
  const relationships = [...hasOne, ...hasMany];
5547
6118
  return [
5548
6119
  "include",
5549
6120
  new parameter_default({
5550
6121
  path: "include",
5551
6122
  type: "array",
5552
- values: relationships
6123
+ // Every top level name stays allowed (as before), plus the nested paths
6124
+ // (`comments.user`) reachable through each related serializer in the
6125
+ // controller's namespace (at every level, even below a root fallback
6126
+ // serializer), down to the controller's `maxIncludeDepth`.
6127
+ values: Array.from(
6128
+ /* @__PURE__ */ new Set([
6129
+ ...relationships,
6130
+ ...enumerateIncludePaths(
6131
+ model,
6132
+ relationships,
6133
+ maxIncludeDepth,
6134
+ (related) => controller.serializerFor(related)
6135
+ )
6136
+ ])
6137
+ )
5553
6138
  })
5554
6139
  ];
5555
6140
  }
@@ -5587,6 +6172,15 @@ function getCollectionQueryParams(controller) {
5587
6172
  return getCustomParams(controller);
5588
6173
  }
5589
6174
 
6175
+ // src/packages/router/route/params/utils/validate-client-id.ts
6176
+ function validateClientId(params) {
6177
+ const { data } = params;
6178
+ if (data && typeof data === "object" && Reflect.has(data, "id")) {
6179
+ throw new client_generated_id_error_default();
6180
+ }
6181
+ return true;
6182
+ }
6183
+
5590
6184
  // src/packages/router/route/params/utils/validate-resource-id.ts
5591
6185
  function validateResourceId({
5592
6186
  params: {
@@ -5752,6 +6346,9 @@ var Route = class extends set_default {
5752
6346
  ...req.params,
5753
6347
  ...this.parseParams(req.url.params)
5754
6348
  };
6349
+ if (this.action === "create" && req.method === "POST") {
6350
+ validateClientId(params);
6351
+ }
5755
6352
  if (req.method !== "OPTIONS") {
5756
6353
  params = this.params.validate(params);
5757
6354
  }
@@ -6067,7 +6664,7 @@ var router_default = Router;
6067
6664
  import { join as joinPath2 } from "path";
6068
6665
 
6069
6666
  // src/packages/loader/utils/format-key.ts
6070
- import { dasherize as dasherize5 } from "inflection";
6667
+ import { dasherize as dasherize6 } from "inflection";
6071
6668
  var NAMESPACE_DELIMITER = /\$-/g;
6072
6669
  function formatKey(key, formatter) {
6073
6670
  return chain(key).pipe((str) => {
@@ -6075,7 +6672,7 @@ function formatKey(key, formatter) {
6075
6672
  return formatter(str);
6076
6673
  }
6077
6674
  return str;
6078
- }).pipe(underscore).pipe(dasherize5).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6675
+ }).pipe(underscore).pipe(dasherize6).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6079
6676
  }
6080
6677
 
6081
6678
  // src/packages/loader/utils/bundle-for.ts
@@ -6147,26 +6744,10 @@ function bundleFor(path) {
6147
6744
  return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
6148
6745
  }
6149
6746
 
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
6747
  // src/packages/loader/resolver/utils/closest-child.ts
6167
- import { posix as posix2 } from "path";
6748
+ import { posix as posix3 } from "path";
6168
6749
  function closestChild(source, key) {
6169
- const [[, result] = []] = Array.from(source).map(([path, value]) => [posix2.basename(path), value]).filter(([resource2]) => key === resource2);
6750
+ const [[, result] = []] = Array.from(source).map(([path, value]) => [posix3.basename(path), value]).filter(([resource2]) => key === resource2);
6170
6751
  return result;
6171
6752
  }
6172
6753
 
@@ -6189,7 +6770,7 @@ function resolve(group) {
6189
6770
  }
6190
6771
 
6191
6772
  // src/packages/loader/builder/utils/create-parent-builder.ts
6192
- import { posix as posix3 } from "path";
6773
+ import { posix as posix4 } from "path";
6193
6774
 
6194
6775
  // src/packages/loader/builder/utils/sort-by-namespace.ts
6195
6776
  function sortByNamespace([a], [b]) {
@@ -6210,7 +6791,7 @@ function createParentBuilder(construct) {
6210
6791
  let grandparent = null;
6211
6792
  if (key !== "root") {
6212
6793
  const found = result.find((namespace) => {
6213
- const dirname = posix3.dirname(key);
6794
+ const dirname = posix4.dirname(key);
6214
6795
  if (namespace.key === "root") {
6215
6796
  return dirname === ".";
6216
6797
  }
@@ -6280,13 +6861,13 @@ function normalizePort(port) {
6280
6861
  }
6281
6862
 
6282
6863
  // src/packages/application/utils/create-controller.ts
6283
- import { posix as posix4 } from "path";
6864
+ import { posix as posix5 } from "path";
6284
6865
  function createController(constructor, opts) {
6285
6866
  const { key, store, serializers } = opts;
6286
- const namespace = posix4.dirname(key).replace(".", "");
6867
+ const namespace = posix5.dirname(key).replace(".", "");
6287
6868
  let { parent } = opts;
6288
6869
  let model = tryCatchSync(
6289
- () => store.modelFor(posix4.basename(key))
6870
+ () => store.modelFor(posix5.basename(key))
6290
6871
  );
6291
6872
  let serializer = serializers.get(key);
6292
6873
  if (!model) {
@@ -6342,13 +6923,13 @@ function createController(constructor, opts) {
6342
6923
  }
6343
6924
 
6344
6925
  // src/packages/application/utils/create-serializer.ts
6345
- import { posix as posix5 } from "path";
6926
+ import { posix as posix6 } from "path";
6346
6927
  function createSerializer(constructor, opts) {
6347
6928
  const { key, store } = opts;
6348
- const namespace = posix5.dirname(key).replace(".", "");
6929
+ const namespace = posix6.dirname(key).replace(".", "");
6349
6930
  let { parent } = opts;
6350
6931
  let model = tryCatchSync(
6351
- () => store.modelFor(posix5.basename(key))
6932
+ () => store.modelFor(posix6.basename(key))
6352
6933
  );
6353
6934
  if (!model) {
6354
6935
  model = null;
@@ -6372,6 +6953,85 @@ function createSerializer(constructor, opts) {
6372
6953
  return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
6373
6954
  }
6374
6955
 
6956
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6957
+ import { posix as posix7 } from "path";
6958
+
6959
+ // src/errors/namespaced-serializer-missing-error.ts
6960
+ var NamespacedSerializerMissingError = class extends ReferenceError {
6961
+ /**
6962
+ * @param missing - Each missing serializer key (`admin/comments`) with an
6963
+ * example of how it is reached (`admin/posts?include=comments`).
6964
+ */
6965
+ constructor(missing) {
6966
+ const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
6967
+ super(
6968
+ [
6969
+ "Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
6970
+ ...lines
6971
+ ].join("\n")
6972
+ );
6973
+ }
6974
+ };
6975
+ var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
6976
+
6977
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6978
+ var distance = (via) => {
6979
+ const [, include] = via.split("?include=");
6980
+ return include ? include.split(".").length : 0;
6981
+ };
6982
+ function validateNamespacedSerializers(controllers, serializers) {
6983
+ const missing = /* @__PURE__ */ new Map();
6984
+ controllers.forEach((controller, key) => {
6985
+ const { namespace, model } = controller;
6986
+ if (!namespace || !model) {
6987
+ return;
6988
+ }
6989
+ const application = controllers.get(posix7.join(namespace, "application"));
6990
+ if (!application || application.serializerFallback !== false) {
6991
+ return;
6992
+ }
6993
+ const require2 = (type, via) => {
6994
+ const serializerKey = posix7.join(namespace, type);
6995
+ const serializer = serializers.get(serializerKey);
6996
+ if (!serializer) {
6997
+ const known = missing.get(serializerKey);
6998
+ if (known === void 0 || distance(via) < distance(known)) {
6999
+ missing.set(serializerKey, via);
7000
+ }
7001
+ }
7002
+ return serializer;
7003
+ };
7004
+ const own = require2(model.resourceName, key);
7005
+ if (!own) {
7006
+ return;
7007
+ }
7008
+ const depth = Math.max(controller.maxIncludeDepth, 1);
7009
+ let level = [
7010
+ [model, own, []]
7011
+ ];
7012
+ for (let current = 1; current <= depth && level.length; current += 1) {
7013
+ const next = [];
7014
+ level.forEach(([parent, serializer, path]) => {
7015
+ [...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
7016
+ const opts = parent.relationshipFor(name);
7017
+ if (!opts) {
7018
+ return;
7019
+ }
7020
+ const nextPath = [...path, name];
7021
+ const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
7022
+ if (related) {
7023
+ next.push([opts.model, related, nextPath]);
7024
+ }
7025
+ });
7026
+ });
7027
+ level = next;
7028
+ }
7029
+ });
7030
+ if (missing.size) {
7031
+ throw new namespaced_serializer_missing_error_default(missing);
7032
+ }
7033
+ }
7034
+
6375
7035
  // src/packages/application/initialize.ts
6376
7036
  async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
6377
7037
  const load = createLoader(path);
@@ -6394,6 +7054,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6394
7054
  parent
6395
7055
  })
6396
7056
  );
7057
+ serializers.forEach((serializer) => {
7058
+ Reflect.defineProperty(serializer, "serializers", {
7059
+ value: serializers,
7060
+ writable: false,
7061
+ enumerable: false,
7062
+ configurable: false
7063
+ });
7064
+ });
6397
7065
  models.forEach((model) => {
6398
7066
  Reflect.defineProperty(model, "serializer", {
6399
7067
  value: closestChild(serializers, model.resourceName),
@@ -6419,6 +7087,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6419
7087
  configurable: false
6420
7088
  });
6421
7089
  });
7090
+ validateNamespacedSerializers(controllers, serializers);
6422
7091
  const ApplicationController = controllers.get("application");
6423
7092
  if (!ApplicationController) {
6424
7093
  throw new controller_missing_error_default("application");