lumen-framework 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cli.cjs +71 -38
  2. package/dist/cli.cjs.map +4 -4
  3. package/dist/index.js +978 -272
  4. package/dist/index.js.map +4 -4
  5. package/dist/index.mjs +969 -263
  6. package/dist/index.mjs.map +4 -4
  7. package/dist/types/errors/namespaced-serializer-missing-error.d.ts +15 -0
  8. package/dist/types/packages/application/utils/validate-namespaced-serializers.d.ts +15 -0
  9. package/dist/types/packages/controller/errors/index.d.ts +1 -0
  10. package/dist/types/packages/controller/errors/related-record-not-found-error.d.ts +14 -0
  11. package/dist/types/packages/controller/index.d.ts +65 -0
  12. package/dist/types/packages/controller/utils/find-many.d.ts +2 -1
  13. package/dist/types/packages/controller/utils/find-one.d.ts +2 -1
  14. package/dist/types/packages/controller/utils/params-to-query.d.ts +7 -2
  15. package/dist/types/packages/controller/utils/validate-relationships.d.ts +8 -0
  16. package/dist/types/packages/database/constants.d.ts +1 -0
  17. package/dist/types/packages/database/model/index.d.ts +11 -0
  18. package/dist/types/packages/database/model/utils/process-write-error.d.ts +9 -1
  19. package/dist/types/packages/database/validation/errors/validation-error.d.ts +11 -2
  20. package/dist/types/packages/jsonapi/errors/not-acceptable-error.d.ts +1 -1
  21. package/dist/types/packages/jsonapi/index.d.ts +3 -1
  22. package/dist/types/packages/jsonapi/interfaces.d.ts +1 -1
  23. package/dist/types/packages/jsonapi/utils/has-media-type-params.d.ts +6 -0
  24. package/dist/types/packages/jsonapi/utils/is-jsonapi.d.ts +2 -0
  25. package/dist/types/packages/jsonapi/utils/media-type.d.ts +19 -0
  26. package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +13 -0
  27. package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +14 -0
  28. package/dist/types/packages/router/route/params/errors/index.d.ts +2 -0
  29. package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +2 -0
  30. package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +2 -0
  31. package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +2 -0
  32. package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +2 -0
  33. package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +2 -0
  34. package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +2 -0
  35. package/dist/types/packages/router/route/params/index.d.ts +1 -0
  36. package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +12 -0
  37. package/dist/types/packages/router/route/params/parameter/index.d.ts +8 -0
  38. package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +8 -0
  39. package/dist/types/packages/serializer/index.d.ts +111 -36
  40. package/dist/types/packages/serializer/utils/include-tree.d.ts +29 -0
  41. package/dist/types/packages/serializer/utils/load-linkage.d.ts +22 -0
  42. package/dist/types/packages/server/index.d.ts +2 -1
  43. package/dist/types/packages/server/interfaces.d.ts +5 -0
  44. package/dist/types/packages/server/utils/source-for.d.ts +10 -0
  45. package/dist/types/packages/server/utils/validate-accept.d.ts +5 -1
  46. package/dist/types/packages/server/utils/validate-content-type.d.ts +4 -0
  47. package/package.json +1 -1
  48. package/dist/types/packages/jsonapi/utils/has-media-type.d.ts +0 -4
package/dist/index.js CHANGED
@@ -43,6 +43,8 @@ module.exports = __toCommonJS(index_exports);
43
43
  var import_chalk4 = require("chalk");
44
44
 
45
45
  // src/packages/database/constants.ts
46
+ var UNIQUE_CONSTRAINT_CODES = /* @__PURE__ */ new Set(["23505", "ER_DUP_ENTRY"]);
47
+ var UNIQUE_CONSTRAINT = /unique(\s+key)?\s+constraint|duplicate key/i;
46
48
  var VALID_DRIVERS = [
47
49
  "pg",
48
50
  "sqlite3",
@@ -892,7 +894,7 @@ function underscore(source = "", upper = false) {
892
894
  var normalize_model_name_default = compose(import_inflection2.singularize, import_inflection2.dasherize, underscore);
893
895
 
894
896
  // src/packages/database/model/index.ts
895
- var import_inflection9 = require("inflection");
897
+ var import_inflection10 = require("inflection");
896
898
 
897
899
  // src/packages/database/query/index.ts
898
900
  var import_inflection5 = require("inflection");
@@ -1090,10 +1092,16 @@ async function buildResults({
1090
1092
  }
1091
1093
  );
1092
1094
  }
1095
+ const missing = new Set(
1096
+ entries(record).filter(([key, value]) => value == null && pkPattern.test(key)).map(([key]) => key.split(".")[0])
1097
+ );
1098
+ const absent = Array.from(missing).filter(
1099
+ (name) => model.relationshipFor(name)?.type === "hasOne"
1100
+ );
1093
1101
  const instance = Reflect.construct(model, [
1094
1102
  entries(record).reduce((r, entry) => {
1095
1103
  let [key, value] = entry;
1096
- if (value == null && pkPattern.test(key)) {
1104
+ if (key.indexOf(".") >= 0 && missing.has(key.split(".")[0])) {
1097
1105
  return r;
1098
1106
  } else if (key.indexOf(".") >= 0) {
1099
1107
  const [a, b] = key.split(".");
@@ -1113,6 +1121,7 @@ async function buildResults({
1113
1121
  };
1114
1122
  }, {})
1115
1123
  ]);
1124
+ absent.forEach((name) => instance.absentRelationships.add(name));
1116
1125
  instance.currentChangeSet.persist();
1117
1126
  return instance;
1118
1127
  });
@@ -1124,6 +1133,17 @@ function createRunner(target, opts) {
1124
1133
  const { resolve: resolve2, reject } = opts;
1125
1134
  let didRun = false;
1126
1135
  RUNNERS.set(target, async () => {
1136
+ if (didRun) {
1137
+ return;
1138
+ }
1139
+ didRun = true;
1140
+ try {
1141
+ await run();
1142
+ } catch (err) {
1143
+ reject(err);
1144
+ }
1145
+ });
1146
+ async function run() {
1127
1147
  let results;
1128
1148
  const {
1129
1149
  model,
@@ -1133,10 +1153,6 @@ function createRunner(target, opts) {
1133
1153
  shouldCount,
1134
1154
  relationships
1135
1155
  } = target;
1136
- if (didRun) {
1137
- return;
1138
- }
1139
- didRun = true;
1140
1156
  if (!shouldCount && !snapshots.some(([name]) => name === "select")) {
1141
1157
  target.select(...target.model.attributeNames);
1142
1158
  }
@@ -1177,7 +1193,7 @@ function createRunner(target, opts) {
1177
1193
  resolve2(result);
1178
1194
  }
1179
1195
  }
1180
- });
1196
+ }
1181
1197
  }
1182
1198
  }
