json-rest-api 1.0.29 → 2.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 (292) 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 +634 -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 +100 -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 +3827 -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 +36 -0
  44. package/index.js +1 -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 +64 -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 +213 -164
  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 +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. 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