json-rest-api 1.0.29 → 2.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 (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,25 +1,16 @@
1
- /**
2
- * Express Plugin for Hooked API
3
- *
4
- * This plugin creates HTTP endpoints for your REST API by listening to
5
- * route registrations from the REST API plugin and creating Express routes.
6
- *
7
- * Features:
8
- * - Automatic route creation via addRoute hook
9
- * - JSON:API compliant request/response handling
10
- * - Query parameter parsing
11
- * - Error mapping to HTTP status codes
12
- * - Content type validation
13
- * - Middleware injection points
14
- * - File upload support with busboy or formidable
15
- */
16
-
17
- import { requirePackage } from 'hooked-api'
1
+ import { throwMissingPackage } from '../../../lib/missing-package.js'
2
+ import onHeaders from 'on-headers'
3
+ import { getOperationDiagnosticContext } from '../../../lib/error-context.js'
18
4
  import { createContext } from './lib/request-helpers.js'
19
5
  import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
20
6
  import {
21
7
  isWriteMethod,
22
8
  isAllowedWriteContentType,
9
+ isMultipartContentType,
10
+ mergeResponseHeaders,
11
+ acceptsJsonApi,
12
+ getNotAcceptableErrorBody,
13
+ getNotFoundErrorBody,
23
14
  getUnsupportedMediaTypeErrorBody
24
15
  } from './lib/transport-http-helpers.js'
25
16
  import {
@@ -27,8 +18,11 @@ import {
27
18
  buildTransportRequestData,
28
19
  createConnectorContext,
29
20
  runTransportRequestLifecycle,
21
+ applyTransportResponseLifecycle,
22
+ addWriteOutcomeToHttpErrors,
30
23
  buildTransportRejectionBody,
31
24
  executeConnectorRoute,
25
+ logHttpRequestError,
32
26
  handleConnectorError
33
27
  } from './lib/connector-core.js'
34
28
 
@@ -36,25 +30,15 @@ export const ExpressPlugin = {
36
30
  name: 'express',
37
31
  dependencies: ['rest-api'],
38
32
 
39
- async install ({ on, vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
40
- addHook('release', 'releaseHook', {},
41
- async ({ api }) => {} // TODO: Anything to do here?
42
- )
43
-
33
+ async install ({ vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
44
34
  // Dynamic import for Express
45
35
  let express
46
36
  try {
47
37
  express = (await import('express')).default
48
38
  } catch (e) {
49
- requirePackage('express', 'express',
39
+ throwMissingPackage('express', 'express',
50
40
  'Express.js is required for HTTP server functionality. This is a peer dependency.')
51
41
  }
52
- // Enhance the logger
53
- const enhancedLog = createEnhancedLogger(log, {
54
- logFullErrors: true,
55
- includeStack: true
56
- })
57
-
58
42
  // Initialize express namespace
59
43
  if (!api.http) {
60
44
  api.http = {}
@@ -62,10 +46,12 @@ export const ExpressPlugin = {
62
46
  api.http.express = {}
63
47
 
64
48
  const expressOptions = pluginOptions || {}
49
+ const httpValidators = expressOptions.httpValidators ?? false
50
+ if (typeof httpValidators !== 'boolean') throw new TypeError('httpValidators must be a boolean')
65
51
 
66
52
  // Get mountPath from options (this is now a transport concern)
67
53
  const mountPath = expressOptions.mountPath || ''
68
- const basePath = mountPath // Keep basePath for internal use
54
+ const basePath = mountPath.replace(/\/+$/, '')
69
55
  const publicBaseUrl = expressOptions.publicBaseUrl || ''
70
56
  const strictContentType = expressOptions.strictContentType !== false
71
57
  const requestSizeLimit = expressOptions.requestSizeLimit || '1mb'
@@ -86,19 +72,16 @@ export const ExpressPlugin = {
86
72
  let detector
87
73
 
88
74
  if (parserLib === 'busboy') {
89
- try {
90
- const { createBusboyDetector } = await import('../lib/busboy-detector.js')
91
- detector = createBusboyDetector(parserOptions)
92
- } catch (e) {
93
- log.warn('Busboy not installed. Install with: npm install busboy')
94
- }
75
+ const { createBusboyDetector } = await import('./lib/busboy-detector.js')
76
+ detector = createBusboyDetector(parserOptions)
95
77
  } else if (parserLib === 'formidable') {
96
- try {
97
- const { createFormidableDetector } = await import('../lib/formidable-detector.js')
98
- detector = createFormidableDetector(parserOptions)
99
- } catch (e) {
100
- log.warn('Formidable not installed. Install with: npm install formidable')
101
- }
78
+ const { createFormidableDetector } = await import('./lib/formidable-detector.js')
79
+ detector = createFormidableDetector(parserOptions)
80
+ } else if (typeof parserLib === 'function') detector = await parserLib(parserOptions)
81
+ else throw new Error('fileParser must be busboy, formidable, or a detector factory')
82
+
83
+ if (!detector?.name || typeof detector.detect !== 'function' || typeof detector.parse !== 'function') {
84
+ throw new Error('File detector must have name, detect() and parse()')
102
85
  }
103
86
 
104
87
  if (detector) {
@@ -122,94 +105,125 @@ export const ExpressPlugin = {
122
105
  const router = expressOptions.router || express.Router()
123
106
  const notFoundRouter = express.Router()
124
107
 
108
+ const isApiPath = path => !basePath || path === basePath || path.startsWith(`${basePath}/`)
109
+ const allowsMultipart = () => expressOptions.enableFileUploads !== false && !!api.rest?.fileDetectors?.length
110
+ const ensureContext = (req, res) => {
111
+ if (!req.context || !req.transportData) {
112
+ const requestData = buildTransportRequestData({
113
+ method: req.method,
114
+ url: req.url,
115
+ path: req.path,
116
+ headers: req.headers,
117
+ body: req.body,
118
+ params: req.params,
119
+ query: req.query
120
+ })
121
+ const { context, transportData } = createConnectorContext({
122
+ request: req,
123
+ reply: res,
124
+ source: 'express',
125
+ mountPath: basePath,
126
+ publicBaseUrl,
127
+ requestData,
128
+ createContext,
129
+ urlPrefixOverride: req.urlPrefixOverride
130
+ })
131
+ req.context = context
132
+ req.transportData = transportData
133
+ }
134
+ return { context: req.context, transportData: req.transportData }
135
+ }
136
+ const applyHeaders = (res, headers) => res.set(mergeResponseHeaders({ vary: res.getHeader('Vary') }, headers, { vary: 'Accept' }))
137
+ const sendResponse = async (req, res, status, body) => {
138
+ const { context, transportData } = ensureContext(req, res)
139
+ body = addWriteOutcomeToHttpErrors(body, context)
140
+ const headers = await applyTransportResponseLifecycle({ context, transportData, status, body, runHooks })
141
+ applyHeaders(res, headers)
142
+ return res.status(status).type('application/vnd.api+json').json(body)
143
+ }
144
+ router.use((req, res, next) => next(isApiPath(req.path) ? undefined : 'router'))
145
+ router.use((req, res, next) => {
146
+ onHeaders(res, function () {
147
+ if (httpValidators && req.method === 'PUT') this.removeHeader('ETag')
148
+ if (String(this.getHeader('Content-Type')).split(';')[0] === 'application/vnd.api+json') {
149
+ // Express adds a charset during JSON serialization; JSON:API forbids it.
150
+ this.setHeader('Content-Type', 'application/vnd.api+json')
151
+ }
152
+ })
153
+ next()
154
+ })
155
+
156
+ // Reject unsupported media types before a body parser can report a syntax error.
157
+ if (strictContentType) {
158
+ router.use((req, res, next) => {
159
+ if (isWriteMethod(req.method) && !isAllowedWriteContentType(req.get('Content-Type'), { allowMultipart: allowsMultipart() })) {
160
+ sendResponse(req, res, 415, getUnsupportedMediaTypeErrorBody({ allowMultipart: allowsMultipart() })).catch(next)
161
+ return
162
+ }
163
+ next()
164
+ })
165
+ }
166
+
125
167
  // Add body parsing middleware
126
168
  router.use(express.json({
127
169
  limit: requestSizeLimit,
170
+ strict: false,
128
171
  type: ['application/json', 'application/vnd.api+json']
129
172
  }))
130
173
 
131
174
  // Add transport hook middleware
132
- router.use(async (req, res, next) => {
133
- const requestData = buildTransportRequestData({
134
- method: req.method,
135
- url: req.url,
136
- path: req.path,
137
- headers: req.headers,
138
- body: req.body,
139
- params: req.params,
140
- query: req.query
141
- })
142
- const { context, transportData } = createConnectorContext({
143
- request: req,
144
- reply: res,
145
- source: 'express',
146
- mountPath: basePath,
147
- publicBaseUrl,
148
- requestData,
149
- createContext,
150
- urlPrefixOverride: req.urlPrefixOverride
151
- })
175
+ const handleTransportRequest = async (req, res, next) => {
176
+ const { context } = ensureContext(req, res)
152
177
  const { rejected, handled } = await runTransportRequestLifecycle({
153
178
  context,
154
179
  runHooks
155
180
  })
156
181
 
157
182
  if (rejected) {
158
- if (transportData.response.headers) {
159
- res.set(transportData.response.headers)
160
- }
161
- return res.status(context.rejection.status || 500).json(
162
- buildTransportRejectionBody(context)
163
- )
183
+ return sendResponse(req, res, context.rejection.status || 500, buildTransportRejectionBody(context))
164
184
  }
165
185
 
166
186
  if (handled) {
167
187
  return
168
188
  }
169
189
 
170
- // Store transport data and context for later use
171
- req.transportData = transportData
172
- req.context = context
173
- next()
174
- })
175
-
176
- // Content type validation middleware
177
- if (strictContentType) {
178
- router.use((req, res, next) => {
179
- if (isWriteMethod(req.method)) {
180
- const contentType = req.get('Content-Type')
190
+ res.vary('Accept')
191
+ if (!acceptsJsonApi(req.get('Accept'))) {
192
+ return sendResponse(req, res, 406, getNotAcceptableErrorBody())
193
+ }
181
194
 
182
- if (!isAllowedWriteContentType(contentType, { allowMultipart: true })) {
183
- return res.status(415).json(
184
- getUnsupportedMediaTypeErrorBody({ allowMultipart: true })
185
- )
186
- }
187
- }
188
- next()
189
- })
195
+ next()
190
196
  }
197
+ router.use((req, res, next) => {
198
+ handleTransportRequest(req, res, next).catch(next)
199
+ })
191
200
 
192
201
  /**
193
202
  * Error handler - maps REST API errors to HTTP responses
194
203
  */
195
- const handleError = async (error, req, res) => {
196
- enhancedLog.logError('HTTP request error', error, {
204
+ const handleError = async (error, req, res, routeMeta) => {
205
+ const { context, transportData } = ensureContext(req, res)
206
+ await logHttpRequestError({
207
+ error,
208
+ context,
209
+ log,
210
+ scopes,
211
+ routeMeta,
212
+ api,
197
213
  method: req.method,
198
- path: req.path,
199
- url: req.url
214
+ path: req.route?.path,
215
+ message: 'HTTP request error'
200
216
  })
201
217
 
202
218
  const { status, body: errorResponse, headers } = await handleConnectorError({
203
219
  error,
204
- context: req.context,
205
- transportData: req.transportData,
220
+ context,
221
+ transportData,
206
222
  runHooks
207
223
  })
208
224
 
209
- if (headers) {
210
- res.set(headers)
211
- }
212
- res.status(status).json(errorResponse)
225
+ applyHeaders(res, headers)
226
+ res.status(status).type('application/vnd.api+json').json(errorResponse)
213
227
  }
214
228
 
215
229
  /**
@@ -222,45 +236,21 @@ export const ExpressPlugin = {
222
236
  const beforeMiddleware = expressOptions.middleware?.beforeAll || []
223
237
 
224
238
  try {
225
- // Extract the handler logic into a shared function to keep it DRY (Don't Repeat Yourself).
226
239
  const expressHandler = async (req, res) => {
227
240
  try {
228
- let context = req.context
229
- let transportData = req.transportData
230
-
231
- if (!context || !transportData) {
232
- const requestData = buildTransportRequestData({
233
- method: req.method,
234
- url: req.url,
235
- path: req.path,
236
- headers: req.headers,
237
- body: req.body,
238
- params: req.params,
239
- query: req.query
240
- })
241
- const setup = createConnectorContext({
242
- request: req,
243
- reply: res,
244
- source: 'express',
245
- mountPath: basePath,
246
- publicBaseUrl,
247
- requestData,
248
- createContext,
249
- urlPrefixOverride: req.urlPrefixOverride
250
- })
251
- context = setup.context
252
- transportData = setup.transportData
253
- req.context = context
254
- req.transportData = transportData
255
- }
241
+ const { context, transportData } = ensureContext(req, res)
256
242
 
257
243
  const outcome = await executeConnectorRoute({
244
+ api,
245
+ httpValidators,
258
246
  method: req.method,
259
247
  handler,
260
248
  queryString: extractQueryString(req.url),
261
249
  headers: req.headers,
262
250
  params: req.params,
263
- body: req.body,
251
+ body: isMultipartContentType(req.get('Content-Type')) && routeMeta?.kind === 'resource' && ['post', 'put', 'patch'].includes(routeMeta.operation)
252
+ ? { data: { type: routeMeta.scopeName, attributes: {} } }
253
+ : req.body,
264
254
  context,
265
255
  transportData,
266
256
  routeMeta,
@@ -269,86 +259,52 @@ export const ExpressPlugin = {
269
259
  publicBaseUrl,
270
260
  runHooks
271
261
  })
272
- res.set(outcome.headers)
262
+ applyHeaders(res, outcome.headers)
273
263
 
274
- // Set content type
275
264
  res.set('Content-Type', 'application/vnd.api+json')
276
265
 
277
266
  if (outcome.location) {
278
267
  res.set('Location', outcome.location)
279
268
  }
280
269
 
281
- // Handle response based on status
282
270
  if (outcome.status === 204) {
283
271
  res.sendStatus(204)
272
+ } else if (outcome.serialized) {
273
+ res.status(outcome.status).send(outcome.body)
284
274
  } else {
285
275
  res.status(outcome.status).json(outcome.body)
286
276
  }
287
277
  } catch (error) {
288
- handleError(error, req, res)
278
+ await handleError(error, req, res, routeMeta)
289
279
  }
290
280
  }
291
281
 
292
- // CRITICAL: Express routing method selection - wildcard vs specific routes
293
- //
294
- // Express provides two different ways to register routes:
295
- // 1. router.METHOD(path, handler) - e.g., router.get('/users', handler)
296
- // - Only responds to the SPECIFIC HTTP method
297
- // - Perfect for normal REST endpoints
298
- //
299
- // 2. router.use(path, handler)
300
- // - Responds to ALL HTTP methods
301
- // - Needed for wildcard paths that must handle any method
302
- //
303
- // The CORS plugin needs wildcard routes because it must handle OPTIONS
304
- // requests for ANY path under the API prefix, even paths that don't exist
305
- // as defined routes. For example:
306
- // - Defined route: GET /api/users
307
- // - Browser might send: OPTIONS /api/users/invalid/path
308
- // - CORS must still respond with proper headers
309
- //
310
282
  if (path === vars.transport.matchAll) {
311
- // This is a wildcard route (path = '*')
312
- // We MUST use router.use() because:
313
- // - We need to catch ALL paths (using '*' or '/api/*')
314
- // - We need to handle a SPECIFIC method (e.g., OPTIONS)
315
- // - router.options('*') would NOT work for paths like '/api/some/nested/path'
316
-
317
- // Since router.use() responds to ALL methods, we need a wrapper
318
- // that only handles our specific method (e.g., OPTIONS)
283
+ // CORS must also match unknown paths; router.use needs an explicit method check.
319
284
  const methodSpecificMiddleware = (req, res, next) => {
320
285
  if (req.method.toLowerCase() === method.toLowerCase()) {
321
286
  expressHandler(req, res)
322
287
  } else {
323
- // Not our method, pass to next middleware
324
288
  next()
325
289
  }
326
290
  }
327
- // IMPORTANT: We do NOT need to pass a path to router.use() here!
328
- // When no path is provided, router.use() matches ALL requests
329
- // This is exactly what we want for wildcard routes
330
291
  router.use(...beforeMiddleware, methodSpecificMiddleware)
331
292
  } else {
332
- // This is a normal route with a specific path (e.g., '/api/users')
333
- // We use router.METHOD() because:
334
- // - We want to respond to ONLY this specific HTTP method
335
- // - The path is exact, not a wildcard
336
- // - This is more efficient than router.use() with method checking
337
- //
338
- // Note: Routes from RestApiPlugin already include the full path with mountPath
339
- // So we use them as-is without adding basePath to avoid double-prefixing
340
-
293
+ // Resource routes already contain the mount path.
341
294
  router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler)
342
-
343
- // Debug logging for route registration
344
295
  }
345
296
  } catch (routeError) {
346
- log.error('[EXPRESS DEBUG] Error creating route:', {
347
- error: routeError.message,
348
- stack: routeError.stack,
349
- path,
350
- method: method.toLowerCase()
351
- })
297
+ try {
298
+ await createEnhancedLogger(log).logError('Express route registration failed', routeError, {
299
+ ...getOperationDiagnosticContext(context, {
300
+ phase: 'routeRegistration',
301
+ method,
302
+ scopeName: routeMeta?.scopeName,
303
+ backend: api.knex?.instance?.client?.config?.client
304
+ }),
305
+ path
306
+ })
307
+ } catch { /* Diagnostic failure must not replace the registration failure. */ }
352
308
  throw routeError
353
309
  }
354
310
 
@@ -359,55 +315,28 @@ export const ExpressPlugin = {
359
315
  })
360
316
 
361
317
  // Apply global middleware if configured
362
- let finalRouter = router
318
+ const finalRouter = express.Router()
363
319
  if (expressOptions.middleware?.beforeAll) {
364
- const globalRouter = express.Router()
365
- globalRouter.use(...expressOptions.middleware.beforeAll)
366
- globalRouter.use(router)
367
- finalRouter = globalRouter
320
+ finalRouter.use(...expressOptions.middleware.beforeAll)
368
321
  }
322
+ finalRouter.use(router)
323
+ // Catch parser, request-hook and route-matching errors on Express 4 and 5.
324
+ finalRouter.use((error, req, res, next) => {
325
+ if (res.headersSent) return next(error)
326
+ handleError(error, req, res).catch(next)
327
+ })
369
328
 
370
329
  // Set up 404 handler in separate router (unless disabled)
371
330
  if (expressOptions.handle404 !== false) {
372
- notFoundRouter.use(async (req, res, next) => {
373
- if (basePath && req.path.startsWith(basePath)) {
374
- // Create minimal context for 404
375
- const context = createContext(req, res, 'express')
376
- const transportData = {
377
- request: {
378
- method: req.method,
379
- url: req.url,
380
- path: req.path,
381
- headers: req.headers
382
- },
383
- response: {
384
- headers: {},
385
- status: 404,
386
- body: {
387
- errors: [{
388
- status: '404',
389
- title: 'Not Found',
390
- detail: `The requested endpoint ${req.method} ${req.path} does not exist`
391
- }]
392
- }
393
- }
394
- }
395
-
396
- // Add transport data to context
397
- context.transport = transportData
398
-
399
- // Run transport:response hook for 404
400
- await runHooks('transport:response', context)
401
-
402
- // Apply response headers from hooks
403
- if (transportData.response.headers) {
404
- res.set(transportData.response.headers)
405
- }
406
-
407
- res.status(404).json(transportData.response.body)
331
+ const handleNotFound = async (req, res, next) => {
332
+ if (isApiPath(req.path)) {
333
+ await sendResponse(req, res, 404, getNotFoundErrorBody(req.method, req.path))
408
334
  } else {
409
335
  next()
410
336
  }
337
+ }
338
+ notFoundRouter.use((req, res, next) => {
339
+ handleNotFound(req, res, next).catch(error => handleError(error, req, res).catch(next))
411
340
  })
412
341
  }
413
342