1183
1199
  function runQuery(target) {
@@ -1882,7 +1898,9 @@ async function get(owner, key) {
1882
1898
  let { foreignKey } = opts;
1883
1899
  value = owner.currentChangeSet.get(key);
1884
1900
  foreignKey = (0, import_inflection7.camelize)(foreignKey, true);
1885
- if (!value) {
1901
+ if (!value && owner.absentRelationships.has(key)) {
1902
+ value = null;
1903
+ } else if (!value) {
1886
1904
  switch (type) {
1887
1905
  case "hasOne":
1888
1906
  value = await getHasOne(owner, {
@@ -2544,13 +2562,37 @@ async function initializeClass({
2544
2562
  return model;
2545
2563
  }
2546
2564
 
2565
+ // src/packages/server/utils/source-for.ts
2566
+ var import_inflection9 = require("inflection");
2567
+ function memberName(key) {
2568
+ return (0, import_inflection9.dasherize)(underscore(key));
2569
+ }
2570
+ function escapePointer(segment) {
2571
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
2572
+ }
2573
+ function sourceFor(path) {
2574
+ const [root, ...rest] = path.split(".").filter(Boolean).map(memberName);
2575
+ if (!root) {
2576
+ return {};
2577
+ } else if (root === "data") {
2578
+ return {
2579
+ pointer: `/${[root, ...rest].map(escapePointer).join("/")}`
2580
+ };
2581
+ }
2582
+ return {
2583
+ parameter: `${root}${rest.map((key) => `[${key}]`).join("")}`
2584
+ };
2585
+ }
2586
+
2547
2587
  // src/packages/database/validation/errors/validation-error.ts
2548
2588
  var ValidationError = class extends Error {
2549
- constructor(key, value) {
2550
- super(`Validation failed for ${key}: ${value}`);
2589
+ constructor(key) {
2590
+ super(`Validation failed for ${key}.`);
2591
+ this.key = key;
2592
+ this.source = sourceFor(`data.attributes.${key}`);
2551
2593
  }
2552
2594
  };
2553
- var validation_error_default = ValidationError;
2595
+ var validation_error_default = createServerError(ValidationError, 422);
2554
2596
 
2555
2597
  // src/packages/database/validation/index.ts
2556
2598
  var Validation = class {
@@ -2590,12 +2632,32 @@ function validate(instance) {
2590
2632
  validator: Reflect.get(instance.constructor.validates, key)
2591
2633
  })).filter(({ validator }) => validator).map((props) => new validation_default(props)).reduce((result, validation) => {
2592
2634
  if (!validation.isValid()) {
2593
- throw new validation_error_default(validation.key, String(validation.value));
2635
+ throw new validation_error_default(validation.key);
2594
2636
  }
2595
2637
  return result;
2596
2638
  }, true);
2597
2639
  }
2598
2640
 
2641
+ // src/packages/database/model/utils/process-write-error.ts
2642
+ function driverMessage(message) {
2643
+ const index = message.lastIndexOf(" - ");
2644
+ return index >= 0 ? message.slice(index + 3) : message;
2645
+ }
2646
+ function processWriteError(err) {
2647
+ if (!(err instanceof Error)) {
2648
+ return err;
2649
+ }
2650
+ const { code } = err;
2651
+ const { message } = err;
2652
+ if (UNIQUE_CONSTRAINT_CODES.has(String(code)) || UNIQUE_CONSTRAINT.test(message)) {
2653
+ return new unique_constraint_error_default(driverMessage(message));
2654
+ }
2655
+ return err;
2656
+ }
2657
+ function rethrowWriteError(err) {
2658
+ throw processWriteError(err);
2659
+ }
2660
+
2599
2661
  // src/packages/database/model/utils/run-hooks.ts
2600
2662
  function runHooks(record, trx, ...hooks) {
2601
2663
  return hooks.filter((hook) => Boolean(hook)).reduce(
@@ -2635,6 +2697,12 @@ var Model = class {
2635
2697
  writable: false,
2636
2698
  enumerable: false,
2637
2699
  configurable: false
2700
+ },
2701
+ absentRelationships: {
2702
+ value: /* @__PURE__ */ new Set(),
2703
+ writable: false,
2704
+ enumerable: false,
2705
+ configurable: false
2638
2706
  }
2639
2707
  });
2640
2708
  const {
@@ -2981,9 +3049,9 @@ var Model = class {
2981
3049
  return createTransactionResultProxy(this, hadDirtyAttrs || hadDirtyAssoc);
2982
3050
  };
2983
3051
  if (transaction) {
2984
- return run(transaction);
3052
+ return run(transaction).catch(rethrowWriteError);
2985
3053
  }
2986
- return this.transaction(run);
3054
+ return this.transaction(run).catch(rethrowWriteError);
2987
3055
  }
2988
3056
  /**
2989
3057
  * Permanently delete the instance from the database.
@@ -3103,9 +3171,9 @@ var Model = class {
3103
3171
  return createTransactionResultProxy(instance, true);
3104
3172
  };
3105
3173
  if (transaction) {
3106
- return run(transaction);
3174
+ return run(transaction).catch(rethrowWriteError);
3107
3175
  }
3108
- return this.transaction(run);
3176
+ return this.transaction(run).catch(rethrowWriteError);
3109
3177
  }
3110
3178
  /**
3111
3179
  * Specify the transaction object to use for following save, update, or
@@ -3283,7 +3351,7 @@ var Model = class {
3283
3351
  return Promise.resolve(this);
3284
3352
  }
3285
3353
  if (!this.tableName) {
3286
- const getTableName = compose(import_inflection9.pluralize, underscore);
3354
+ const getTableName = compose(import_inflection10.pluralize, underscore);
3287
3355
  const tableName = getTableName(this.name);
3288
3356
  Reflect.defineProperty(this, "tableName", {
3289
3357
  value: tableName,
@@ -3481,7 +3549,7 @@ function parseNestedObject(source) {
3481
3549
  }
3482
3550
 
3483
3551
  // src/packages/server/request/parser/utils/format.ts
3484
- var import_inflection11 = require("inflection");
3552
+ var import_inflection12 = require("inflection");
3485
3553
 
3486
3554
  // src/packages/server/request/parser/constants.ts
3487
3555
  var INT = /^\d+$/;
@@ -3492,7 +3560,7 @@ var TRUE = /^true$/;
3492
3560
  var BRACKETS = /(?:\[])/g;
3493
3561
 
3494
3562
  // src/utils/transform-keys.ts
3495
- var import_inflection10 = require("inflection");
3563
+ var import_inflection11 = require("inflection");
3496
3564
  function transformKeys(source, transformer, deep = false) {
3497
3565
  if (Array.isArray(source)) {
3498
3566
  return source.slice(0);
@@ -3516,14 +3584,14 @@ function transformKeys(source, transformer, deep = false) {
3516
3584
  function camelizeKeys(source, deep) {
3517
3585
  return transformKeys(
3518
3586
  source,
3519
- (key) => (0, import_inflection10.camelize)(underscore(key), true),
3587
+ (key) => (0, import_inflection11.camelize)(underscore(key), true),
3520
3588
  deep
3521
3589
  );
3522
3590
  }
3523
3591
  function dasherizeKeys(source, deep) {
3524
3592
  return transformKeys(
3525
3593
  source,
3526
- (key) => (0, import_inflection10.dasherize)(underscore(key)),
3594
+ (key) => (0, import_inflection11.dasherize)(underscore(key)),
3527
3595
  deep
3528
3596
  );
3529
3597
  }
@@ -3538,7 +3606,7 @@ function makeArray(source) {
3538
3606
  function formatString(source, method) {
3539
3607
  if (method === "GET") {
3540
3608
  if (source.indexOf(",") >= 0) {
3541
- return source.split(",").map((str) => (0, import_inflection11.camelize)(underscore(str), true));
3609
+ return source.split(",").map((str) => (0, import_inflection12.camelize)(underscore(str), true));
3542
3610
  } else if (INT.test(source)) {
3543
3611
  return Number.parseInt(source, 10);
3544
3612
  } else if (BOOL.test(source)) {
@@ -3565,9 +3633,9 @@ function formatObject(source, method, formatter) {
3565
3633
  }
3566
3634
  function formatSort(sort) {
3567
3635
  if (sort.startsWith("-")) {
3568
- return `-${(0, import_inflection11.camelize)(underscore(sort.substr(1)), true)}`;
3636
+ return `-${(0, import_inflection12.camelize)(underscore(sort.substr(1)), true)}`;
3569
3637
  }
3570
- return (0, import_inflection11.camelize)(underscore(sort), true);
3638
+ return (0, import_inflection12.camelize)(underscore(sort), true);
3571
3639
  }
3572
3640
  function formatFields(fields) {
3573
3641
  return entries(fields).reduce(
@@ -3728,10 +3796,11 @@ var MIME_TYPE = "application/vnd.api+json";
3728
3796
 
3729
3797
  // src/packages/jsonapi/errors/not-acceptable-error.ts
3730
3798
  var NotAcceptableError = class extends TypeError {
3731
- constructor(contentType) {
3799
+ constructor(accept) {
3732
3800
  super(line`
3733
- Media type parameters is not supported. Try your request again
3734
- without specifying '${contentType.replace(MIME_TYPE, "")}'.
3801
+ Every '${MIME_TYPE}' in Accept: '${accept}' has media type parameters,
3802
+ which are not supported. Try your request again with '${MIME_TYPE}'
3803
+ without parameters.
3735
3804
  `);
3736
3805
  }
3737
3806
  };
@@ -3741,8 +3810,9 @@ var not_acceptable_error_default = createServerError(NotAcceptableError, 406);
3741
3810
  var UnsupportedMediaTypeError = class extends TypeError {
3742
3811
  constructor(contentType) {
3743
3812
  super(line`
3744
- Media type parameters is not supported. Try your request again
3745
- without specifying '${contentType.replace(MIME_TYPE, "")}'.
3813
+ Media type parameters are not supported in Content-Type:
3814
+ '${contentType}'. Try your request again with Content-Type:
3815
+ '${MIME_TYPE}' without parameters.
3746
3816
  `);
3747
3817
  }
3748
3818
  };
@@ -3757,17 +3827,59 @@ var InvalidContentTypeError = class extends TypeError {
3757
3827
  `);
3758
3828
  }
3759
3829
  };
3760
- var invalid_content_type_error_default = createServerError(InvalidContentTypeError, 400);
3830
+ var invalid_content_type_error_default = createServerError(InvalidContentTypeError, 415);
3831
+
3832
+ // src/packages/jsonapi/utils/media-type.ts
3833
+ var QUALITY = /^q\s*=/i;
3834
+ function splitUnquoted(value, delimiter) {
3835
+ const parts = [];
3836
+ let current = "";
3837
+ let quoted = false;
3838
+ let escaped = false;
3839
+ for (const char of value) {
3840
+ if (escaped) {
3841
+ escaped = false;
3842
+ } else if (quoted && char === "\\") {
3843
+ escaped = true;
3844
+ } else if (char === '"') {
3845
+ quoted = !quoted;
3846
+ } else if (!quoted && char === delimiter) {
3847
+ parts.push(current);
3848
+ current = "";
3849
+ continue;
3850
+ }
3851
+ current += char;
3852
+ }
3853
+ return [...parts, current];
3854
+ }
3855
+ function parseMediaType(value) {
3856
+ const [type = "", ...params] = splitUnquoted(value, ";").map(
3857
+ (part) => part.trim()
3858
+ );
3859
+ return {
3860
+ type: type.toLowerCase(),
3861
+ params: params.filter(Boolean)
3862
+ };
3863
+ }
3864
+ function parseAccept(value) {
3865
+ return splitUnquoted(value, ",").map((range) => {
3866
+ const { type, params } = parseMediaType(range);
3867
+ const quality = params.findIndex((param) => QUALITY.test(param));
3868
+ return {
3869
+ type,
3870
+ params: quality === -1 ? params : params.slice(0, quality)
3871
+ };
3872
+ }).filter(({ type }) => type.length > 0);
3873
+ }
3761
3874
 
3762
3875
  // src/packages/jsonapi/utils/is-jsonapi.ts
3763
3876
  function isJSONAPI(value) {
3764
- return value.startsWith(MIME_TYPE);
3877
+ return parseMediaType(value).type === MIME_TYPE;
3765
3878
  }
3766
3879
 
3767
- // src/packages/jsonapi/utils/has-media-type.ts
3768
- var PATTERN2 = /^application\/vnd.api\+json;charset=.+$/;
3769
- function hasMediaType(value) {
3770
- return PATTERN2.test(value);
3880
+ // src/packages/jsonapi/utils/has-media-type-params.ts
3881
+ function hasMediaTypeParams(value) {
3882
+ return parseMediaType(value).params.length > 0;
3771
3883
  }
3772
3884
 
3773
3885
  // src/utils/env.ts
@@ -3788,6 +3900,10 @@ function dataFor(status, err) {
3788
3900
  if (title) {
3789
3901
  errData.title = title;
3790
3902
  }
3903
+ const source = err?.source;
3904
+ if (source && (source.pointer || source.parameter)) {
3905
+ errData.source = source;
3906
+ }
3791
3907
  if (err && (isDevelopment() || /^\[public\]/gi.test(err.message))) {
3792
3908
  errData.detail = err.message.replace(/^\[public\]/gi, "");
3793
3909
  }
@@ -3868,9 +3984,12 @@ function createResponder(req, res) {
3868
3984
  }
3869
3985
 
3870
3986
  // src/packages/server/utils/validate-accept.ts
3871
- function validateAccept(contentType) {
3872
- if (contentType && hasMediaType(contentType)) {
3873
- throw new not_acceptable_error_default(contentType);
3987
+ function validateAccept(accept) {
3988
+ if (accept) {
3989
+ const ranges = parseAccept(accept).filter(({ type }) => type === MIME_TYPE);
3990
+ if (ranges.length && ranges.every(({ params }) => params.length)) {
3991
+ throw new not_acceptable_error_default(accept);
3992
+ }
3874
3993
  }
3875
3994
  return true;
3876
3995
  }
@@ -3879,7 +3998,7 @@ function validateAccept(contentType) {
3879
3998
  function validateContentType(contentType) {
3880
3999
  if (!contentType || !isJSONAPI(contentType)) {
3881
4000
  throw new invalid_content_type_error_default(contentType);
3882
- } else if (hasMediaType(contentType)) {
4001
+ } else if (hasMediaTypeParams(contentType)) {
3883
4002
  throw new unsupported_media_type_error_default(contentType);
3884
4003
  }
3885
4004
  return true;
@@ -4007,9 +4126,10 @@ function merge(dest, source) {
4007
4126
  }
4008
4127
 
4009
4128
  // src/packages/controller/utils/params-to-query.ts
4010
- function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4129
+ function paramsToQuery(model, { id, page, sort, filter, fields, include }, serializerFor = (related) => related.serializer) {
4011
4130
  const relationships = entries(model.relationships);
4012
4131
  const includedFields = omit(fields, model.resourceName);
4132
+ const included = include && include.map((path) => path.split(".")[0]);
4013
4133
  let query = {
4014
4134
  id,
4015
4135
  filter,
@@ -4052,9 +4172,9 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4052
4172
  if (!value.includes(relationship.model.primaryKey)) {
4053
4173
  value = [relationship.model.primaryKey, ...value];
4054
4174
  }
4055
- if (include && value.length === 1 && include.includes(name)) {
4056
- value = [...value, ...relationship.model.serializer.attributes];
4057
- } else if (!include && value.length > 1) {
4175
+ if (included && value.length === 1 && included.includes(name)) {
4176
+ value = [...value, ...serializerFor(relationship.model).attributes];
4177
+ } else if (!included && value.length > 1) {
4058
4178
  value = value.slice(0, 1);
4059
4179
  }
4060
4180
  return {
@@ -4069,18 +4189,19 @@ function paramsToQuery(model, { id, page, sort, filter, fields, include }) {
4069
4189
  }
4070
4190
 
4071
4191
  // src/packages/controller/utils/find-one.ts
4072
- function findOne(model, req) {
4192
+ function findOne(model, req, serializerFor) {
4073
4193
  const params = merge(req.defaultParams, req.params);
4074
- const { id, select, include } = paramsToQuery(model, params);
4194
+ const { id, select, include } = paramsToQuery(model, params, serializerFor);
4075
4195
  return model.find(id).select(...select).include(include);
4076
4196
  }
4077
4197
 
4078
4198
  // src/packages/controller/utils/find-many.ts
4079
- function findMany(model, req) {
4199
+ function findMany(model, req, serializerFor) {
4080
4200
  const params = merge(req.defaultParams, req.params);
4081
4201
  const { sort, page, limit, select, filter, include } = paramsToQuery(
4082
4202
  model,
4083
- params
4203
+ params,
4204
+ serializerFor
4084
4205
  );
4085
4206
  return model.select(...select).include(include).limit(limit).page(page).where(filter).order(...sort);
4086
4207
  }
@@ -4110,6 +4231,53 @@ function resolveRelationships(model, relationships = {}) {
4110
4231
  );
4111
4232
  }
4112
4233
 
4234
+ // src/packages/controller/errors/related-record-not-found-error.ts
4235
+ var RelatedRecordNotFoundError = class extends Error {
4236
+ constructor({ name, primaryKey }, primaryKeyValue, path) {
4237
+ super(
4238
+ `Could not find ${name} with ${primaryKey} ${stringify(primaryKeyValue)}.`
4239
+ );
4240
+ this.source = sourceFor(path);
4241
+ }
4242
+ };
4243
+ var related_record_not_found_error_default = createServerError(RelatedRecordNotFoundError, 404);
4244
+
4245
+ // src/packages/controller/utils/validate-relationships.ts
4246
+ async function validateRelationships(model, relationships = {}) {
4247
+ await Promise.all(
4248
+ entries(relationships).map(async ([key, value]) => {
4249
+ const opts = model.relationshipFor(key);
4250
+ const { data = null } = value || {};
4251
+ if (!opts || !data) {
4252
+ return;
4253
+ }
4254
+ const { model: related } = opts;
4255
+ const isMany = Array.isArray(data);
4256
+ const ids = (isMany ? data : [data]).map(
4257
+ (item) => Reflect.get(Object(item), "id")
4258
+ );
4259
+ if (!ids.length) {
4260
+ return;
4261
+ }
4262
+ const column = related.columnNameFor(related.primaryKey) || "id";
4263
+ const rows = await related.table().whereIn(
4264
+ `${related.tableName}.${column}`,
4265
+ ids.filter((id) => id != null)
4266
+ ).select(`${related.tableName}.${column}`);
4267
+ const found = new Set(rows.map((row) => String(Reflect.get(row, column))));
4268
+ const missing = ids.findIndex((id) => !found.has(String(id)));
4269
+ if (missing >= 0) {
4270
+ const path = `data.relationships.${key}.data`;
4271
+ throw new related_record_not_found_error_default(
4272
+ related,
4273
+ ids[missing],
4274
+ isMany ? `${path}.${missing}` : path
4275
+ );
4276
+ }
4277
+ })
4278
+ );
4279
+ }
4280
+
4113
4281
  // src/packages/controller/constants.ts
4114
4282
  var BUILT_IN_ACTIONS = Object.freeze(["show", "index", "create", "update", "destroy"]);
4115
4283
 
@@ -4295,6 +4463,74 @@ var Controller = class {
4295
4463
  * @public
4296
4464
  */
4297
4465
  defaultPerPage = 25;
4466
+ /**
4467
+ * How many relationships deep an `?include` path may go on this
4468
+ * controller's routes. `comments.reactions.user` is 3 levels deep; with `1`
4469
+ * only direct relationships (`comments`) can be included. Paths deeper than
4470
+ * this are rejected with `400 Bad Request`.
4471
+ *
4472
+ * Set it on `ApplicationController` to change it for the whole app, or on a
4473
+ * single controller to override it there.
4474
+ *
4475
+ * ```javascript
4476
+ * class ApplicationController extends Controller {
4477
+ * maxIncludeDepth = 2;
4478
+ * }
4479
+ * ```
4480
+ *
4481
+ * Every allowed path is enumerated up front from the serializers'
4482
+ * relationships, and each nested level costs its own queries per request, so
4483
+ * keep this small.
4484
+ *
4485
+ * @property maxIncludeDepth
4486
+ * @type {Number}
4487
+ * @default 3
4488
+ * @public
4489
+ */
4490
+ maxIncludeDepth = 3;
4491
+ /**
4492
+ * Whether a namespace may fall back to the root Serializer of a type it has
4493
+ * no Serializer for. Read from a namespace's `ApplicationController` and
4494
+ * applies to the whole namespace.
4495
+ *
4496
+ * By default `app/controllers/admin/comments.js` without an
4497
+ * `app/serializers/admin/comments.js` — or an included type without one —
4498
+ * is serialized by the root Serializer, with every attribute and
4499
+ * relationship it declares. For a namespace that must only expose what it
4500
+ * declares itself, turn the fallback off:
4501
+ *
4502
+ * ```javascript
4503
+ * // app/controllers/admin/application.js
4504
+ * class AdminApplicationController extends ApplicationController {
4505
+ * serializerFallback = false;
4506
+ * }
4507
+ * ```
4508
+ *
4509
+ * The application then refuses to boot while any type the namespace can
4510
+ * serialize or `include` (down to each controller's `maxIncludeDepth`) has
4511
+ * no Serializer in that namespace, listing each missing one.
4512
+ *
4513
+ * @property serializerFallback
4514
+ * @type {Boolean}
4515
+ * @default true
4516
+ * @public
4517
+ */
4518
+ serializerFallback = true;
4519
+ /**
4520
+ * The Serializer to serialize (and validate, and load) related resources of
4521
+ * this Controller's responses with: the related model's Serializer in this
4522
+ * Controller's namespace, falling back to the root one.
4523
+ *
4524
+ * Always this Controller's namespace — not its Serializer's, which is the
4525
+ * root one when the namespace has no Serializer for this resource.
4526
+ *
4527
+ * @method serializerFor
4528
+ * @private
4529
+ */
4530
+ serializerFor(model) {
4531
+ const { serializer, namespace } = this;
4532
+ return serializer ? serializer.serializerFor(model, namespace) : model.serializer;
4533
+ }
4298
4534
  constructor({ model, namespace, serializer }) {
4299
4535
  Object.assign(this, {
4300
4536
  model,
@@ -4320,7 +4556,7 @@ var Controller = class {
4320
4556
  * @public
4321
4557
  */
4322
4558
  index(req) {
4323
- return findMany(this.model, req);
4559
+ return findMany(this.model, req, (related) => this.serializerFor(related));
4324
4560
  }
4325
4561
  /**
4326
4562
  * This method supports including relationships, and sparse fieldsets via
@@ -4335,7 +4571,7 @@ var Controller = class {
4335
4571
  * @public
4336
4572
  */
4337
4573
  show(req) {
4338
- return findOne(this.model, req);
4574
+ return findOne(this.model, req, (related) => this.serializerFor(related));
4339
4575
  }
4340
4576
  /**
4341
4577
  * Create and return a single Model instance that the Controller instance
@@ -4356,6 +4592,7 @@ var Controller = class {
4356
4592
  data: { attributes, relationships }
4357
4593
  }
4358
4594
  } = req;
4595
+ await validateRelationships(model, relationships);
4359
4596
  const record = await model.create({
4360
4597
  ...attributes,
4361
4598
  ...resolveRelationships(model, relationships)
@@ -4381,12 +4618,13 @@ var Controller = class {
4381
4618
  */
4382
4619
  update(req) {
4383
4620
  const { model } = this;
4384
- return findOne(model, req).then((record) => {
4621
+ return findOne(model, req, (related) => this.serializerFor(related)).then(async (record) => {
4385
4622
  const {
4386
4623
  params: {
4387
4624
  data: { attributes, relationships }
4388
4625
  }
4389
4626
  } = req;
4627
+ await validateRelationships(model, relationships);
4390
4628
  return record.update({
4391
4629
  ...attributes,
4392
4630
  ...resolveRelationships(model, relationships)
@@ -4410,7 +4648,7 @@ var Controller = class {
4410
4648
  * @public
4411
4649
  */
4412
4650
  destroy(req) {
4413
- return findOne(this.model, req).then((record) => record.destroy()).then(() => 204);
4651
+ return findOne(this.model, req, (related) => this.serializerFor(related)).then((record) => record.destroy()).then(() => 204);
4414
4652
  }
4415
4653
  /**
4416
4654
  * Respond to HEAD or OPTIONS requests.
@@ -4428,7 +4666,194 @@ var Controller = class {
4428
4666
  var controller_default = Controller;
4429
4667
 
4430
4668
  // src/packages/serializer/index.ts
4431
- var import_inflection12 = require("inflection");
4669
+ var import_path3 = require("path");
4670
+ var import_inflection14 = require("inflection");
4671
+
4672
+ // src/packages/loader/resolver/utils/closest-ancestor.ts
4673
+ var import_path2 = require("path");
4674
+ function closestAncestor(source, key) {
4675
+ const name = import_path2.posix.basename(key);
4676
+ let namespace = import_path2.posix.dirname(key);
4677
+ if (namespace === ".") {
4678
+ return source.get(name);
4679
+ }
4680
+ namespace = import_path2.posix.dirname(namespace);
4681
+ const ancestor = source.get(import_path2.posix.join(namespace, name));
4682
+ if (ancestor) {
4683
+ return ancestor;
4684
+ }
4685
+ return closestAncestor(source, import_path2.posix.join(import_path2.posix.dirname(namespace), name));
4686
+ }
4687
+
4688
+ // src/packages/serializer/utils/load-linkage.ts
4689
+ var import_inflection13 = require("inflection");
4690
+ var valueOf = (record, key) => Reflect.get(record, key);
4691
+ var toId = (value) => value == null ? null : String(value);
4692
+ async function loadLinkage(model, records, names) {
4693
+ const ids = Array.from(
4694
+ new Set(records.map((record) => record.getPrimaryKey()))
4695
+ );
4696
+ const linkage = /* @__PURE__ */ new Map();
4697
+ const belongsTo = [];
4698
+ const queries = [];
4699
+ ids.forEach((id) => {
4700
+ linkage.set(
4701
+ String(id),
4702
+ names.reduce((result, name) => {
4703
+ const opts = model.relationshipFor(name);
4704
+ result[name] = opts && opts.type === "hasMany" ? [] : null;
4705
+ return result;
4706
+ }, {})
4707
+ );
4708
+ });
4709
+ if (!ids.length) {
4710
+ return linkage;
4711
+ }
4712
+ const link = (ownerId, name, relatedId) => {
4713
+ const owner = linkage.get(String(ownerId));
4714
+ const id = toId(relatedId);
4715
+ if (!owner || id === null) {
4716
+ return;
4717
+ }
4718
+ const current = owner[name];
4719
+ if (Array.isArray(current)) {
4720
+ if (!current.includes(id)) {
4721
+ current.push(id);
4722
+ }
4723
+ } else if (current === null) {
4724
+ owner[name] = id;
4725
+ }
4726
+ };
4727
+ names.forEach((name) => {
4728
+ const opts = model.relationshipFor(name);
4729
+ if (!opts) {
4730
+ return;
4731
+ }
4732
+ const { type, through, model: related } = opts;
4733
+ const foreignKey = (0, import_inflection13.camelize)(opts.foreignKey, true);
4734
+ if (type === "belongsTo") {
4735
+ belongsTo.push([name, foreignKey, related]);
4736
+ } else if (through) {
4737
+ const inverse = related.relationshipFor(opts.inverse);
4738
+ if (!inverse) {
4739
+ return;
4740
+ }
4741
+ const relatedKey = (0, import_inflection13.camelize)(inverse.foreignKey, true);
4742
+ queries.push(
4743
+ (async () => {
4744
+ const rows = await through.select(foreignKey, relatedKey).where({ [foreignKey]: ids });
4745
+ rows.forEach((row) => {
4746
+ link(valueOf(row, foreignKey), name, valueOf(row, relatedKey));
4747
+ });
4748
+ })()
4749
+ );
4750
+ } else {
4751
+ queries.push(
4752
+ (async () => {
4753
+ const rows = await related.select(related.primaryKey, foreignKey).where({ [foreignKey]: ids }).order(related.primaryKey, "ASC");
4754
+ rows.forEach((row) => {
4755
+ link(valueOf(row, foreignKey), name, row.getPrimaryKey());
4756
+ });
4757
+ })()
4758
+ );
4759
+ }
4760
+ });
4761
+ if (belongsTo.length) {
4762
+ queries.push(
4763
+ (async () => {
4764
+ const rows = await model.select(model.primaryKey, ...belongsTo.map(([, key]) => key)).where({ [model.primaryKey]: ids });
4765
+ rows.forEach((row) => {
4766
+ belongsTo.forEach(([name, key]) => {
4767
+ link(row.getPrimaryKey(), name, valueOf(row, key));
4768
+ });
4769
+ });
4770
+ await dropDangling(linkage, belongsTo);
4771
+ })()
4772
+ );
4773
+ }
4774
+ await Promise.all(queries);
4775
+ return linkage;
4776
+ }
4777
+ async function dropDangling(linkage, belongsTo) {
4778
+ const wanted = /* @__PURE__ */ new Map();
4779
+ belongsTo.forEach(([name, , related]) => {
4780
+ const ids = wanted.get(related) || /* @__PURE__ */ new Set();
4781
+ linkage.forEach(({ [name]: id }) => {
4782
+ if (typeof id === "string") {
4783
+ ids.add(id);
4784
+ }
4785
+ });
4786
+ wanted.set(related, ids);
4787
+ });
4788
+ const existing = /* @__PURE__ */ new Map();
4789
+ await Promise.all(
4790
+ Array.from(wanted, async ([related, ids]) => {
4791
+ if (!ids.size) {
4792
+ existing.set(related, ids);
4793
+ return;
4794
+ }
4795
+ const rows = await related.select(related.primaryKey).where({ [related.primaryKey]: Array.from(ids) });
4796
+ existing.set(
4797
+ related,
4798
+ new Set(rows.map((row) => String(row.getPrimaryKey())))
4799
+ );
4800
+ })
4801
+ );
4802
+ belongsTo.forEach(([name, , related]) => {
4803
+ const found = existing.get(related);
4804
+ linkage.forEach((owner) => {
4805
+ const id = owner[name];
4806
+ if (typeof id === "string" && !found?.has(id)) {
4807
+ owner[name] = null;
4808
+ }
4809
+ });
4810
+ });
4811
+ }
4812
+
4813
+ // src/packages/serializer/utils/include-tree.ts
4814
+ function createIncludeTree(paths = []) {
4815
+ const tree = /* @__PURE__ */ new Map();
4816
+ paths.forEach((path) => {
4817
+ path.split(".").filter(Boolean).reduce((node, name) => {
4818
+ let child = node.get(name);
4819
+ if (!child) {
4820
+ child = /* @__PURE__ */ new Map();
4821
+ node.set(name, child);
4822
+ }
4823
+ return child;
4824
+ }, tree);
4825
+ });
4826
+ return tree;
4827
+ }
4828
+ function enumerateIncludePaths(model, names, depth, serializerFor = (related) => related.serializer) {
4829
+ if (depth < 1) {
4830
+ return [];
4831
+ }
4832
+ return names.reduce((paths, name) => {
4833
+ const opts = model.relationshipFor(name);
4834
+ if (!opts) {
4835
+ return paths;
4836
+ }
4837
+ const { model: related } = opts;
4838
+ const serializer = serializerFor(related);
4839
+ if (!serializer) {
4840
+ return [...paths, name];
4841
+ }
4842
+ const { hasOne, hasMany } = serializer;
4843
+ return [
4844
+ ...paths,
4845
+ name,
4846
+ ...enumerateIncludePaths(
4847
+ related,
4848
+ [...hasOne, ...hasMany],
4849
+ depth - 1,
4850
+ serializerFor
4851
+ ).map((path) => `${name}.${path}`)
4852
+ ];
4853
+ }, []);
4854
+ }
4855
+
4856
+ // src/packages/serializer/index.ts
4432
4857
  var Serializer = class {
4433
4858
  /**
4434
4859
  * An Array of the `hasOne` or `belongsTo` relationships on a Serializer
@@ -4515,9 +4940,17 @@ var Serializer = class {
4515
4940
  * the resource and relationship objects in the returned [JSON API](
4516
4941
  * http://jsonapi.org) document object.
4517
4942
  *
4518
- * @param {Array} options.include - An array of strings containing the
4519
- * relationship keys that should be added to the top level included object of
4520
- * the returned [JSON API](http://jsonapi.org) document object.
4943
+ * @param {Array} options.include - An array of relationship paths (e.g.
4944
+ * `'comments'` or `'comments.user'`) whose resources should be added to the
4945
+ * top level included object of the returned [JSON API](http://jsonapi.org)
4946
+ * document object. Intermediate resources of a nested path are included too.
4947
+ *
4948
+ * @param {String} options.namespace - The namespace of the request, i.e. of
4949
+ * the Controller handling it. Every link in the document is built in it, and
4950
+ * included resources are serialized by their Serializer in it (falling back
4951
+ * to the root). Defaults to this Serializer's namespace — which is the root
4952
+ * one when a namespaced Controller has no Serializer of its own, so the
4953
+ * Controller passes its namespace explicitly.
4521
4954
  *
4522
4955
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) document
4523
4956
  * object.
@@ -4528,38 +4961,61 @@ var Serializer = class {
4528
4961
  data,
4529
4962
  links,
4530
4963
  domain,
4531
- include
4964
+ include,
4965
+ namespace = this.namespace
4532
4966
  }) {
4533
- let serialized = {};
4534
- const included = [];
4967
+ const tree = createIncludeTree(include);
4968
+ const related = /* @__PURE__ */ new Map();
4969
+ const included = /* @__PURE__ */ new Map();
4970
+ const names = Array.from(tree.keys());
4971
+ let primary;
4972
+ let serialized;
4535
4973
  if (Array.isArray(data)) {
4536
- serialized = {
4537
- data: await Promise.all(
4538
- data.map(
4539
- (item) => this.formatOne({
4540
- item,
4541
- domain,
4542
- include,
4543
- included
4544
- })
4545
- )
4974
+ primary = await Promise.all(
4975
+ data.map(
4976
+ (item) => this.formatOne({
4977
+ item,
4978
+ domain,
4979
+ related,
4980
+ namespace,
4981
+ include: names
4982
+ })
4546
4983
  )
4547
- };
4984
+ );
4985
+ serialized = { data: primary };
4548
4986
  } else {
4549
- serialized = {
4550
- data: await this.formatOne({
4987
+ const resource2 = await this.formatOne({
4988
+ domain,
4989
+ related,
4990
+ namespace,
4991
+ item: data,
4992
+ include: names,
4993
+ links: false
4994
+ });
4995
+ primary = [resource2];
4996
+ serialized = { data: resource2 };
4997
+ }
4998
+ for (const [name, children] of tree) {
4999
+ const opts = this.model.relationshipFor(name);
5000
+ const records = related.get(name);
5001
+ if (opts && records) {
5002
+ await this.addIncluded({
4551
5003
  domain,
4552
- include,
5004
+ records,
4553
5005
  included,
4554
- item: data,
4555
- links: false
4556
- })
4557
- };
5006
+ namespace,
5007
+ model: opts.model,
5008
+ tree: children
5009
+ });
5010
+ }
4558
5011
  }
4559
- if (included.length) {
5012
+ primary.forEach((resource2) => {
5013
+ included.delete(resourceKey(resource2));
5014
+ });
5015
+ if (included.size) {
4560
5016
  serialized = {
4561
5017
  ...serialized,
4562
- included: uniq(included, "id", "type")
5018
+ included: Array.from(included.values())
4563
5019
  };
4564
5020
  }
4565
5021
  return {
@@ -4574,6 +5030,12 @@ var Serializer = class {
4574
5030
  * Transform a single Model instance into a [JSON API](http://jsonapi.org)
4575
5031
  * resource object.
4576
5032
  *
5033
+ * Relationships are serialized in one of two ways. By default each one is
5034
+ * read from the Model instance (for primary data the query has already
5035
+ * loaded them). When `linkage` is given — as it is for included resources,
5036
+ * whose relationships are batch-loaded by `addIncluded()` — the resource
5037
+ * linkage is built from it instead, without touching the database.
5038
+ *
4577
5039
  * @method formatOne
4578
5040
  *
4579
5041
  * @param {Object} options - An options object used for building the returned
@@ -4590,17 +5052,19 @@ var Serializer = class {
4590
5052
  * the top level links object or relationship links objects in the returned
4591
5053
  * [JSON API](http://jsonapi.org) resource object.
4592
5054
  *
4593
- * @param {Array} options.include - An array of strings containing the
4594
- * relationship keys that should be added to the top level included object of
4595
- * a [JSON API](http://jsonapi.org) document object.
5055
+ * @param {Array} options.include - An array of the relationship keys whose
5056
+ * related records should be collected into `options.related`.
4596
5057
  *
4597
- * @param {Array} options.included - An array of [JSON API](
4598
- * http://jsonapi.org) resource objects that will be added to the top level
4599
- * included array of a [JSON API](http://jsonapi.org) document object.
5058
+ * @param {Map} options.related - Collects, per relationship key in
5059
+ * `options.include`, the related Model instances that belong in the top
5060
+ * level included object of a [JSON API](http://jsonapi.org) document object.
4600
5061
  *
4601
- * @param {Boolean} options.formatRelationships - Wether or not
4602
- * relationships should be formatted and included in the returned
4603
- * [JSON API](http://jsonapi.org) resource object.
5062
+ * @param {Object} options.linkage - Pre-loaded resource linkage (related
5063
+ * primary keys per relationship key) to serialize relationships from.
5064
+ *
5065
+ * @param {String} options.namespace - The namespace to build links in.
5066
+ * Defaults to this Serializer's; included resources pass the namespace of
5067
+ * the request, so every link in a document points into the same namespace.
4604
5068
  *
4605
5069
  * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource
4606
5070
  * object.
@@ -4611,13 +5075,15 @@ var Serializer = class {
4611
5075
  item,
4612
5076
  links,
4613
5077
  domain,
4614
- include,
4615
- included,
4616
- formatRelationships = true
5078
+ include = [],
5079
+ related,
5080
+ linkage,
5081
+ namespace = this.namespace
4617
5082
  }) {
4618
5083
  const { resourceName: type } = item;
4619
5084
  const id = String(item.getPrimaryKey());
4620
- let relationships = {};
5085
+ const names = [...this.hasOne, ...this.hasMany];
5086
+ let relationships;
4621
5087
  const attributes = dasherizeKeys(
4622
5088
  item.getAttributes(
4623
5089
  ...Object.keys(item.rawColumnData).filter(
@@ -4630,34 +5096,41 @@ var Serializer = class {
4630
5096
  type,
4631
5097
  attributes
4632
5098
  };
4633
- if (formatRelationships) {
5099
+ if (linkage) {
5100
+ relationships = names.reduce(
5101
+ (hash, name) => ({
5102
+ ...hash,
5103
+ [(0, import_inflection14.dasherize)(underscore(name))]: this.formatLinkage(
5104
+ domain,
5105
+ this.model.relationshipFor(name)?.model.resourceName,
5106
+ linkage[name],
5107
+ namespace
5108
+ )
5109
+ }),
5110
+ {}
5111
+ );
5112
+ } else {
5113
+ const collect = (name, records) => {
5114
+ if (related && include.includes(name)) {
5115
+ related.set(name, [...related.get(name) || [], ...records]);
5116
+ }
5117
+ };
4634
5118
  relationships = await promiseHash(
4635
- [...this.hasOne, ...this.hasMany].reduce(
5119
+ names.reduce(
4636
5120
  (hash, name) => ({
4637
5121
  ...hash,
4638
- [(0, import_inflection12.dasherize)(underscore(name))]: (async () => {
4639
- const related = await Reflect.get(item, name);
4640
- if (Array.isArray(related)) {
5122
+ [(0, import_inflection14.dasherize)(underscore(name))]: (async () => {
5123
+ const value = await Reflect.get(item, name);
5124
+ if (Array.isArray(value)) {
5125
+ collect(name, value);
4641
5126
  return {
4642
- data: await Promise.all(
4643
- related.map(async (relatedItem) => {
4644
- const { data: relatedData } = await this.formatRelationship({
4645
- domain,
4646
- included,
4647
- item: relatedItem,
4648
- include: include.includes(name)
4649
- });
4650
- return relatedData;
4651
- })
5127
+ data: value.map(
5128
+ (relatedItem) => this.formatRelationship(relatedItem, domain, namespace).data
4652
5129
  )
4653
5130
  };
4654
- } else if (related && related.id != null) {
4655
- return this.formatRelationship({
4656
- domain,
4657
- included,
4658
- item: related,
4659
- include: include.includes(name)
4660
- });
5131
+ } else if (value && value.id != null) {
5132
+ collect(name, [value]);
5133
+ return this.formatRelationship(value, domain, namespace);
4661
5134
  }
4662
5135
  return {
4663
5136
  data: null
@@ -4672,16 +5145,9 @@ var Serializer = class {
4672
5145
  serialized.relationships = relationships;
4673
5146
  }
4674
5147
  if (links || typeof links !== "boolean") {
4675
- const { namespace } = this;
4676
- if (namespace) {
4677
- serialized.links = {
4678
- self: `${domain}/${namespace}/${type}/${id}`
4679
- };
4680
- } else {
4681
- serialized.links = {
4682
- self: `${domain}/${type}/${id}`
4683
- };
4684
- }
5148
+ serialized.links = {
5149
+ self: this.linkFor(domain, type, id, namespace)
5150
+ };
4685
5151
  }
4686
5152
  return serialized;
4687
5153
  }
@@ -4691,70 +5157,167 @@ var Serializer = class {
4691
5157
  *
4692
5158
  * @method formatRelationship
4693
5159
  *
4694
- * @param {Object} options - An options object used for building the returned
5160
+ * @param {Model} item - The Model instance to transform into the returned
4695
5161
  * [JSON API](http://jsonapi.org) relationship object.
4696
5162
  *
4697
- * @param {Model} options.item - The Model instance to transform into the
5163
+ * @param {String} domain - A string used to build links included in the
4698
5164
  * returned [JSON API](http://jsonapi.org) relationship object.
4699
5165
  *
4700
- * @param {String} options.domain - A string used to build links included in
4701
- * the returned [JSON API](http://jsonapi.org) relationship object.
5166
+ * @return {Object} A [JSON API](http://jsonapi.org) relationship object.
4702
5167
  *
4703
- * @param {Array} options.include - An array of strings containing the
4704
- * relationship keys that should be added to the top level included object of
4705
- * a [JSON API](http://jsonapi.org) document object.
4706
- *
4707
- * @param {Array} options.included - An array of [JSON API](
4708
- * http://jsonapi.org) resource objects that will be added to the top level
4709
- * included array of a [JSON API](http://jsonapi.org) document object.
4710
- *
4711
- * @return {Promise} Resolves with a [JSON API](http://jsonapi.org)
4712
- * relationship object.
5168
+ * @private
5169
+ */
5170
+ formatRelationship(item, domain, namespace = this.namespace) {
5171
+ return this.formatLinkage(
5172
+ domain,
5173
+ item.resourceName,
5174
+ String(item.getPrimaryKey()),
5175
+ namespace
5176
+ );
5177
+ }
5178
+ /**
5179
+ * Build a [JSON API](http://jsonapi.org) relationship object from resource
5180
+ * linkage, in the same shape `formatRelationship()` produces from Model
5181
+ * instances: to-one relationships carry a `links` object, to-many ones only
5182
+ * `data`, and a missing to-one relationship is `{ data: null }`.
4713
5183
  *
5184
+ * @method formatLinkage
4714
5185
  * @private
4715
5186
  */
4716
- async formatRelationship({
4717
- item,
4718
- domain,
4719
- include,
4720
- included
4721
- }) {
4722
- const { namespace } = this;
4723
- const {
4724
- resourceName: type,
4725
- constructor: { serializer }
4726
- } = item;
4727
- const id = String(item.getPrimaryKey());
4728
- let links;
4729
- if (namespace) {
4730
- links = {
4731
- self: `${domain}/${namespace}/${type}/${id}`
4732
- };
4733
- } else {
4734
- links = {
4735
- self: `${domain}/${type}/${id}`
5187
+ formatLinkage(domain, type, linkage, namespace = this.namespace) {
5188
+ if (Array.isArray(linkage)) {
5189
+ return {
5190
+ data: type ? linkage.map((id) => ({ id, type })) : []
4736
5191
  };
4737
5192
  }
4738
- if (include) {
4739
- included.push(
4740
- await serializer.formatOne({
4741
- item,
4742
- domain,
4743
- include: [],
4744
- included: [],
4745
- formatRelationships: false
4746
- })
4747
- );
5193
+ if (linkage == null || !type) {
5194
+ return {
5195
+ data: null
5196
+ };
4748
5197
  }
4749
5198
  return {
4750
5199
  data: {
4751
- id,
5200
+ id: linkage,
4752
5201
  type
4753
5202
  },
4754
- links
5203
+ links: {
5204
+ self: this.linkFor(domain, type, linkage, namespace)
5205
+ }
4755
5206
  };
4756
5207
  }
5208
+ /**
5209
+ * Add `records` (instances of `model`) to `included` as resource objects,
5210
+ * then recurse into the relationships named in `tree`. Each is serialized by
5211
+ * `model`'s Serializer in this Serializer's namespace (`serializerFor()`), so
5212
+ * `/admin/posts?include=comments` uses `AdminCommentsSerializer` when there
5213
+ * is one and `CommentsSerializer` otherwise. The relationships of every level
5214
+ * are batch-loaded with one query per relationship, not one per record.
5215
+ *
5216
+ * @method addIncluded
5217
+ * @private
5218
+ */
5219
+ async addIncluded({
5220
+ model,
5221
+ records,
5222
+ tree,
5223
+ domain,
5224
+ included,
5225
+ namespace
5226
+ }) {
5227
+ const serializer = this.serializerFor(model, namespace);
5228
+ const unique = Array.from(
5229
+ new Map(records.map((record) => [record.getPrimaryKey(), record])).values()
5230
+ );
5231
+ if (!unique.length) {
5232
+ return;
5233
+ }
5234
+ const names = [...serializer.hasOne, ...serializer.hasMany];
5235
+ const linkage = await loadLinkage(model, unique, names);
5236
+ for (const item of unique) {
5237
+ const id = String(item.getPrimaryKey());
5238
+ const key = resourceKey({ id, type: item.resourceName });
5239
+ if (!included.has(key)) {
5240
+ included.set(
5241
+ key,
5242
+ await serializer.formatOne({
5243
+ item,
5244
+ domain,
5245
+ linkage: linkage.get(id),
5246
+ namespace
5247
+ })
5248
+ );
5249
+ }
5250
+ }
5251
+ for (const [name, children] of tree) {
5252
+ const opts = model.relationshipFor(name);
5253
+ if (!opts || !names.includes(name)) {
5254
+ continue;
5255
+ }
5256
+ const { model: next } = opts;
5257
+ const ids = /* @__PURE__ */ new Set();
5258
+ linkage.forEach(({ [name]: value }) => {
5259
+ (Array.isArray(value) ? value : [value]).forEach((relatedId) => {
5260
+ if (relatedId != null) {
5261
+ ids.add(relatedId);
5262
+ }
5263
+ });
5264
+ });
5265
+ if (ids.size) {
5266
+ const attributes = this.serializerFor(
5267
+ next,
5268
+ namespace
5269
+ ).attributes.filter((attr) => next.attributeNames.includes(attr));
5270
+ const nextRecords = await next.select(next.primaryKey, ...attributes).where({ [next.primaryKey]: Array.from(ids) });
5271
+ await this.addIncluded({
5272
+ domain,
5273
+ included,
5274
+ namespace,
5275
+ model: next,
5276
+ records: nextRecords,
5277
+ tree: children
5278
+ });
5279
+ }
5280
+ }
5281
+ }
5282
+ /**
5283
+ * Resolve the Serializer for `model` in `namespace` (this Serializer's by
5284
+ * default), the way a namespaced Controller resolves its own:
5285
+ * `admin/comments` if it exists, otherwise the closest ancestor namespace's,
5286
+ * down to the root `comments` Serializer. Falls back to `model.serializer`
5287
+ * when this Serializer was not created by an application (e.g. in
5288
+ * isolation).
5289
+ *
5290
+ * Pass the request's namespace when there is one: a Serializer's own
5291
+ * namespace is the root one whenever it is a namespaced Controller's
5292
+ * fallback.
5293
+ *
5294
+ * @method serializerFor
5295
+ * @private
5296
+ */
5297
+ serializerFor(model, namespace = this.namespace) {
5298
+ const { serializers } = this;
5299
+ if (serializers) {
5300
+ const key = import_path3.posix.join(namespace || ".", model.resourceName);
5301
+ const serializer = serializers.get(key) || closestAncestor(serializers, key);
5302
+ if (serializer) {
5303
+ return serializer;
5304
+ }
5305
+ }
5306
+ return model.serializer;
5307
+ }
5308
+ /**
5309
+ * @private
5310
+ */
5311
+ linkFor(domain, type, id, namespace = this.namespace) {
5312
+ if (namespace) {
5313
+ return `${domain}/${namespace}/${type}/${id}`;
5314
+ }
5315
+ return `${domain}/${type}/${id}`;
5316
+ }
4757
5317
  };
5318
+ function resourceKey({ id, type }) {
5319
+ return `${type}:${id}`;
5320
+ }
4758
5321
  var serializer_default = Serializer;
4759
5322
 
4760
5323
  // src/packages/config/index.ts
@@ -4953,7 +5516,10 @@ function resource(action) {
4953
5516
  data,
4954
5517
  links,
4955
5518
  domain,
4956
- include
5519
+ include,
5520
+ // The request's namespace, not the serializer's: a namespaced
5521
+ // controller without its own serializer is given the root one.
5522
+ namespace
4957
5523
  });
4958
5524
  }
4959
5525
  return data;
@@ -5034,6 +5600,7 @@ var ParameterTypeError = class extends TypeError {
5034
5600
  Expected type '${type || "undefined"}' for parameter '${path}' but got
5035
5601
  '${actual}'.
5036
5602
  `);
5603
+ this.source = sourceFor(path);
5037
5604
  }
5038
5605
  };
5039
5606
  var parameter_type_error_default = createServerError(ParameterTypeError, 400);
@@ -5046,6 +5613,7 @@ var ParameterValueError = class extends TypeError {
5046
5613
  [${param.size ? Array.from(param.values()).join(", ") : ""}] but got
5047
5614
  ${String(actual)}.
5048
5615
  `);
5616
+ this.source = sourceFor(param.path);
5049
5617
  }
5050
5618
  };
5051
5619
  var parameter_value_error_default = createServerError(ParameterValueError, 400);
@@ -5054,6 +5622,7 @@ var parameter_value_error_default = createServerError(ParameterValueError, 400);
5054
5622
  var InvalidParameterError = class extends TypeError {
5055
5623
  constructor(path) {
5056
5624
  super(`'${path}' is not a valid parameter for this resource.`);
5625
+ this.source = sourceFor(path);
5057
5626
  }
5058
5627
  };
5059
5628
  var invalid_parameter_error_default = createServerError(InvalidParameterError, 400);
@@ -5069,6 +5638,7 @@ var ResourceMismatchError = class extends TypeError {
5069
5638
  Expected '${String(expected)}' for parameter '${path}' but got
5070
5639
  ${String(normalized)}.
5071
5640
  `);
5641
+ this.source = sourceFor(path);
5072
5642
  }
5073
5643
  };
5074
5644
  var resource_mismatch_error_default = createServerError(ResourceMismatchError, 409);
@@ -5077,14 +5647,34 @@ var resource_mismatch_error_default = createServerError(ResourceMismatchError, 4
5077
5647
  var ParameterRequiredError = class extends TypeError {
5078
5648
  constructor(path) {
5079
5649
  super(`Missing required parameter '${path}'.`);
5650
+ this.source = sourceFor(path);
5080
5651
  }
5081
5652
  };
5082
5653
  var parameter_required_error_default = createServerError(ParameterRequiredError, 400);
5083
5654
 
5655
+ // src/packages/router/route/params/errors/client-generated-id-error.ts
5656
+ var ClientGeneratedIdError = class extends TypeError {
5657
+ constructor() {
5658
+ super("Client-generated IDs are not supported for this resource.");
5659
+ this.source = sourceFor("data.id");
5660
+ }
5661
+ };
5662
+ var client_generated_id_error_default = createServerError(ClientGeneratedIdError, 403);
5663
+
5664
+ // src/packages/router/route/params/errors/forbidden-parameter-error.ts
5665
+ var ForbiddenParameterError = class extends TypeError {
5666
+ constructor(path) {
5667
+ super(`Setting '${path}' is not supported for this resource.`);
5668
+ this.source = sourceFor(path);
5669
+ }
5670
+ };
5671
+ var forbidden_parameter_error_default = createServerError(ForbiddenParameterError, 403);
5672
+
5084
5673
  // src/packages/router/route/params/errors/parameter-not-nullable-error.ts
5085
5674
  var ParameterNotNullableError = class extends TypeError {
5086
5675
  constructor({ path }) {
5087
5676
  super(`Parameter '${path}' is not nullable.`);
5677
+ this.source = sourceFor(path);
5088
5678
  }
5089
5679
  };
5090
5680
  var parameter_not_nullable_error_default = createServerError(ParameterNotNullableError, 400);
@@ -5213,13 +5803,14 @@ var Parameter = class extends set_default {
5213
5803
  path,
5214
5804
  type,
5215
5805
  required: Boolean(required),
5216
- sanitize: Boolean(sanitize)
5806
+ sanitize: Boolean(sanitize),
5807
+ restricted: values !== void 0
5217
5808
  });
5218
5809
  this.freeze();
5219
5810
  }
5220
5811
  validate(value) {
5221
5812
  validateType2(this, value);
5222
- if (this.size > 0) {
5813
+ if (this.restricted) {
5223
5814
  return validateValue(this, value);
5224
5815
  }
5225
5816
  return value;
@@ -5238,6 +5829,17 @@ function getURLParams(dynamicSegments) {
5238
5829
  ]);
5239
5830
  }
5240
5831
 
5832
+ // src/packages/router/route/params/parameter/forbidden-parameter.ts
5833
+ var ForbiddenParameter = class extends parameter_default {
5834
+ constructor(path) {
5835
+ super({ path });
5836
+ }
5837
+ validate() {
5838
+ throw new forbidden_parameter_error_default(this.path);
5839
+ }
5840
+ };
5841
+ var forbidden_parameter_default = ForbiddenParameter;
5842
+
5241
5843
  // src/packages/router/route/params/utils/get-data-params.ts
5242
5844
  function getIDParam({ model }) {
5243
5845
  const primaryKeyColumn = model.columnFor(model.primaryKey);
@@ -5290,16 +5892,48 @@ function getRelationshipsParam({
5290
5892
  model,
5291
5893
  params
5292
5894
  }) {
5895
+ const forbidden = Object.keys(model.relationships).filter((key) => !params.includes(key)).map((key) => [
5896
+ key,
5897
+ new forbidden_parameter_default(`data.relationships.${key}`)
5898
+ ]);
5293
5899
  return [
5294
5900
  "relationships",
5295
5901
  new parameter_group_default(
5296
- params.reduce((group, param) => {
5297
- const path = `data.relationships.${param}`;
5298
- const opts = model.relationshipFor(param);
5299
- if (!opts) {
5300
- return group;
5301
- }
5302
- if (opts.type === "hasMany") {
5902
+ [
5903
+ ...params.reduce((group, param) => {
5904
+ const path = `data.relationships.${param}`;
5905
+ const opts = model.relationshipFor(param);
5906
+ if (!opts) {
5907
+ return group;
5908
+ }
5909
+ if (opts.type === "hasMany") {
5910
+ return [
5911
+ ...group,
5912
+ [
5913
+ param,
5914
+ new parameter_group_default(
5915
+ [
5916
+ [
5917
+ "data",
5918
+ new parameter_default({
5919
+ type: "array",
5920
+ path: `${path}.data`,
5921
+ required: true
5922
+ })
5923
+ ]
5924
+ ],
5925
+ {
5926
+ path
5927
+ }
5928
+ )
5929
+ ]
5930
+ ];
5931
+ }
5932
+ const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5933
+ let primaryKeyType = "number";
5934
+ if (primaryKeyColumn) {
5935
+ primaryKeyType = typeForColumn(primaryKeyColumn);
5936
+ }
5303
5937
  return [
5304
5938
  ...group,
5305
5939
  [
@@ -5308,11 +5942,32 @@ function getRelationshipsParam({
5308
5942
  [
5309
5943
  [
5310
5944
  "data",
5311
- new parameter_default({
5312
- type: "array",
5313
- path: `${path}.data`,
5314
- required: true
5315
- })
5945
+ new parameter_group_default(
5946
+ [
5947
+ [
5948
+ "id",
5949
+ new parameter_default({
5950
+ type: primaryKeyType,
5951
+ path: `${path}.data.id`,
5952
+ required: true
5953
+ })
5954
+ ],
5955
+ [
5956
+ "type",
5957
+ new parameter_default({
5958
+ type: "string",
5959
+ path: `${path}.data.type`,
5960
+ values: [opts.model.resourceName],
5961
+ required: true
5962
+ })
5963
+ ]
5964
+ ],
5965
+ {
5966
+ type: "array",
5967
+ path: `${path}.data`,
5968
+ required: true
5969
+ }
5970
+ )
5316
5971
  ]
5317
5972
  ],
5318
5973
  {
@@ -5321,55 +5976,9 @@ function getRelationshipsParam({
5321
5976
  )
5322
5977
  ]
5323
5978
  ];
5324
- }
5325
- const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey);
5326
- let primaryKeyType = "number";
5327
- if (primaryKeyColumn) {
5328
- primaryKeyType = typeForColumn(primaryKeyColumn);
5329
- }
5330
- return [
5331
- ...group,
5332
- [
5333
- param,
5334
- new parameter_group_default(
5335
- [
5336
- [
5337
- "data",
5338
- new parameter_group_default(
5339
- [
5340
- [
5341
- "id",
5342
- new parameter_default({
5343
- type: primaryKeyType,
5344
- path: `${path}.data.id`,
5345
- required: true
5346
- })
5347
- ],
5348
- [
5349
- "type",
5350
- new parameter_default({
5351
- type: "string",
5352
- path: `${path}.data.type`,
5353
- values: [opts.model.resourceName],
5354
- required: true
5355
- })
5356
- ]
5357
- ],
5358
- {
5359
- type: "array",
5360
- path: `${path}.data`,
5361
- required: true
5362
- }
5363
- )
5364
- ]
5365
- ],
5366
- {
5367
- path
5368
- }
5369
- )
5370
- ]
5371
- ];
5372
- }, []),
5979
+ }, []),
5980
+ ...forbidden
5981
+ ],
5373
5982
  {
5374
5983
  path: "data.relationships"
5375
5984
  }
@@ -5477,10 +6086,9 @@ function getFilterParam({ filter }) {
5477
6086
  )
5478
6087
  ];
5479
6088
  }
5480
- function getFieldsParam({
5481
- model,
5482
- serializer: { hasOne, hasMany, attributes }
5483
- }) {
6089
+ function getFieldsParam(controller) {
6090
+ const { model, serializer } = controller;
6091
+ const { hasOne, hasMany, attributes } = serializer;
5484
6092
  const relationships = [...hasOne, ...hasMany];
5485
6093
  return [
5486
6094
  "fields",
@@ -5509,7 +6117,7 @@ function getFieldsParam({
5509
6117
  sanitize: true,
5510
6118
  values: [
5511
6119
  opts.model.primaryKey,
5512
- ...opts.model.serializer.attributes
6120
+ ...controller.serializerFor(opts.model).attributes
5513
6121
  ]
5514
6122
  })
5515
6123
  ]
@@ -5527,16 +6135,30 @@ function getFieldsParam({
5527
6135
  )
5528
6136
  ];
5529
6137
  }
5530
- function getIncludeParam({
5531
- serializer: { hasOne, hasMany }
5532
- }) {
6138
+ function getIncludeParam(controller) {
6139
+ const { model, maxIncludeDepth, serializer } = controller;
6140
+ const { hasOne, hasMany } = serializer;
5533
6141
  const relationships = [...hasOne, ...hasMany];
5534
6142
  return [
5535
6143
  "include",
5536
6144
  new parameter_default({
5537
6145
  path: "include",
5538
6146
  type: "array",
5539
- values: relationships
6147
+ // Every top level name stays allowed (as before), plus the nested paths
6148
+ // (`comments.user`) reachable through each related serializer in the
6149
+ // controller's namespace (at every level, even below a root fallback
6150
+ // serializer), down to the controller's `maxIncludeDepth`.
6151
+ values: Array.from(
6152
+ /* @__PURE__ */ new Set([
6153
+ ...relationships,
6154
+ ...enumerateIncludePaths(
6155
+ model,
6156
+ relationships,
6157
+ maxIncludeDepth,
6158
+ (related) => controller.serializerFor(related)
6159
+ )
6160
+ ])
6161
+ )
5540
6162
  })
5541
6163
  ];
5542
6164
  }
@@ -5574,6 +6196,15 @@ function getCollectionQueryParams(controller) {
5574
6196
  return getCustomParams(controller);
5575
6197
  }
5576
6198
 
6199
+ // src/packages/router/route/params/utils/validate-client-id.ts
6200
+ function validateClientId(params) {
6201
+ const { data } = params;
6202
+ if (data && typeof data === "object" && Reflect.has(data, "id")) {
6203
+ throw new client_generated_id_error_default();
6204
+ }
6205
+ return true;
6206
+ }
6207
+
5577
6208
  // src/packages/router/route/params/utils/validate-resource-id.ts
5578
6209
  function validateResourceId({
5579
6210
  params: {
@@ -5739,6 +6370,9 @@ var Route = class extends set_default {
5739
6370
  ...req.params,
5740
6371
  ...this.parseParams(req.url.params)
5741
6372
  };
6373
+ if (this.action === "create" && req.method === "POST") {
6374
+ validateClientId(params);
6375
+ }
5742
6376
  if (req.method !== "OPTIONS") {
5743
6377
  params = this.params.validate(params);
5744
6378
  }
@@ -6051,10 +6685,10 @@ var Router = class extends map_default {
6051
6685
  var router_default = Router;
6052
6686
 
6053
6687
  // src/packages/loader/utils/bundle-for.ts
6054
- var import_path2 = require("path");
6688
+ var import_path4 = require("path");
6055
6689
 
6056
6690
  // src/packages/loader/utils/format-key.ts
6057
- var import_inflection13 = require("inflection");
6691
+ var import_inflection15 = require("inflection");
6058
6692
  var NAMESPACE_DELIMITER = /\$-/g;
6059
6693
  function formatKey(key, formatter) {
6060
6694
  return chain(key).pipe((str) => {
@@ -6062,7 +6696,7 @@ function formatKey(key, formatter) {
6062
6696
  return formatter(str);
6063
6697
  }
6064
6698
  return str;
6065
- }).pipe(underscore).pipe(import_inflection13.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6699
+ }).pipe(underscore).pipe(import_inflection15.dasherize).pipe((str) => str.replace(NAMESPACE_DELIMITER, "/")).value();
6066
6700
  }
6067
6701
 
6068
6702
  // src/packages/loader/utils/bundle-for.ts
@@ -6129,31 +6763,15 @@ function normalize2(manifest) {
6129
6763
  }
6130
6764
  function bundleFor(path) {
6131
6765
  const manifest = Reflect.apply(require, null, [
6132
- (0, import_path2.join)(path, "dist", "bundle")
6766
+ (0, import_path4.join)(path, "dist", "bundle")
6133
6767
  ]);
6134
6768
  return chain(manifest).pipe(normalize2).pipe(entries).pipe((pairs) => new map_default(pairs)).value().freeze();
6135
6769
  }
6136
6770
 
6137
- // src/packages/loader/resolver/utils/closest-ancestor.ts
6138
- var import_path3 = require("path");
6139
- function closestAncestor(source, key) {
6140
- const name = import_path3.posix.basename(key);
6141
- let namespace = import_path3.posix.dirname(key);
6142
- if (namespace === ".") {
6143
- return source.get(name);
6144
- }
6145
- namespace = import_path3.posix.dirname(namespace);
6146
- const ancestor = source.get(import_path3.posix.join(namespace, name));
6147
- if (ancestor) {
6148
- return ancestor;
6149
- }
6150
- return closestAncestor(source, import_path3.posix.join(import_path3.posix.dirname(namespace), name));
6151
- }
6152
-
6153
6771
  // src/packages/loader/resolver/utils/closest-child.ts
6154
- var import_path4 = require("path");
6772
+ var import_path5 = require("path");
6155
6773
  function closestChild(source, key) {
6156
- const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path4.posix.basename(path), value]).filter(([resource2]) => key === resource2);
6774
+ const [[, result] = []] = Array.from(source).map(([path, value]) => [import_path5.posix.basename(path), value]).filter(([resource2]) => key === resource2);
6157
6775
  return result;
6158
6776
  }
6159
6777
 
@@ -6176,7 +6794,7 @@ function resolve(group) {
6176
6794
  }
6177
6795
 
6178
6796
  // src/packages/loader/builder/utils/create-parent-builder.ts
6179
- var import_path5 = require("path");
6797
+ var import_path6 = require("path");
6180
6798
 
6181
6799
  // src/packages/loader/builder/utils/sort-by-namespace.ts
6182
6800
  function sortByNamespace([a], [b]) {
@@ -6197,7 +6815,7 @@ function createParentBuilder(construct) {
6197
6815
  let grandparent = null;
6198
6816
  if (key !== "root") {
6199
6817
  const found = result.find((namespace) => {
6200
- const dirname = import_path5.posix.dirname(key);
6818
+ const dirname = import_path6.posix.dirname(key);
6201
6819
  if (namespace.key === "root") {
6202
6820
  return dirname === ".";
6203
6821
  }
@@ -6267,13 +6885,13 @@ function normalizePort(port) {
6267
6885
  }
6268
6886
 
6269
6887
  // src/packages/application/utils/create-controller.ts
6270
- var import_path6 = require("path");
6888
+ var import_path7 = require("path");
6271
6889
  function createController(constructor, opts) {
6272
6890
  const { key, store, serializers } = opts;
6273
- const namespace = import_path6.posix.dirname(key).replace(".", "");
6891
+ const namespace = import_path7.posix.dirname(key).replace(".", "");
6274
6892
  let { parent } = opts;
6275
6893
  let model = tryCatchSync(
6276
- () => store.modelFor(import_path6.posix.basename(key))
6894
+ () => store.modelFor(import_path7.posix.basename(key))
6277
6895
  );
6278
6896
  let serializer = serializers.get(key);
6279
6897
  if (!model) {
@@ -6329,13 +6947,13 @@ function createController(constructor, opts) {
6329
6947
  }
6330
6948
 
6331
6949
  // src/packages/application/utils/create-serializer.ts
6332
- var import_path7 = require("path");
6950
+ var import_path8 = require("path");
6333
6951
  function createSerializer(constructor, opts) {
6334
6952
  const { key, store } = opts;
6335
- const namespace = import_path7.posix.dirname(key).replace(".", "");
6953
+ const namespace = import_path8.posix.dirname(key).replace(".", "");
6336
6954
  let { parent } = opts;
6337
6955
  let model = tryCatchSync(
6338
- () => store.modelFor(import_path7.posix.basename(key))
6956
+ () => store.modelFor(import_path8.posix.basename(key))
6339
6957
  );
6340
6958
  if (!model) {
6341
6959
  model = null;
@@ -6359,6 +6977,85 @@ function createSerializer(constructor, opts) {
6359
6977
  return deepFreezeProps(instance, true, "hasOne", "hasMany", "attributes");
6360
6978
  }
6361
6979
 
6980
+ // src/packages/application/utils/validate-namespaced-serializers.ts
6981
+ var import_path9 = require("path");
6982
+
6983
+ // src/errors/namespaced-serializer-missing-error.ts
6984
+ var NamespacedSerializerMissingError = class extends ReferenceError {
6985
+ /**
6986
+ * @param missing - Each missing serializer key (`admin/comments`) with an
6987
+ * example of how it is reached (`admin/posts?include=comments`).
6988
+ */
6989
+ constructor(missing) {
6990
+ const lines = Array.from(missing).sort(([a], [b]) => a.localeCompare(b)).map(([key, via]) => ` - ${key} (reached from ${via})`);
6991
+ super(
6992
+ [
6993
+ "Missing namespaced serializers. These namespaces set `serializerFallback = false`, so every type their controllers can serialize or include needs a serializer of its own:",
6994
+ ...lines
6995
+ ].join("\n")
6996
+ );
6997
+ }
6998
+ };
6999
+ var namespaced_serializer_missing_error_default = NamespacedSerializerMissingError;
7000
+
7001
+ // src/packages/application/utils/validate-namespaced-serializers.ts
7002
+ var distance = (via) => {
7003
+ const [, include] = via.split("?include=");
7004
+ return include ? include.split(".").length : 0;
7005
+ };
7006
+ function validateNamespacedSerializers(controllers, serializers) {
7007
+ const missing = /* @__PURE__ */ new Map();
7008
+ controllers.forEach((controller, key) => {
7009
+ const { namespace, model } = controller;
7010
+ if (!namespace || !model) {
7011
+ return;
7012
+ }
7013
+ const application = controllers.get(import_path9.posix.join(namespace, "application"));
7014
+ if (!application || application.serializerFallback !== false) {
7015
+ return;
7016
+ }
7017
+ const require2 = (type, via) => {
7018
+ const serializerKey = import_path9.posix.join(namespace, type);
7019
+ const serializer = serializers.get(serializerKey);
7020
+ if (!serializer) {
7021
+ const known = missing.get(serializerKey);
7022
+ if (known === void 0 || distance(via) < distance(known)) {
7023
+ missing.set(serializerKey, via);
7024
+ }
7025
+ }
7026
+ return serializer;
7027
+ };
7028
+ const own = require2(model.resourceName, key);
7029
+ if (!own) {
7030
+ return;
7031
+ }
7032
+ const depth = Math.max(controller.maxIncludeDepth, 1);
7033
+ let level = [
7034
+ [model, own, []]
7035
+ ];
7036
+ for (let current = 1; current <= depth && level.length; current += 1) {
7037
+ const next = [];
7038
+ level.forEach(([parent, serializer, path]) => {
7039
+ [...serializer.hasOne, ...serializer.hasMany].forEach((name) => {
7040
+ const opts = parent.relationshipFor(name);
7041
+ if (!opts) {
7042
+ return;
7043
+ }
7044
+ const nextPath = [...path, name];
7045
+ const related = require2(opts.model.resourceName, `${key}?include=${nextPath.join(".")}`);
7046
+ if (related) {
7047
+ next.push([opts.model, related, nextPath]);
7048
+ }
7049
+ });
7050
+ });
7051
+ level = next;
7052
+ }
7053
+ });
7054
+ if (missing.size) {
7055
+ throw new namespaced_serializer_missing_error_default(missing);
7056
+ }
7057
+ }
7058
+
6362
7059
  // src/packages/application/initialize.ts
6363
7060
  async function initialize3(app, { path, port, logging, database, server: serverConfig }) {
6364
7061
  const load = createLoader(path);
@@ -6381,6 +7078,14 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6381
7078
  parent
6382
7079
  })
6383
7080
  );
7081
+ serializers.forEach((serializer) => {
7082
+ Reflect.defineProperty(serializer, "serializers", {
7083
+ value: serializers,
7084
+ writable: false,
7085
+ enumerable: false,
7086
+ configurable: false
7087
+ });
7088
+ });
6384
7089
  models.forEach((model) => {
6385
7090
  Reflect.defineProperty(model, "serializer", {
6386
7091
  value: closestChild(serializers, model.resourceName),
@@ -6406,6 +7111,7 @@ async function initialize3(app, { path, port, logging, database, server: serverC
6406
7111
  configurable: false
6407
7112
  });
6408
7113
  });
7114
+ validateNamespacedSerializers(controllers, serializers);
6409
7115
  const ApplicationController = controllers.get("application");
6410
7116
  if (!ApplicationController) {
6411
7117
  throw new controller_missing_error_default("application");