lumen-framework 3.1.1 → 4.0.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 (258) hide show
  1. package/README.md +75 -109
  2. package/bin/lumen +23 -41
  3. package/dist/cli.cjs +856 -479
  4. package/dist/cli.cjs.map +4 -4
  5. package/dist/index.js +3273 -1719
  6. package/dist/index.js.map +4 -4
  7. package/dist/index.mjs +3251 -1697
  8. package/dist/index.mjs.map +4 -4
  9. package/dist/testing.js +280 -0
  10. package/dist/testing.js.map +7 -0
  11. package/dist/testing.mjs +252 -0
  12. package/dist/testing.mjs.map +7 -0
  13. package/dist/types/errors/links-only-error.d.ts +13 -0
  14. package/dist/types/errors/reserved-field-name-error.d.ts +13 -0
  15. package/dist/types/errors/unknown-attribute-error.d.ts +13 -0
  16. package/dist/types/index.d.ts +14 -0
  17. package/dist/types/interfaces.d.ts +1 -1
  18. package/dist/types/packages/application/index.d.ts +38 -45
  19. package/dist/types/packages/application/initialize.d.ts +3 -5
  20. package/dist/types/packages/application/interfaces.d.ts +13 -5
  21. package/dist/types/packages/application/utils/create-controller.d.ts +14 -4
  22. package/dist/types/packages/application/utils/create-serializer.d.ts +2 -2
  23. package/dist/types/packages/application/utils/normalize-port.d.ts +1 -3
  24. package/dist/types/packages/application/utils/resolve-visibility.d.ts +15 -0
  25. package/dist/types/packages/application/utils/restrict-open-namespaces.d.ts +19 -0
  26. package/dist/types/packages/application/utils/validate-attributes.d.ts +17 -0
  27. package/dist/types/packages/application/utils/validate-links-only.d.ts +19 -0
  28. package/dist/types/packages/application/utils/validate-namespaced-serializers.d.ts +3 -3
  29. package/dist/types/packages/application/utils/validate-reserved-names.d.ts +12 -0
  30. package/dist/types/packages/application/utils/warn-query-param-names.d.ts +10 -0
  31. package/dist/types/packages/cli/commands/dbcreate.d.ts +1 -1
  32. package/dist/types/packages/cli/commands/dbdrop.d.ts +1 -1
  33. package/dist/types/packages/cli/commands/destroy.d.ts +3 -2
  34. package/dist/types/packages/cli/commands/generate.d.ts +5 -5
  35. package/dist/types/packages/cli/commands/index.d.ts +0 -1
  36. package/dist/types/packages/cli/generator/index.d.ts +6 -6
  37. package/dist/types/packages/cli/generator/interfaces.d.ts +3 -3
  38. package/dist/types/packages/cli/generator/utils/create-generator.d.ts +2 -2
  39. package/dist/types/packages/cli/generator/utils/generate-type.d.ts +9 -9
  40. package/dist/types/packages/cli/generator/utils/migration-conflict.d.ts +4 -4
  41. package/dist/types/packages/cli/templates/pnpm-workspace.d.ts +15 -0
  42. package/dist/types/packages/cli/utils/create-spinner.d.ts +14 -0
  43. package/dist/types/packages/cli/utils/print-statements.d.ts +12 -0
  44. package/dist/types/packages/cli/utils/server-database.d.ts +27 -0
  45. package/dist/types/packages/compiler/interfaces.d.ts +1 -1
  46. package/dist/types/packages/config/interfaces.d.ts +10 -4
  47. package/dist/types/packages/controller/constants.d.ts +7 -2
  48. package/dist/types/packages/controller/errors/related-record-not-found-error.d.ts +4 -4
  49. package/dist/types/packages/controller/index.d.ts +364 -473
  50. package/dist/types/packages/controller/interfaces.d.ts +21 -6
  51. package/dist/types/packages/controller/utils/find-many.d.ts +2 -5
  52. package/dist/types/packages/controller/utils/find-one.d.ts +2 -5
  53. package/dist/types/packages/controller/utils/params-to-query.d.ts +8 -10
  54. package/dist/types/packages/controller/utils/resolve-relationships.d.ts +1 -3
  55. package/dist/types/packages/controller/utils/validate-relationships.d.ts +5 -3
  56. package/dist/types/packages/controller/visibility/errors.d.ts +21 -0
  57. package/dist/types/packages/controller/visibility/index.d.ts +51 -0
  58. package/dist/types/packages/database/attribute/index.d.ts +4 -6
  59. package/dist/types/packages/database/attribute/interfaces.d.ts +1 -1
  60. package/dist/types/packages/database/attribute/utils/create-attribute.d.ts +3 -5
  61. package/dist/types/packages/database/attribute/utils/create-getter.d.ts +2 -2
  62. package/dist/types/packages/database/attribute/utils/create-setter.d.ts +3 -5
  63. package/dist/types/packages/database/constants.d.ts +1 -0
  64. package/dist/types/packages/database/errors/index.d.ts +1 -0
  65. package/dist/types/packages/database/errors/invalid-driver-error.d.ts +1 -3
  66. package/dist/types/packages/database/errors/migrations-pending-error.d.ts +1 -3
  67. package/dist/types/packages/database/errors/model-missing-error.d.ts +1 -3
  68. package/dist/types/packages/database/errors/relationship-config-error.d.ts +10 -0
  69. package/dist/types/packages/database/errors/unique-constraint-error.d.ts +1 -1
  70. package/dist/types/packages/database/index.d.ts +9 -6
  71. package/dist/types/packages/database/initialize.d.ts +3 -5
  72. package/dist/types/packages/database/interfaces.d.ts +115 -25
  73. package/dist/types/packages/database/migration/index.d.ts +5 -7
  74. package/dist/types/packages/database/migration/interfaces.d.ts +2 -4
  75. package/dist/types/packages/database/migration/utils/generate-timestamp.d.ts +9 -1
  76. package/dist/types/packages/database/model/index.d.ts +348 -759
  77. package/dist/types/packages/database/model/initialize-class.d.ts +7 -1
  78. package/dist/types/packages/database/model/interfaces.d.ts +30 -12
  79. package/dist/types/packages/database/model/utils/attribute.d.ts +14 -0
  80. package/dist/types/packages/database/model/utils/get-columns.d.ts +1 -3
  81. package/dist/types/packages/database/model/utils/persistence.d.ts +5 -14
  82. package/dist/types/packages/database/model/utils/process-write-error.d.ts +2 -2
  83. package/dist/types/packages/database/model/utils/run-hooks.d.ts +7 -3
  84. package/dist/types/packages/database/model/utils/validate.d.ts +4 -1
  85. package/dist/types/packages/database/query/errors/record-not-found-error.d.ts +1 -1
  86. package/dist/types/packages/database/query/index.d.ts +179 -3
  87. package/dist/types/packages/database/query/runner/index.d.ts +1 -3
  88. package/dist/types/packages/database/query/runner/utils/build-results.d.ts +3 -4
  89. package/dist/types/packages/database/query/utils/format-select.d.ts +1 -3
  90. package/dist/types/packages/database/relationship/index.d.ts +7 -6
  91. package/dist/types/packages/database/relationship/interfaces.d.ts +16 -3
  92. package/dist/types/packages/database/relationship/utils/getters.d.ts +7 -13
  93. package/dist/types/packages/database/relationship/utils/inverse-setters.d.ts +5 -9
  94. package/dist/types/packages/database/relationship/utils/setters.d.ts +7 -13
  95. package/dist/types/packages/database/relationship/utils/unassociate.d.ts +1 -3
  96. package/dist/types/packages/database/relationship/utils/update-relationship.d.ts +6 -2
  97. package/dist/types/packages/database/transaction/index.d.ts +9 -10
  98. package/dist/types/packages/database/transaction/interfaces.d.ts +7 -1
  99. package/dist/types/packages/database/utils/connect.d.ts +16 -3
  100. package/dist/types/packages/database/utils/create-migrations.d.ts +1 -3
  101. package/dist/types/packages/database/utils/normalize-model-name.d.ts +1 -3
  102. package/dist/types/packages/database/utils/pending-migrations.d.ts +1 -3
  103. package/dist/types/packages/database/utils/primary-key-type.d.ts +10 -0
  104. package/dist/types/packages/database/utils/type-for-column.d.ts +3 -5
  105. package/dist/types/packages/database/utils/validate-relationships.d.ts +13 -0
  106. package/dist/types/packages/database/validation/errors/validation-error.d.ts +4 -4
  107. package/dist/types/packages/database/validation/index.d.ts +3 -5
  108. package/dist/types/packages/database/validation/interfaces.d.ts +1 -1
  109. package/dist/types/packages/freezeable/map/index.d.ts +1 -3
  110. package/dist/types/packages/freezeable/set/index.d.ts +1 -3
  111. package/dist/types/packages/freezeable/utils/freeze.d.ts +5 -15
  112. package/dist/types/packages/freezeable/utils/is-frozen.d.ts +1 -3
  113. package/dist/types/packages/fs/index.d.ts +7 -7
  114. package/dist/types/packages/fs/interfaces.d.ts +4 -4
  115. package/dist/types/packages/fs/utils/parse-path.d.ts +2 -2
  116. package/dist/types/packages/fs/watcher/interfaces.d.ts +1 -1
  117. package/dist/types/packages/jsonapi/errors/invalid-content-type-error.d.ts +3 -5
  118. package/dist/types/packages/jsonapi/errors/not-acceptable-error.d.ts +2 -4
  119. package/dist/types/packages/jsonapi/errors/unsupported-media-type-error.d.ts +2 -4
  120. package/dist/types/packages/jsonapi/index.d.ts +1 -1
  121. package/dist/types/packages/jsonapi/interfaces.d.ts +47 -36
  122. package/dist/types/packages/jsonapi/utils/has-media-type-params.d.ts +1 -1
  123. package/dist/types/packages/jsonapi/utils/is-jsonapi.d.ts +1 -1
  124. package/dist/types/packages/jsonapi/utils/media-type.d.ts +2 -2
  125. package/dist/types/packages/loader/builder/index.d.ts +3 -3
  126. package/dist/types/packages/loader/builder/interfaces.d.ts +7 -7
  127. package/dist/types/packages/loader/builder/utils/create-children-builder.d.ts +2 -2
  128. package/dist/types/packages/loader/builder/utils/create-parent-builder.d.ts +9 -2
  129. package/dist/types/packages/loader/builder/utils/sort-by-namespace.d.ts +2 -2
  130. package/dist/types/packages/loader/index.d.ts +1 -1
  131. package/dist/types/packages/loader/interfaces.d.ts +2 -2
  132. package/dist/types/packages/loader/resolver/index.d.ts +2 -2
  133. package/dist/types/packages/loader/resolver/utils/closest-ancestor.d.ts +2 -2
  134. package/dist/types/packages/loader/resolver/utils/closest-child.d.ts +2 -2
  135. package/dist/types/packages/logger/constants.d.ts +3 -3
  136. package/dist/types/packages/logger/errors/invalid-config-error.d.ts +5 -0
  137. package/dist/types/packages/logger/index.d.ts +65 -142
  138. package/dist/types/packages/logger/interfaces.d.ts +52 -10
  139. package/dist/types/packages/logger/request-logger/index.d.ts +3 -5
  140. package/dist/types/packages/logger/request-logger/interfaces.d.ts +5 -5
  141. package/dist/types/packages/logger/request-logger/templates.d.ts +5 -9
  142. package/dist/types/packages/logger/request-logger/utils/filter-params.d.ts +6 -1
  143. package/dist/types/packages/logger/request-logger/utils/log-json.d.ts +2 -4
  144. package/dist/types/packages/logger/request-logger/utils/log-text.d.ts +1 -3
  145. package/dist/types/packages/logger/request-logger/utils/params-for.d.ts +9 -0
  146. package/dist/types/packages/logger/utils/error-name.d.ts +7 -0
  147. package/dist/types/packages/logger/utils/line.d.ts +1 -3
  148. package/dist/types/packages/logger/writer/constants.d.ts +0 -1
  149. package/dist/types/packages/logger/writer/index.d.ts +6 -6
  150. package/dist/types/packages/logger/writer/interfaces.d.ts +2 -2
  151. package/dist/types/packages/logger/writer/utils/format-message.d.ts +2 -2
  152. package/dist/types/packages/lumenify/index.d.ts +20 -5
  153. package/dist/types/packages/lumenify/utils/create-response-proxy.d.ts +1 -1
  154. package/dist/types/packages/pm/cluster/index.d.ts +56 -7
  155. package/dist/types/packages/pm/cluster/interfaces.d.ts +2 -1
  156. package/dist/types/packages/pm/index.d.ts +2 -2
  157. package/dist/types/packages/router/definitions/context/index.d.ts +5 -7
  158. package/dist/types/packages/router/definitions/context/utils/create-definition-group.d.ts +3 -5
  159. package/dist/types/packages/router/definitions/context/utils/create-definition.d.ts +11 -6
  160. package/dist/types/packages/router/definitions/context/utils/normalize-resource-args.d.ts +4 -5
  161. package/dist/types/packages/router/definitions/index.d.ts +5 -9
  162. package/dist/types/packages/router/definitions/interfaces.d.ts +4 -4
  163. package/dist/types/packages/router/index.d.ts +23 -11
  164. package/dist/types/packages/router/interfaces.d.ts +4 -4
  165. package/dist/types/packages/router/namespace/index.d.ts +7 -9
  166. package/dist/types/packages/router/namespace/interfaces.d.ts +3 -3
  167. package/dist/types/packages/router/namespace/utils/normalize-name.d.ts +1 -3
  168. package/dist/types/packages/router/namespace/utils/normalize-path.d.ts +1 -3
  169. package/dist/types/packages/router/resource/index.d.ts +7 -8
  170. package/dist/types/packages/router/resource/interfaces.d.ts +12 -4
  171. package/dist/types/packages/router/resource/utils/normalize-only.d.ts +3 -5
  172. package/dist/types/packages/router/route/action/enhancers/resource.d.ts +1 -3
  173. package/dist/types/packages/router/route/action/enhancers/track-perf.d.ts +1 -3
  174. package/dist/types/packages/router/route/action/index.d.ts +7 -2
  175. package/dist/types/packages/router/route/action/interfaces.d.ts +4 -0
  176. package/dist/types/packages/router/route/action/utils/create-page-links.d.ts +21 -5
  177. package/dist/types/packages/router/route/action/utils/get-action-name.d.ts +1 -3
  178. package/dist/types/packages/router/route/action/utils/get-controller-name.d.ts +1 -3
  179. package/dist/types/packages/router/route/index.d.ts +13 -9
  180. package/dist/types/packages/router/route/interfaces.d.ts +7 -5
  181. package/dist/types/packages/router/route/params/errors/client-generated-id-error.d.ts +4 -4
  182. package/dist/types/packages/router/route/params/errors/forbidden-parameter-error.d.ts +4 -4
  183. package/dist/types/packages/router/route/params/errors/index.d.ts +1 -0
  184. package/dist/types/packages/router/route/params/errors/invalid-parameter-error.d.ts +4 -6
  185. package/dist/types/packages/router/route/params/errors/parameter-not-nullable-error.d.ts +4 -6
  186. package/dist/types/packages/router/route/params/errors/parameter-range-error.d.ts +9 -0
  187. package/dist/types/packages/router/route/params/errors/parameter-required-error.d.ts +4 -6
  188. package/dist/types/packages/router/route/params/errors/parameter-type-error.d.ts +4 -6
  189. package/dist/types/packages/router/route/params/errors/parameter-value-error.d.ts +4 -6
  190. package/dist/types/packages/router/route/params/errors/resource-mismatch-error.d.ts +4 -6
  191. package/dist/types/packages/router/route/params/index.d.ts +5 -9
  192. package/dist/types/packages/router/route/params/interfaces.d.ts +12 -8
  193. package/dist/types/packages/router/route/params/parameter/forbidden-parameter.d.ts +1 -1
  194. package/dist/types/packages/router/route/params/parameter/ignored-parameter.d.ts +14 -0
  195. package/dist/types/packages/router/route/params/parameter/index.d.ts +21 -5
  196. package/dist/types/packages/router/route/params/parameter/interfaces.d.ts +2 -2
  197. package/dist/types/packages/router/route/params/parameter/utils/validate-range.d.ts +3 -0
  198. package/dist/types/packages/router/route/params/parameter/utils/validate-value.d.ts +1 -3
  199. package/dist/types/packages/router/route/params/parameter-group/index.d.ts +3 -5
  200. package/dist/types/packages/router/route/params/parameter-group/utils/missing-params.d.ts +8 -0
  201. package/dist/types/packages/router/route/params/utils/get-data-params.d.ts +5 -1
  202. package/dist/types/packages/router/route/params/utils/get-default-collection-params.d.ts +1 -3
  203. package/dist/types/packages/router/route/params/utils/get-default-member-params.d.ts +5 -2
  204. package/dist/types/packages/router/route/params/utils/get-query-params.d.ts +3 -9
  205. package/dist/types/packages/router/route/params/utils/get-url-params.d.ts +1 -3
  206. package/dist/types/packages/router/route/params/utils/parse-column-value.d.ts +16 -0
  207. package/dist/types/packages/router/route/params/utils/validate-client-id.d.ts +1 -1
  208. package/dist/types/packages/router/route/params/utils/validate-resource-id.d.ts +1 -3
  209. package/dist/types/packages/router/route/params/utils/validate-type.d.ts +3 -5
  210. package/dist/types/packages/router/route/utils/get-dynamic-segments.d.ts +1 -3
  211. package/dist/types/packages/router/route/utils/get-static-path.d.ts +6 -2
  212. package/dist/types/packages/router/utils/create-replacer.d.ts +10 -2
  213. package/dist/types/packages/serializer/index.d.ts +243 -434
  214. package/dist/types/packages/serializer/interfaces.d.ts +19 -1
  215. package/dist/types/packages/serializer/utils/include-tree.d.ts +12 -3
  216. package/dist/types/packages/serializer/utils/load-linkage.d.ts +9 -3
  217. package/dist/types/packages/server/errors/error-list.d.ts +25 -0
  218. package/dist/types/packages/server/errors/method-not-allowed-error.d.ts +11 -0
  219. package/dist/types/packages/server/index.d.ts +9 -9
  220. package/dist/types/packages/server/interfaces.d.ts +59 -7
  221. package/dist/types/packages/server/request/constants.d.ts +2 -2
  222. package/dist/types/packages/server/request/index.d.ts +3 -5
  223. package/dist/types/packages/server/request/interfaces.d.ts +72 -8
  224. package/dist/types/packages/server/request/parser/errors/malformed-request-error.d.ts +3 -5
  225. package/dist/types/packages/server/request/parser/index.d.ts +6 -1
  226. package/dist/types/packages/server/request/parser/utils/format.d.ts +13 -11
  227. package/dist/types/packages/server/request/parser/utils/normalize-document.d.ts +13 -0
  228. package/dist/types/packages/server/request/parser/utils/parse-nested-object.d.ts +1 -3
  229. package/dist/types/packages/server/request/parser/utils/parse-read.d.ts +2 -4
  230. package/dist/types/packages/server/request/parser/utils/parse-write.d.ts +17 -1
  231. package/dist/types/packages/server/request/utils/get-domain.d.ts +1 -3
  232. package/dist/types/packages/server/responder/index.d.ts +1 -3
  233. package/dist/types/packages/server/responder/utils/content-type-for.d.ts +8 -0
  234. package/dist/types/packages/server/responder/utils/data-for.d.ts +3 -5
  235. package/dist/types/packages/server/responder/utils/normalize.d.ts +2 -3
  236. package/dist/types/packages/server/response/index.d.ts +3 -5
  237. package/dist/types/packages/server/response/interfaces.d.ts +14 -3
  238. package/dist/types/packages/server/utils/client-ip-for.d.ts +10 -0
  239. package/dist/types/packages/server/utils/create-server-error.d.ts +11 -3
  240. package/dist/types/packages/server/utils/request-id-for.d.ts +9 -0
  241. package/dist/types/packages/server/utils/set-cors-headers.d.ts +2 -2
  242. package/dist/types/packages/server/utils/source-for.d.ts +19 -3
  243. package/dist/types/packages/server/utils/status-for-error.d.ts +6 -0
  244. package/dist/types/packages/server/utils/validate-accept.d.ts +1 -1
  245. package/dist/types/packages/server/utils/validate-content-type.d.ts +8 -3
  246. package/dist/types/packages/testing/audit-visibility.d.ts +189 -0
  247. package/dist/types/packages/testing/index.d.ts +4 -0
  248. package/dist/types/packages/testing/start-app.d.ts +52 -0
  249. package/dist/types/packages/testing/utils/identifiers-in.d.ts +14 -0
  250. package/dist/types/testing.d.ts +9 -0
  251. package/dist/types/utils/chalk.d.ts +16 -0
  252. package/dist/types/utils/pick.d.ts +2 -2
  253. package/package.json +54 -26
  254. package/dist/types/packages/cli/commands/test.d.ts +0 -4
  255. package/dist/types/packages/logger/utils/sql.d.ts +0 -4
  256. package/dist/types/packages/router/route/params/parameter-group/utils/has-required-params.d.ts +0 -5
  257. package/dist/types/utils/create-query-string.d.ts +0 -6
  258. package/dist/types/utils/has-own-property.d.ts +0 -1
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ auditVisibility: () => auditVisibility,
24
+ startApp: () => startApp
25
+ });
26
+ module.exports = __toCommonJS(testing_exports);
27
+
28
+ // src/packages/testing/utils/identifiers-in.ts
29
+ var isIdentifier = (value) => typeof value === "object" && value !== null && "type" in value && "id" in value;
30
+ function identifiersIn(document) {
31
+ const found = [];
32
+ const add = (value) => {
33
+ if (isIdentifier(value)) {
34
+ found.push({ type: value.type, id: String(value.id) });
35
+ }
36
+ };
37
+ const addLinkage = (linkage) => {
38
+ (Array.isArray(linkage) ? linkage : [linkage]).forEach(add);
39
+ };
40
+ const addResource = (resource) => {
41
+ add(resource);
42
+ if (isIdentifier(resource)) {
43
+ Object.values(resource.relationships ?? {}).forEach(
44
+ (relationship) => addLinkage(relationship?.data)
45
+ );
46
+ }
47
+ };
48
+ if (typeof document !== "object" || document === null) {
49
+ return found;
50
+ }
51
+ const { data, included } = document;
52
+ (Array.isArray(data) ? data : [data]).forEach(addResource);
53
+ (included ?? []).forEach(addResource);
54
+ return found;
55
+ }
56
+
57
+ // src/packages/testing/audit-visibility.ts
58
+ var READS = /* @__PURE__ */ new Set(["index", "show", "showRelationship", "showRelated"]);
59
+ var DENIED = /* @__PURE__ */ new Set([401, 403, 404]);
60
+ var MAX_PAGES = 1e3;
61
+ function originOf(app) {
62
+ const address = app.server.instance.address();
63
+ if (!address) {
64
+ throw new Error(
65
+ "auditVisibility: the application is not listening. Start it (`startApp()`), or pass the `origin` of the server to audit."
66
+ );
67
+ }
68
+ return `http://localhost:${address.port}`;
69
+ }
70
+ function allows(visible, id) {
71
+ if (visible === true) {
72
+ return true;
73
+ }
74
+ if (typeof visible === "function") {
75
+ return visible(id);
76
+ }
77
+ return Boolean(visible?.some((allowed) => String(allowed) === id));
78
+ }
79
+ function includesOf(route) {
80
+ const include = route.params.get("include");
81
+ const paths = include ? Array.from(include) : [];
82
+ const longest = paths.filter(
83
+ (path) => !paths.some((other) => other.startsWith(`${path}.`))
84
+ );
85
+ return longest.length ? [void 0, longest.join(",")] : [void 0];
86
+ }
87
+ async function auditVisibility(app, {
88
+ namespace = "",
89
+ visible,
90
+ headers = {},
91
+ ids = {},
92
+ query: required = {},
93
+ origin = originOf(app),
94
+ onDocument
95
+ }) {
96
+ const audit = { requests: [], violations: [], seen: {} };
97
+ const seen = /* @__PURE__ */ new Map();
98
+ const routes = Array.from(app.router.values()).filter(
99
+ (route) => route.method === "GET" && READS.has(route.action) && route.controller.namespace === namespace
100
+ );
101
+ if (!routes.length) {
102
+ throw new Error(
103
+ `auditVisibility: namespace '/${namespace}' serves no index, show, relationship or related routes.`
104
+ );
105
+ }
106
+ const relative = (link) => {
107
+ const { pathname, search } = new URL(link, origin);
108
+ return pathname + search;
109
+ };
110
+ const idsCache = /* @__PURE__ */ new Map();
111
+ const idsOf = async (route) => {
112
+ const { model } = route.controller;
113
+ const type = model.resourceName;
114
+ let list = idsCache.get(type);
115
+ if (!list) {
116
+ const given = ids[type];
117
+ list = given ? given.map(String) : (await model.select(model.primaryKey)).map((record) => String(record.getPrimaryKey()));
118
+ idsCache.set(type, list);
119
+ }
120
+ return list;
121
+ };
122
+ const check = (url, status, found) => {
123
+ found.forEach(({ type, id }) => {
124
+ if (!seen.has(type)) {
125
+ seen.set(type, /* @__PURE__ */ new Set());
126
+ }
127
+ seen.get(type)?.add(id);
128
+ if (!allows(visible[type], id)) {
129
+ audit.violations.push({ url, status, type, id });
130
+ }
131
+ });
132
+ };
133
+ const read = async (first, route) => {
134
+ let url = first;
135
+ const hiddenOwner = route.action.startsWith("showRel") && route.id !== void 0 && !allows(visible[route.type], route.id);
136
+ for (let page = 0; url && page < MAX_PAGES; page += 1) {
137
+ audit.requests.push(url);
138
+ const res = await fetch(origin + url, {
139
+ headers: { Accept: "application/vnd.api+json", ...headers }
140
+ });
141
+ if (!res.ok && !DENIED.has(res.status)) {
142
+ audit.violations.push({ url, status: res.status });
143
+ await res.body?.cancel();
144
+ return;
145
+ }
146
+ if (res.ok && hiddenOwner && page === 0) {
147
+ audit.violations.push({
148
+ url,
149
+ status: res.status,
150
+ type: route.type,
151
+ id: route.id,
152
+ message: `answered for ${route.type} ${route.id}, which the request may not see`
153
+ });
154
+ }
155
+ if (!res.ok || !res.headers.get("content-type")?.includes("json")) {
156
+ await res.body?.cancel();
157
+ return;
158
+ }
159
+ const document = await res.json();
160
+ check(url, res.status, identifiersIn(document));
161
+ if (onDocument && document) {
162
+ const found = await onDocument({
163
+ url,
164
+ status: res.status,
165
+ route,
166
+ document
167
+ });
168
+ const messages = typeof found === "string" ? [found] : Array.from(found ?? []);
169
+ const at = { url, status: res.status };
170
+ messages.forEach((message) => {
171
+ audit.violations.push({ ...at, message });
172
+ });
173
+ }
174
+ const next = document?.links?.next;
175
+ url = typeof next === "string" ? relative(next) : void 0;
176
+ }
177
+ };
178
+ for (const route of routes) {
179
+ const paged = route.params.get("page") ? (route.related ?? route.controller).maxPerPage : void 0;
180
+ const served = route.type === "relationship" ? void 0 : (route.related ?? route.controller).model.resourceName;
181
+ const query = (include) => {
182
+ const params = new URLSearchParams(served ? required[served] : {});
183
+ if (include) {
184
+ params.set("include", include);
185
+ }
186
+ if (paged) {
187
+ params.set("page[size]", String(paged));
188
+ }
189
+ const search = params.toString();
190
+ return search ? `?${search}` : "";
191
+ };
192
+ const about = {
193
+ action: route.action,
194
+ type: route.controller.model.resourceName,
195
+ ...route.relationship ? { relationship: route.relationship } : {}
196
+ };
197
+ const targets = route.dynamicSegments.length ? (await idsOf(route)).map((id) => [
198
+ route.path.replace(":id", encodeURIComponent(id)),
199
+ { ...about, id }
200
+ ]) : [[route.path, about]];
201
+ for (const [path, target] of targets) {
202
+ for (const include of includesOf(route)) {
203
+ await read(path + query(include), target);
204
+ }
205
+ }
206
+ }
207
+ seen.forEach((list, type) => {
208
+ audit.seen[type] = Array.from(list);
209
+ });
210
+ return audit;
211
+ }
212
+
213
+ // src/packages/testing/start-app.ts
214
+ var import_events = require("events");
215
+ var import_module = require("module");
216
+ var import_path = require("path");
217
+ var started = /* @__PURE__ */ new Set();
218
+ var failed = /* @__PURE__ */ new Map();
219
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
220
+ async function startApp(path, { env, port = 0 } = {}) {
221
+ const root = (0, import_path.resolve)(path);
222
+ const bundlePath = (0, import_path.join)(root, "dist", "bundle.js");
223
+ if (started.has(bundlePath)) {
224
+ throw new Error(
225
+ `startApp: ${root} was already started in this process. Its models are set up once per process: start it once and share it.`
226
+ );
227
+ }
228
+ const previous = process.env.NODE_ENV;
229
+ const restoreEnv = () => {
230
+ if (previous === void 0) {
231
+ delete process.env.NODE_ENV;
232
+ } else {
233
+ process.env.NODE_ENV = previous;
234
+ }
235
+ };
236
+ process.env.NODE_ENV = env ?? previous ?? "test";
237
+ started.add(bundlePath);
238
+ let app;
239
+ try {
240
+ const {
241
+ Application: App,
242
+ config,
243
+ database
244
+ } = (0, import_module.createRequire)(bundlePath)(bundlePath);
245
+ app = await new App({ ...config, database, path: root, port });
246
+ if (!app.server.instance.listening) {
247
+ await (0, import_events.once)(app.server.instance, "listening");
248
+ }
249
+ } catch (error) {
250
+ started.delete(bundlePath);
251
+ restoreEnv();
252
+ const earlier = failed.get(bundlePath);
253
+ if (failed.has(bundlePath)) {
254
+ throw new Error(
255
+ `startApp: booting ${root} failed: ${messageOf(error)}. An earlier attempt in this process failed first, with: ${messageOf(earlier)}. A failed boot can leave the app half set up: fix that one first.`,
256
+ { cause: error }
257
+ );
258
+ }
259
+ failed.set(bundlePath, error);
260
+ throw error;
261
+ }
262
+ const { port: listening } = app.server.instance.address();
263
+ return {
264
+ app,
265
+ origin: `http://localhost:${listening}`,
266
+ close: async () => {
267
+ try {
268
+ await app.close();
269
+ } finally {
270
+ restoreEnv();
271
+ }
272
+ }
273
+ };
274
+ }
275
+ // Annotate the CommonJS export names for ESM import in node:
276
+ 0 && (module.exports = {
277
+ auditVisibility,
278
+ startApp
279
+ });
280
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/testing.ts", "../src/packages/testing/utils/identifiers-in.ts", "../src/packages/testing/audit-visibility.ts", "../src/packages/testing/start-app.ts"],
4
+ "sourcesContent": ["/**\n * Helpers for an app's tests, imported from `lumen-framework/testing`. They\n * are kept out of the main entry, so they never reach an app's runtime\n * bundle.\n *\n * @module lumen-framework/testing\n */\nexport { auditVisibility, startApp } from './packages/testing';\nexport type {\n AuditedDocument,\n AuditedRoute,\n AuditVisibilityOptions,\n DocumentCheckResult,\n StartAppOptions,\n StartedApp,\n VisibilityAudit,\n VisibilityViolation,\n VisibleRecords\n} from './packages/testing';\n", "/** A resource's `type` and `id`. */\nexport type Identifier = { type: string; id: string };\n\ntype Linkage = Identifier | Array<Identifier> | null | undefined;\n\ntype Resource = Identifier & {\n relationships?: Record<string, { data?: Linkage }>;\n};\n\nconst isIdentifier = (value: unknown): value is Identifier =>\n typeof value === 'object' &&\n value !== null &&\n 'type' in value &&\n 'id' in value;\n\n/**\n * Every resource a JSON:API document names: its primary data, its `included`\n * resources, and the linkage of each one's relationships. A relationship\n * endpoint's primary data is linkage itself, so it is covered the same way.\n * Ids are strings, as JSON:API sends them.\n *\n * @internal\n */\nexport default function identifiersIn(document: unknown): Array<Identifier> {\n const found: Array<Identifier> = [];\n\n const add = (value: unknown) => {\n if (isIdentifier(value)) {\n found.push({ type: value.type, id: String(value.id) });\n }\n };\n\n const addLinkage = (linkage: Linkage) => {\n (Array.isArray(linkage) ? linkage : [linkage]).forEach(add);\n };\n\n const addResource = (resource: unknown) => {\n add(resource);\n\n if (isIdentifier(resource)) {\n Object.values((resource as Resource).relationships ?? {}).forEach(\n relationship => addLinkage(relationship?.data)\n );\n }\n };\n\n if (typeof document !== 'object' || document === null) {\n return found;\n }\n\n const { data, included } = document as {\n data?: unknown;\n included?: Array<unknown>;\n };\n\n (Array.isArray(data) ? data : [data]).forEach(addResource);\n (included ?? []).forEach(addResource);\n\n return found;\n}\n", "import type { AddressInfo } from 'net';\n\nimport type Application from '../application';\nimport type { Model } from '../database';\nimport type Route from '../router/route';\n\nimport identifiersIn from './utils/identifiers-in';\nimport type { Identifier } from './utils/identifiers-in';\n\n/**\n * Which records of one type a request may see, for {@link auditVisibility}:\n * every one (`true`), the ids listed, or the ids a function accepts.\n */\nexport type VisibleRecords =\n true | ReadonlyArray<string | number> | ((id: string) => boolean);\n\n/** The options of {@link auditVisibility}. */\nexport type AuditVisibilityOptions = {\n /**\n * The namespace to audit: `''` (the default) for the root, `'admin'` for\n * `/admin`. Nested namespaces are audited on their own.\n */\n namespace?: string;\n\n /**\n * What the request may see, keyed by type. Written by hand, independently\n * of the app's visibility rules: it is what they are checked against. A\n * type left out may not appear at all.\n */\n visible: Record<string, VisibleRecords>;\n\n /**\n * Headers sent with every request, such as the `Authorization` of the user\n * the audit is for.\n */\n headers?: Record<string, string>;\n\n /**\n * The ids to request each type's routes with (`GET /posts/:id`,\n * `/posts/:id/comments`, \u2026), keyed by type: visible and hidden ones\n * alike. A type left out is requested with every id in the database, so\n * keep the database small or list the ids.\n */\n ids?: Record<string, ReadonlyArray<string | number>>;\n\n /**\n * Query parameters a read requires, keyed by the type it serves: they are\n * sent with that type's list, its records by id, and every related\n * endpoint that serves it (`/posts/:id/comments` serves `comments`, and\n * takes the parameters of the comments' controller). Relationship\n * endpoints take none.\n *\n * ```javascript\n * query: { comments: { fromDate: '2026-01-01' } }\n * ```\n */\n query?: Record<string, Record<string, string>>;\n\n /**\n * Where the application is served, when not by `app` itself: a server\n * started as a separate process (`lumen serve`), such as\n * `'http://localhost:4000'`. `app` still provides the routes, the\n * include paths and the ids, so it must be booted in the test, but need\n * not listen.\n */\n origin?: string;\n\n /**\n * Called with each document a read answers with, for checks beyond which\n * records appear, such as the fields a request may see of each. Return a\n * message, or several, for each problem found: each is reported as a\n * violation of that request. May be async.\n *\n * ```javascript\n * onDocument({ document }) {\n * return document.included\n * ?.filter(({ type, attributes }) => type === 'users' && attributes.email)\n * .map(({ id }) => `users ${id} shows its email`);\n * }\n * ```\n */\n onDocument?: (\n response: AuditedDocument\n ) => DocumentCheckResult | Promise<DocumentCheckResult>;\n};\n\n/**\n * The read a request was made to, as the route defines it: so a check need\n * not parse the URL to know which record it is about.\n */\nexport type AuditedRoute = {\n /** The action, as hooks see it. */\n action: 'index' | 'show' | 'showRelationship' | 'showRelated';\n /**\n * The route's resource type: for a relationship or related endpoint, the\n * owner's (`posts` for `/posts/3/comments`).\n */\n type: string;\n /**\n * The id in the URL: the record of a `show`, the owner of a relationship\n * or related endpoint. None for a list.\n */\n id?: string;\n /** The relationship of a relationship or related endpoint (`comments`). */\n relationship?: string;\n};\n\n/** A document a read answered with, for `onDocument`. */\nexport type AuditedDocument = {\n /** The request, as a path and query string. */\n url: string;\n /** Its status. */\n status: number;\n /** The read the request was made to. */\n route: AuditedRoute;\n /** The parsed JSON:API document. */\n document: Record<string, unknown>;\n};\n\n/**\n * What `onDocument` returns: a message for each problem found, or nothing\n * when there is none.\n */\nexport type DocumentCheckResult =\n string | ReadonlyArray<string> | null | undefined | void;\n\n/**\n * A record a response contained that the audit's `visible` does not allow; a\n * relationship or related endpoint that answered for an owner `visible` does\n * not allow; a request that failed with an error other than `401`, `403` or\n * `404`, so that what it serves could not be checked; or a problem\n * `onDocument` found.\n */\nexport type VisibilityViolation = {\n /** The request, as a path and query string. */\n url: string;\n /** Its status. */\n status: number;\n /**\n * The record's type, if the violation is a record: one the response named,\n * or the hidden owner an endpoint answered for.\n */\n type?: string;\n /** The record's id, if the violation is a record. */\n id?: string;\n /**\n * What is wrong, for a hidden owner an endpoint answered for and for what\n * `onDocument` found.\n */\n message?: string;\n};\n\n/** What {@link auditVisibility} found. */\nexport type VisibilityAudit = {\n /** Every request made, as a path and query string. */\n requests: Array<string>;\n /** What should not have been in a response, in request order. */\n violations: Array<VisibilityViolation>;\n /**\n * Every record the responses named, keyed by type: as primary data,\n * included or as linkage, allowed or not, each id once. Compare it with\n * what the request should see to check that the audit reached it all.\n */\n seen: Record<string, Array<string>>;\n};\n\n// The built-in reads; custom routes take parameters the audit cannot know.\nconst READS = new Set(['index', 'show', 'showRelationship', 'showRelated']);\n\n// Statuses that reveal nothing: the request may not read, or there is nothing\n// it may see. Any other error means the read went unchecked.\nconst DENIED = new Set([401, 403, 404]);\n\n// Bounds the `links.next` chain of a single list, should one never end.\nconst MAX_PAGES = 1000;\n\n/** Where `app` itself is listening, if it is. */\nfunction originOf(app: Application): string {\n const address = app.server.instance.address() as AddressInfo | null;\n\n if (!address) {\n throw new Error(\n 'auditVisibility: the application is not listening. Start it ' +\n '(`startApp()`), or pass the `origin` of the server to audit.'\n );\n }\n\n return `http://localhost:${address.port}`;\n}\n\nfunction allows(visible: VisibleRecords | undefined, id: string): boolean {\n if (visible === true) {\n return true;\n }\n\n if (typeof visible === 'function') {\n return visible(id);\n }\n\n return Boolean(visible?.some(allowed => String(allowed) === id));\n}\n\n/**\n * The `include` values to request a route with: none, and every path it\n * accepts at once. A compound document holds what each path reaches, so one\n * request covers them all; a path that leads on to a longer one\n * (`comments` to `comments.user`) adds nothing and is left out. A\n * relationship endpoint (linkage only) accepts none.\n */\nfunction includesOf(route: Route): Array<string | undefined> {\n const include = route.params.get('include') as Set<string> | undefined;\n const paths = include ? Array.from(include) : [];\n const longest = paths.filter(\n path => !paths.some(other => other.startsWith(`${path}.`))\n );\n\n return longest.length ? [undefined, longest.join(',')] : [undefined];\n}\n\n/**\n * Request every read a namespace serves \u2014 each list, each record by id, each\n * relationship and related endpoint, without `include` and with every path\n * it accepts, following every page \u2014 and report each record in a response that\n * `visible` does not allow: in `data`, in `included`, or in any\n * relationship's linkage. A relationship or related endpoint that answers\n * for an owner `visible` does not allow is reported too, whatever it\n * answers with: that it answers reveals the owner. A `401`, `403` or `404`\n * (for a hidden record) reveals nothing and passes; any other error is\n * reported, since the read it answers went unchecked.\n *\n * Visibility rules apply to every one of these paths, but scoping in an\n * `index` or `show` override, or in a hook keyed on the action, does not.\n * This checks the result rather than the mechanism, against a list written\n * by hand:\n *\n * ```javascript\n * import { auditVisibility } from 'lumen-framework/testing';\n *\n * it('shows a member only public posts and their comments', async () => {\n * const { violations } = await auditVisibility(app, {\n * namespace: 'members',\n * headers: { Authorization: `Bearer ${memberToken}` },\n * visible: {\n * posts: [publicPost.id],\n * comments: [commentOnPublicPost.id],\n * users: true\n * }\n * });\n *\n * expect(violations).toEqual([]);\n * });\n * ```\n *\n * Requests are made one at a time, to `app` itself (which must then be\n * listening, as {@link startApp} leaves it) or to `origin`. Custom routes are\n * not requested. Each route is requested once per\n * id (and per page), so run it against a small fixture database, or narrow\n * the ids with `ids`.\n *\n * @param app - The application, listening.\n * @param options - What to audit, and what the request may see.\n * @returns Every request made, every violation found, and every record\n * seen.\n * @throws When the namespace serves no reads, so a mistyped namespace cannot\n * pass by checking nothing.\n */\nexport default async function auditVisibility(\n app: Application,\n {\n namespace = '',\n visible,\n headers = {},\n ids = {},\n query: required = {},\n origin = originOf(app),\n onDocument\n }: AuditVisibilityOptions\n): Promise<VisibilityAudit> {\n const audit: VisibilityAudit = { requests: [], violations: [], seen: {} };\n const seen = new Map<string, Set<string>>();\n\n const routes = Array.from(app.router.values()).filter(\n route =>\n route.method === 'GET' &&\n READS.has(route.action) &&\n route.controller.namespace === namespace\n );\n\n if (!routes.length) {\n throw new Error(\n `auditVisibility: namespace '/${namespace}' serves no index, show, ` +\n 'relationship or related routes.'\n );\n }\n\n // A link as a path and query string, which the audit reports.\n const relative = (link: string) => {\n const { pathname, search } = new URL(link, origin);\n\n return pathname + search;\n };\n\n const idsCache = new Map<string, Array<string>>();\n\n const idsOf = async (route: Route): Promise<Array<string>> => {\n const { model } = route.controller;\n const type = model.resourceName;\n let list = idsCache.get(type);\n\n if (!list) {\n const given = ids[type];\n\n list = given\n ? given.map(String)\n : (\n (await model.select(model.primaryKey)) as unknown as Array<Model>\n ).map(record => String(record.getPrimaryKey()));\n\n idsCache.set(type, list);\n }\n\n return list;\n };\n\n const check = (url: string, status: number, found: Array<Identifier>) => {\n found.forEach(({ type, id }) => {\n if (!seen.has(type)) {\n seen.set(type, new Set());\n }\n\n seen.get(type)?.add(id);\n\n if (!allows(visible[type], id)) {\n audit.violations.push({ url, status, type, id });\n }\n });\n };\n\n // One request, and the pages after it.\n const read = async (first: string, route: AuditedRoute) => {\n let url: string | undefined = first;\n\n // A relationship or related endpoint whose owner the request may not\n // see. Answering at all, whatever it answers with, tells the client the\n // owner exists, and what it is linked to: the owner is in the URL, not\n // in the response, so `check()` cannot catch it.\n const hiddenOwner =\n route.action.startsWith('showRel') &&\n route.id !== undefined &&\n !allows(visible[route.type], route.id);\n\n for (let page = 0; url && page < MAX_PAGES; page += 1) {\n audit.requests.push(url);\n\n const res: globalThis.Response = await fetch(origin + url, {\n headers: { Accept: 'application/vnd.api+json', ...headers }\n });\n\n if (!res.ok && !DENIED.has(res.status)) {\n audit.violations.push({ url, status: res.status });\n await res.body?.cancel();\n return;\n }\n\n if (res.ok && hiddenOwner && page === 0) {\n audit.violations.push({\n url,\n status: res.status,\n type: route.type,\n id: route.id,\n message:\n `answered for ${route.type} ${route.id}, which the request ` +\n 'may not see'\n });\n }\n\n // An override may answer with something other than a document (a\n // string is `text/plain`); only a JSON body can name records.\n if (!res.ok || !res.headers.get('content-type')?.includes('json')) {\n await res.body?.cancel();\n return;\n }\n\n const document = (await res.json()) as {\n links?: { next?: unknown };\n } | null;\n\n check(url, res.status, identifiersIn(document));\n\n if (onDocument && document) {\n const found = await onDocument({\n url,\n status: res.status,\n route,\n document: document as Record<string, unknown>\n });\n const messages =\n typeof found === 'string' ? [found] : Array.from(found ?? []);\n\n const at = { url, status: res.status };\n\n messages.forEach(message => {\n audit.violations.push({ ...at, message });\n });\n }\n\n const next: unknown = document?.links?.next;\n\n url = typeof next === 'string' ? relative(next) : undefined;\n }\n };\n\n for (const route of routes) {\n const paged = route.params.get('page')\n ? (route.related ?? route.controller).maxPerPage\n : undefined;\n\n // The type the route's records are of: none for a relationship\n // endpoint, which answers with linkage and takes no parameters.\n const served =\n route.type === 'relationship'\n ? undefined\n : (route.related ?? route.controller).model.resourceName;\n\n const query = (include: string | undefined) => {\n const params = new URLSearchParams(served ? required[served] : {});\n\n if (include) {\n params.set('include', include);\n }\n\n if (paged) {\n params.set('page[size]', String(paged));\n }\n\n const search = params.toString();\n\n return search ? `?${search}` : '';\n };\n\n const about = {\n action: route.action as AuditedRoute['action'],\n type: route.controller.model.resourceName,\n ...(route.relationship ? { relationship: route.relationship } : {})\n };\n\n const targets: Array<[string, AuditedRoute]> = route.dynamicSegments.length\n ? (await idsOf(route)).map(id => [\n route.path.replace(':id', encodeURIComponent(id)),\n { ...about, id }\n ])\n : [[route.path, about]];\n\n for (const [path, target] of targets) {\n for (const include of includesOf(route)) {\n await read(path + query(include), target);\n }\n }\n }\n\n seen.forEach((list, type) => {\n audit.seen[type] = Array.from(list);\n });\n\n return audit;\n}\n", "import { once } from 'events';\nimport { createRequire } from 'module';\nimport { resolve as resolvePath, join as joinPath } from 'path';\nimport type { AddressInfo } from 'net';\n\nimport type Application from '../application';\n\n/** The options of {@link startApp}. */\nexport type StartAppOptions = {\n /**\n * The environment to boot in: which `config/environments/*.js` and which\n * entry of `config/database.js` apply. Sets `NODE_ENV` when given, and\n * defaults it to `'test'` when unset, until `close()` restores it.\n */\n env?: string;\n\n /** The port to listen on. `0`, the default, takes a free one. */\n port?: number;\n};\n\n/** An application {@link startApp} booted. */\nexport type StartedApp = {\n /** The application, for {@link auditVisibility} and the models. */\n app: Application;\n /** Where it listens, such as `'http://localhost:53017'`. */\n origin: string;\n /**\n * Stops the server, closes the database connections, and restores\n * `NODE_ENV` to what it was before `startApp()`.\n */\n close: () => Promise<void>;\n};\n\ntype Bundle = {\n Application: new (options: Record<string, unknown>) => Promise<Application>;\n config: Record<string, unknown>;\n database: Record<string, unknown>;\n};\n\n// The bundles booting or booted in this process. Booting sets up an app's\n// model classes, which happens once per process: a second boot of the same\n// bundle fails. A failed boot is removed again, so a retry reports its own\n// error rather than \"already started\".\nconst started = new Set<string>();\n\n// The first error each bundle failed to boot with: a failed boot can leave the\n// app half set up, so a retry may fail on that rather than on the cause.\nconst failed = new Map<string, unknown>();\n\nconst messageOf = (error: unknown) =>\n error instanceof Error ? error.message : String(error);\n\n/**\n * Boot the app at `path` in the test's own process, from its compiled\n * bundle (`dist/bundle.js`), and listen on a free port.\n *\n * ```javascript\n * import { startApp } from 'lumen-framework/testing';\n *\n * let app, origin, close;\n *\n * beforeAll(async () => {\n * ({ app, origin, close } = await startApp(process.cwd()));\n * });\n *\n * afterAll(() => close());\n * ```\n *\n * The bundle must be compiled for the environment first, as `lumen build`\n * and the `lumen db:*` commands do. Its model classes are set up once per\n * process, so start an app once and share it between test files; starting\n * the same one again throws. `NODE_ENV` holds the app's environment until\n * `close()`.\n *\n * @param path - The app's root directory.\n * @param options - The environment and port.\n * @returns The application, where it listens, and how to stop it.\n * @throws When the app at `path` was already started in this process.\n */\nexport default async function startApp(\n path: string,\n { env, port = 0 }: StartAppOptions = {}\n): Promise<StartedApp> {\n const root = resolvePath(path);\n const bundlePath = joinPath(root, 'dist', 'bundle.js');\n\n if (started.has(bundlePath)) {\n throw new Error(\n `startApp: ${root} was already started in this process. Its models ` +\n 'are set up once per process: start it once and share it.'\n );\n }\n\n // Read when the bundle loads, which is why it is set first. It stays set\n // while the app runs: the app reads it again per request (whether a 500\n // shows its message).\n const previous = process.env.NODE_ENV;\n const restoreEnv = () => {\n if (previous === undefined) {\n delete process.env.NODE_ENV;\n } else {\n process.env.NODE_ENV = previous;\n }\n };\n\n process.env.NODE_ENV = env ?? previous ?? 'test';\n started.add(bundlePath);\n\n let app: Application;\n\n try {\n // The app's bundle, resolved at runtime: not a module of the framework.\n const {\n Application: App,\n config,\n database\n } = createRequire(bundlePath)(bundlePath) as Bundle;\n\n app = await new App({ ...config, database, path: root, port });\n\n if (!app.server.instance.listening) {\n await once(app.server.instance, 'listening');\n }\n } catch (error) {\n started.delete(bundlePath);\n restoreEnv();\n\n const earlier = failed.get(bundlePath);\n\n if (failed.has(bundlePath)) {\n throw new Error(\n `startApp: booting ${root} failed: ${messageOf(error)}. An earlier ` +\n `attempt in this process failed first, with: ${messageOf(earlier)}. ` +\n 'A failed boot can leave the app half set up: fix that one first.',\n { cause: error }\n );\n }\n\n failed.set(bundlePath, error);\n throw error;\n }\n\n const { port: listening } = app.server.instance.address() as AddressInfo;\n\n return {\n app,\n origin: `http://localhost:${listening}`,\n close: async () => {\n try {\n await app.close();\n } finally {\n restoreEnv();\n }\n }\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["resolvePath", "joinPath"]
7
+ }
@@ -0,0 +1,252 @@
1
+ // src/packages/testing/utils/identifiers-in.ts
2
+ var isIdentifier = (value) => typeof value === "object" && value !== null && "type" in value && "id" in value;
3
+ function identifiersIn(document) {
4
+ const found = [];
5
+ const add = (value) => {
6
+ if (isIdentifier(value)) {
7
+ found.push({ type: value.type, id: String(value.id) });
8
+ }
9
+ };
10
+ const addLinkage = (linkage) => {
11
+ (Array.isArray(linkage) ? linkage : [linkage]).forEach(add);
12
+ };
13
+ const addResource = (resource) => {
14
+ add(resource);
15
+ if (isIdentifier(resource)) {
16
+ Object.values(resource.relationships ?? {}).forEach(
17
+ (relationship) => addLinkage(relationship?.data)
18
+ );
19
+ }
20
+ };
21
+ if (typeof document !== "object" || document === null) {
22
+ return found;
23
+ }
24
+ const { data, included } = document;
25
+ (Array.isArray(data) ? data : [data]).forEach(addResource);
26
+ (included ?? []).forEach(addResource);
27
+ return found;
28
+ }
29
+
30
+ // src/packages/testing/audit-visibility.ts
31
+ var READS = /* @__PURE__ */ new Set(["index", "show", "showRelationship", "showRelated"]);
32
+ var DENIED = /* @__PURE__ */ new Set([401, 403, 404]);
33
+ var MAX_PAGES = 1e3;
34
+ function originOf(app) {
35
+ const address = app.server.instance.address();
36
+ if (!address) {
37
+ throw new Error(
38
+ "auditVisibility: the application is not listening. Start it (`startApp()`), or pass the `origin` of the server to audit."
39
+ );
40
+ }
41
+ return `http://localhost:${address.port}`;
42
+ }
43
+ function allows(visible, id) {
44
+ if (visible === true) {
45
+ return true;
46
+ }
47
+ if (typeof visible === "function") {
48
+ return visible(id);
49
+ }
50
+ return Boolean(visible?.some((allowed) => String(allowed) === id));
51
+ }
52
+ function includesOf(route) {
53
+ const include = route.params.get("include");
54
+ const paths = include ? Array.from(include) : [];
55
+ const longest = paths.filter(
56
+ (path) => !paths.some((other) => other.startsWith(`${path}.`))
57
+ );
58
+ return longest.length ? [void 0, longest.join(",")] : [void 0];
59
+ }
60
+ async function auditVisibility(app, {
61
+ namespace = "",
62
+ visible,
63
+ headers = {},
64
+ ids = {},
65
+ query: required = {},
66
+ origin = originOf(app),
67
+ onDocument
68
+ }) {
69
+ const audit = { requests: [], violations: [], seen: {} };
70
+ const seen = /* @__PURE__ */ new Map();
71
+ const routes = Array.from(app.router.values()).filter(
72
+ (route) => route.method === "GET" && READS.has(route.action) && route.controller.namespace === namespace
73
+ );
74
+ if (!routes.length) {
75
+ throw new Error(
76
+ `auditVisibility: namespace '/${namespace}' serves no index, show, relationship or related routes.`
77
+ );
78
+ }
79
+ const relative = (link) => {
80
+ const { pathname, search } = new URL(link, origin);
81
+ return pathname + search;
82
+ };
83
+ const idsCache = /* @__PURE__ */ new Map();
84
+ const idsOf = async (route) => {
85
+ const { model } = route.controller;
86
+ const type = model.resourceName;
87
+ let list = idsCache.get(type);
88
+ if (!list) {
89
+ const given = ids[type];
90
+ list = given ? given.map(String) : (await model.select(model.primaryKey)).map((record) => String(record.getPrimaryKey()));
91
+ idsCache.set(type, list);
92
+ }
93
+ return list;
94
+ };
95
+ const check = (url, status, found) => {
96
+ found.forEach(({ type, id }) => {
97
+ if (!seen.has(type)) {
98
+ seen.set(type, /* @__PURE__ */ new Set());
99
+ }
100
+ seen.get(type)?.add(id);
101
+ if (!allows(visible[type], id)) {
102
+ audit.violations.push({ url, status, type, id });
103
+ }
104
+ });
105
+ };
106
+ const read = async (first, route) => {
107
+ let url = first;
108
+ const hiddenOwner = route.action.startsWith("showRel") && route.id !== void 0 && !allows(visible[route.type], route.id);
109
+ for (let page = 0; url && page < MAX_PAGES; page += 1) {
110
+ audit.requests.push(url);
111
+ const res = await fetch(origin + url, {
112
+ headers: { Accept: "application/vnd.api+json", ...headers }
113
+ });
114
+ if (!res.ok && !DENIED.has(res.status)) {
115
+ audit.violations.push({ url, status: res.status });
116
+ await res.body?.cancel();
117
+ return;
118
+ }
119
+ if (res.ok && hiddenOwner && page === 0) {
120
+ audit.violations.push({
121
+ url,
122
+ status: res.status,
123
+ type: route.type,
124
+ id: route.id,
125
+ message: `answered for ${route.type} ${route.id}, which the request may not see`
126
+ });
127
+ }
128
+ if (!res.ok || !res.headers.get("content-type")?.includes("json")) {
129
+ await res.body?.cancel();
130
+ return;
131
+ }
132
+ const document = await res.json();
133
+ check(url, res.status, identifiersIn(document));
134
+ if (onDocument && document) {
135
+ const found = await onDocument({
136
+ url,
137
+ status: res.status,
138
+ route,
139
+ document
140
+ });
141
+ const messages = typeof found === "string" ? [found] : Array.from(found ?? []);
142
+ const at = { url, status: res.status };
143
+ messages.forEach((message) => {
144
+ audit.violations.push({ ...at, message });
145
+ });
146
+ }
147
+ const next = document?.links?.next;
148
+ url = typeof next === "string" ? relative(next) : void 0;
149
+ }
150
+ };
151
+ for (const route of routes) {
152
+ const paged = route.params.get("page") ? (route.related ?? route.controller).maxPerPage : void 0;
153
+ const served = route.type === "relationship" ? void 0 : (route.related ?? route.controller).model.resourceName;
154
+ const query = (include) => {
155
+ const params = new URLSearchParams(served ? required[served] : {});
156
+ if (include) {
157
+ params.set("include", include);
158
+ }
159
+ if (paged) {
160
+ params.set("page[size]", String(paged));
161
+ }
162
+ const search = params.toString();
163
+ return search ? `?${search}` : "";
164
+ };
165
+ const about = {
166
+ action: route.action,
167
+ type: route.controller.model.resourceName,
168
+ ...route.relationship ? { relationship: route.relationship } : {}
169
+ };
170
+ const targets = route.dynamicSegments.length ? (await idsOf(route)).map((id) => [
171
+ route.path.replace(":id", encodeURIComponent(id)),
172
+ { ...about, id }
173
+ ]) : [[route.path, about]];
174
+ for (const [path, target] of targets) {
175
+ for (const include of includesOf(route)) {
176
+ await read(path + query(include), target);
177
+ }
178
+ }
179
+ }
180
+ seen.forEach((list, type) => {
181
+ audit.seen[type] = Array.from(list);
182
+ });
183
+ return audit;
184
+ }
185
+
186
+ // src/packages/testing/start-app.ts
187
+ import { once } from "events";
188
+ import { createRequire } from "module";
189
+ import { resolve as resolvePath, join as joinPath } from "path";
190
+ var started = /* @__PURE__ */ new Set();
191
+ var failed = /* @__PURE__ */ new Map();
192
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
193
+ async function startApp(path, { env, port = 0 } = {}) {
194
+ const root = resolvePath(path);
195
+ const bundlePath = joinPath(root, "dist", "bundle.js");
196
+ if (started.has(bundlePath)) {
197
+ throw new Error(
198
+ `startApp: ${root} was already started in this process. Its models are set up once per process: start it once and share it.`
199
+ );
200
+ }
201
+ const previous = process.env.NODE_ENV;
202
+ const restoreEnv = () => {
203
+ if (previous === void 0) {
204
+ delete process.env.NODE_ENV;
205
+ } else {
206
+ process.env.NODE_ENV = previous;
207
+ }
208
+ };
209
+ process.env.NODE_ENV = env ?? previous ?? "test";
210
+ started.add(bundlePath);
211
+ let app;
212
+ try {
213
+ const {
214
+ Application: App,
215
+ config,
216
+ database
217
+ } = createRequire(bundlePath)(bundlePath);
218
+ app = await new App({ ...config, database, path: root, port });
219
+ if (!app.server.instance.listening) {
220
+ await once(app.server.instance, "listening");
221
+ }
222
+ } catch (error) {
223
+ started.delete(bundlePath);
224
+ restoreEnv();
225
+ const earlier = failed.get(bundlePath);
226
+ if (failed.has(bundlePath)) {
227
+ throw new Error(
228
+ `startApp: booting ${root} failed: ${messageOf(error)}. An earlier attempt in this process failed first, with: ${messageOf(earlier)}. A failed boot can leave the app half set up: fix that one first.`,
229
+ { cause: error }
230
+ );
231
+ }
232
+ failed.set(bundlePath, error);
233
+ throw error;
234
+ }
235
+ const { port: listening } = app.server.instance.address();
236
+ return {
237
+ app,
238
+ origin: `http://localhost:${listening}`,
239
+ close: async () => {
240
+ try {
241
+ await app.close();
242
+ } finally {
243
+ restoreEnv();
244
+ }
245
+ }
246
+ };
247
+ }
248
+ export {
249
+ auditVisibility,
250
+ startApp
251
+ };
252
+ //# sourceMappingURL=testing.mjs.map