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,9 +1,13 @@
1
1
  import { createContext } from './lib/request-helpers.js'
2
- import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
3
- import { buildTransportRouteSchema } from './lib/transport-route-schemas.js'
2
+ import { buildTransportRouteSchema, getTransportRouteContract } from './lib/transport-route-schemas.js'
3
+ import { validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
4
4
  import {
5
5
  isWriteMethod,
6
6
  isAllowedWriteContentType,
7
+ mergeResponseHeaders,
8
+ acceptsJsonApi,
9
+ getNotAcceptableErrorBody,
10
+ getNotFoundErrorBody,
7
11
  getUnsupportedMediaTypeErrorBody
8
12
  } from './lib/transport-http-helpers.js'
9
13
  import {
@@ -11,59 +15,55 @@ import {
11
15
  buildTransportRequestData,
12
16
  createConnectorContext,
13
17
  runTransportRequestLifecycle,
18
+ applyTransportResponseLifecycle,
19
+ addWriteOutcomeToHttpErrors,
14
20
  buildTransportRejectionBody,
15
21
  executeConnectorRoute,
22
+ logHttpRequestError,
16
23
  handleConnectorError
17
24
  } from './lib/connector-core.js'
18
25
 
19
26
  function applyHeaders (reply, headers = {}) {
20
- for (const [headerName, headerValue] of Object.entries(headers)) {
27
+ const merged = mergeResponseHeaders({ vary: reply.getHeader('vary') }, headers, { vary: 'Accept' })
28
+ for (const [headerName, headerValue] of Object.entries(merged)) {
21
29
  reply.header(headerName, headerValue)
22
30
  }
23
31
  }
24
32
 
25
- function registerVendorJsonParser (app) {
26
- if (typeof app?.addContentTypeParser !== 'function') return
27
- if (typeof app?.hasContentTypeParser === 'function' && app.hasContentTypeParser('application/vnd.api+json')) {
28
- return
29
- }
30
-
31
- app.addContentTypeParser(
32
- 'application/vnd.api+json',
33
- { parseAs: 'string' },
34
- (request, body, done) => {
35
- if (body === '' || body === undefined || body === null) {
36
- done(null, {})
37
- return
38
- }
33
+ async function setJsonApiResponseType (request, reply, payload) {
34
+ // Fastify adds a charset during serialization; normalize after that step.
35
+ reply.header('Content-Type', 'application/vnd.api+json')
36
+ return payload
37
+ }
39
38
 
40
- try {
41
- done(null, JSON.parse(body))
42
- } catch (error) {
43
- error.statusCode = 400
44
- done(error)
45
- }
39
+ function registerJsonParsers (app) {
40
+ const parse = (request, body, done) => {
41
+ try { done(null, body === '' ? {} : JSON.parse(body)) } catch (error) {
42
+ error.statusCode = 400
43
+ error.type = 'entity.parse.failed'
44
+ done(error)
46
45
  }
47
- )
46
+ }
47
+ for (const contentType of ['application/json', 'application/vnd.api+json']) {
48
+ if (app.hasContentTypeParser(contentType)) app.removeContentTypeParser(contentType)
49
+ app.addContentTypeParser(contentType, { parseAs: 'string' }, parse)
50
+ }
48
51
  }
49
52
 
50
53
  export const FastifyPlugin = {
51
54
  name: 'fastify',
52
55
  dependencies: ['rest-api'],
53
56
 
54
- async install ({ vars, helpers, pluginOptions, log, api, runHooks, addHook }) {
57
+ async install ({ vars, helpers, pluginOptions, log, api, scopes, runHooks, addHook }) {
55
58
  const fastifyOptions = pluginOptions || {}
59
+ const httpValidators = fastifyOptions.httpValidators ?? false
60
+ if (typeof httpValidators !== 'boolean') throw new TypeError('httpValidators must be a boolean')
56
61
  const app = fastifyOptions.app
57
62
 
58
63
  if (!app || typeof app.route !== 'function') {
59
64
  throw new Error('FastifyPlugin requires a Fastify instance in pluginOptions.app.')
60
65
  }
61
66
 
62
- const enhancedLog = createEnhancedLogger(log, {
63
- logFullErrors: true,
64
- includeStack: true
65
- })
66
-
67
67
  if (!api.http) {
68
68
  api.http = {}
69
69
  }
@@ -80,19 +80,50 @@ export const FastifyPlugin = {
80
80
  publicBaseUrl
81
81
  }
82
82
 
83
- registerVendorJsonParser(app)
83
+ const ensureContext = (request, reply) => {
84
+ if (!request.jsonRestContext) {
85
+ const requestData = buildTransportRequestData({
86
+ method: request.method,
87
+ url: request.url,
88
+ path: request.url,
89
+ headers: request.headers,
90
+ body: request.body,
91
+ params: request.params,
92
+ query: request.query
93
+ })
94
+ const { context } = createConnectorContext({
95
+ request, reply, source: 'fastify', mountPath, publicBaseUrl, requestData, createContext
96
+ })
97
+ request.jsonRestContext = context
98
+ }
99
+ return request.jsonRestContext
100
+ }
101
+ const sendResponse = async (request, reply, status, body) => {
102
+ const context = ensureContext(request, reply)
103
+ body = addWriteOutcomeToHttpErrors(body, context)
104
+ const headers = await applyTransportResponseLifecycle({ context, transportData: context.transport, status, body, runHooks })
105
+ applyHeaders(reply, headers)
106
+ return reply.code(status).type('application/vnd.api+json').send(body)
107
+ }
84
108
 
85
109
  const buildFastifyHandler = ({ method, path, handler, routeMeta }) => {
86
110
  const routeSchema = buildTransportRouteSchema({ routeMeta, api })
87
111
 
88
112
  const fastifyErrorHandler = async (error, request, reply) => {
89
- enhancedLog.logError('Fastify request error', error, {
113
+ const context = ensureContext(request, reply)
114
+ await logHttpRequestError({
115
+ error,
116
+ context,
117
+ log,
118
+ scopes,
119
+ routeMeta,
120
+ api,
90
121
  method: request.method,
91
- path: request.url
122
+ path,
123
+ message: 'Fastify request error'
92
124
  })
93
125
 
94
- const context = request.jsonRestContext || null
95
- const transportData = context?.transport || null
126
+ const transportData = context.transport
96
127
  const { status, body, headers } = await handleConnectorError({
97
128
  error,
98
129
  context,
@@ -107,65 +138,54 @@ export const FastifyPlugin = {
107
138
  }
108
139
 
109
140
  const preValidation = async (request, reply) => {
110
- if (!strictContentType || !isWriteMethod(method)) {
111
- return
141
+ const context = ensureContext(request, reply)
142
+ const { rejected, handled } = await runTransportRequestLifecycle({ context, runHooks })
143
+ if (rejected) {
144
+ return sendResponse(request, reply, context.rejection.status || 500, buildTransportRejectionBody(context))
112
145
  }
146
+ if (handled) return
113
147
 
114
- const contentType = request.headers?.['content-type'] || ''
115
- if (isAllowedWriteContentType(contentType)) {
116
- return
148
+ applyHeaders(reply)
149
+ if (!acceptsJsonApi(request.headers?.accept)) {
150
+ return sendResponse(request, reply, 406, getNotAcceptableErrorBody())
117
151
  }
152
+ }
118
153
 
119
- reply.code(415)
120
- reply.type('application/vnd.api+json')
121
- return reply.send(getUnsupportedMediaTypeErrorBody())
154
+ const preParsing = async (request, reply, payload) => {
155
+ if (strictContentType && isWriteMethod(request.method) && !isAllowedWriteContentType(request.headers?.['content-type'])) {
156
+ await sendResponse(request, reply, 415, getUnsupportedMediaTypeErrorBody())
157
+ return
158
+ }
159
+ return payload
122
160
  }
123
161
 
124
162
  return {
125
163
  method,
126
164
  url: path,
127
- ...(routeSchema ? { schema: routeSchema } : {}),
165
+ ...(routeSchema
166
+ ? {
167
+ schema: routeSchema,
168
+ // Use the existing request contract without Ajv's field removal or coercion.
169
+ validatorCompiler: () => (payload) => {
170
+ try {
171
+ const contract = getTransportRouteContract({ routeMeta, api })
172
+ return { value: validateRequestContractOrThrow(contract, payload) }
173
+ } catch (error) { return { error } }
174
+ }
175
+ }
176
+ : {}),
128
177
  errorHandler: fastifyErrorHandler,
178
+ onSend: setJsonApiResponseType,
179
+ preParsing,
129
180
  preValidation,
130
181
  handler: async (request, reply) => {
131
- const requestData = buildTransportRequestData({
132
- method: request.method,
133
- url: request.url,
134
- path: request.url,
135
- headers: request.headers,
136
- body: request.body,
137
- params: request.params,
138
- query: request.query
139
- })
140
- const { context, transportData } = createConnectorContext({
141
- request,
142
- reply,
143
- source: 'fastify',
144
- mountPath,
145
- publicBaseUrl,
146
- requestData,
147
- createContext
148
- })
149
- request.jsonRestContext = context
150
-
151
- const { rejected, handled } = await runTransportRequestLifecycle({
152
- context,
153
- runHooks
154
- })
155
-
156
- if (rejected) {
157
- const rejectionBody = buildTransportRejectionBody(context)
158
- applyHeaders(reply, transportData.response.headers)
159
- reply.code(context.rejection.status || 500)
160
- reply.type('application/vnd.api+json')
161
- return reply.send(rejectionBody)
162
- }
163
-
164
- if (handled) {
165
- return
166
- }
182
+ const context = request.jsonRestContext
183
+ if (context.handled) return
184
+ const transportData = context.transport
167
185
 
168
186
  const outcome = await executeConnectorRoute({
187
+ api,
188
+ httpValidators,
169
189
  method,
170
190
  handler,
171
191
  queryString: extractQueryString(request?.raw?.url || request?.url || ''),
@@ -199,9 +219,32 @@ export const FastifyPlugin = {
199
219
  }
200
220
  }
201
221
 
222
+ const pendingRoutes = []
223
+ let registerRoute = route => pendingRoutes.push(route)
224
+ app.register(async (instance) => {
225
+ registerJsonParsers(instance)
226
+ registerRoute = route => instance.route(route)
227
+ for (const route of pendingRoutes.splice(0)) registerRoute(route)
228
+
229
+ if (fastifyOptions.handle404 !== false) {
230
+ instance.register(async (fallback) => {
231
+ const route = buildFastifyHandler({
232
+ method: 'GET',
233
+ handler: async ({ context }) => ({
234
+ statusCode: 404,
235
+ body: getNotFoundErrorBody(context.transport.request.method, context.transport.request.path.split('?')[0])
236
+ })
237
+ })
238
+ fallback.setErrorHandler(route.errorHandler)
239
+ fallback.addHook('onSend', setJsonApiResponseType)
240
+ fallback.addHook('preParsing', route.preParsing)
241
+ fallback.setNotFoundHandler({ preValidation: route.preValidation }, route.handler)
242
+ }, { prefix: mountPath })
243
+ }
244
+ })
245
+
202
246
  addHook('addRoute', 'fastifyRouteCreator', {}, async ({ context }) => {
203
- const route = buildFastifyHandler(context)
204
- app.route(route)
247
+ registerRoute(buildFastifyHandler(context))
205
248
  log.trace(`Fastify route created: ${context.method} ${context.path}`)
206
249
  })
207
250
 
@@ -1,174 +1,92 @@
1
- /**
2
- * Busboy File Detector for HTTP Multipart Uploads
3
- *
4
- * This detector handles multipart/form-data uploads using the busboy library.
5
- * It's suitable for both HTTP and Express connectors.
6
- *
7
- * Features:
8
- * - Streaming parser (memory efficient)
9
- * - Configurable file size limits
10
- * - Automatic field parsing
11
- * - No temporary files by default (keeps in memory)
12
- *
13
- * Usage:
14
- * ```javascript
15
- * import { createBusboyDetector } from 'jsonrestapi/plugins/core/lib/busboy-detector.js';
16
- *
17
- * api.use(HttpPlugin, {
18
- * fileParser: 'busboy',
19
- * fileParserOptions: {
20
- * limits: { fileSize: 10 * 1024 * 1024 } // 10MB
21
- * }
22
- * });
23
- * ```
24
- */
25
-
26
- import { requirePackage } from 'hooked-api'
27
- import { Readable } from 'stream'
28
-
29
- let Busboy
30
- try {
31
- Busboy = (await import('busboy')).default
32
- } catch (e) {
33
- requirePackage('busboy', 'express/http-connector',
34
- 'Busboy is required for multipart/form-data file uploads. This is a peer dependency.')
1
+ import { throwMissingPackage } from '../../../../lib/missing-package.js'
2
+ import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
3
+ import { isMultipartContentType } from './transport-http-helpers.js'
4
+ import { addMultipartField, multipartLimits } from './multipart-helpers.js'
5
+
6
+ let busboyFactory
7
+ try { busboyFactory = (await import('busboy')).default } catch {
8
+ throwMissingPackage('busboy', 'express-connector', 'Busboy is required for multipart uploads. Install the optional busboy peer.')
35
9
  }
36
10
 
37
- /**
38
- * Creates a busboy-based file detector
39
- *
40
- * @param {Object} options - Busboy configuration options
41
- * @param {Object} options.limits - Size limits
42
- * @param {number} options.limits.fileSize - Max file size in bytes
43
- * @param {number} options.limits.files - Max number of files
44
- * @param {number} options.limits.fields - Max number of fields
45
- * @returns {Object} Detector object with detect() and parse() methods
46
- */
11
+ /** Parse a streamed multipart request into buffered files and text fields. */
47
12
  export function createBusboyDetector (options = {}) {
13
+ const limits = { ...multipartLimits, ...options.limits }
48
14
  return {
49
15
  name: 'busboy-multipart',
50
-
51
- /**
52
- * Check if this detector can handle the request
53
- * @param {Object} params - Request parameters
54
- * @returns {boolean} True if this is a multipart request
55
- */
56
- detect: (params) => {
57
- const req = params._httpReq || params._expressReq
58
- if (!req || !req.headers) return false
59
-
60
- const contentType = req.headers['content-type'] || ''
61
- return contentType.includes('multipart/form-data')
62
- },
63
-
64
- /**
65
- * Parse multipart data from the request
66
- * @param {Object} params - Request parameters
67
- * @returns {Promise<{fields: Object, files: Object}>} Parsed data
68
- */
16
+ detect: params => isMultipartContentType((params._httpReq || params._expressReq)?.headers?.['content-type']),
69
17
  parse: async (params) => {
70
18
  const req = params._httpReq || params._expressReq
71
-
19
+ if (!req?.pipe || req.aborted) throw new RestApiPayloadError('Multipart request is unavailable or aborted')
72
20
  return new Promise((resolve, reject) => {
73
- const busboy = new Busboy({
74
- headers: req.headers,
75
- ...options
21
+ let parser
22
+ const fields = new Map()
23
+ const files = new Map()
24
+ const fileNames = new Set()
25
+ let failure
26
+ const cleanup = () => {
27
+ req.removeListener('aborted', onAborted)
28
+ req.removeListener('error', onError)
29
+ }
30
+ const fail = error => {
31
+ if (failure) return
32
+ failure = error
33
+ fields.clear()
34
+ files.clear()
35
+ req.unpipe(parser)
36
+ req.resume()
37
+ cleanup()
38
+ // A limit event can run inside Busboy's current write; destroy afterwards.
39
+ queueMicrotask(() => parser?.destroy())
40
+ reject(error)
41
+ }
42
+ const onError = cause => fail(new RestApiPayloadError('Malformed multipart request', { cause }))
43
+ const onAborted = () => fail(new RestApiPayloadError('Multipart request aborted'))
44
+ const limitError = field => new RestApiPayloadError(`Multipart limit exceeded: ${field}`, { path: field, statusCode: 413 })
45
+ try {
46
+ parser = busboyFactory({
47
+ defParamCharset: 'utf8',
48
+ ...options,
49
+ headers: req.headers,
50
+ // Busboy reports truncation when a limit is reached, including exact size.
51
+ limits: { ...limits, fileSize: limits.fileSize + 1, fieldSize: limits.fieldSize + 1, parts: limits.parts + 1 }
52
+ })
53
+ } catch (cause) { reject(new RestApiPayloadError('Invalid multipart headers or boundary', { cause })); return }
54
+ req.once('aborted', onAborted)
55
+ req.once('error', onError)
56
+ parser.on('field', (name, value, info) => {
57
+ if (failure) return
58
+ if (info.valueTruncated || info.nameTruncated || Buffer.byteLength(value) > limits.fieldSize) return fail(limitError(name))
59
+ try { addMultipartField(fields, name, value) } catch (error) { fail(error) }
76
60
  })
77
-
78
- const fields = {}
79
- const files = {}
80
- const filePromises = []
81
-
82
- // Handle fields
83
- busboy.on('field', (fieldname, val) => {
84
- // Handle array notation (field[] or field[0])
85
- const arrayMatch = fieldname.match(/^(.+)\[\d*\]$/)
86
- if (arrayMatch) {
87
- const baseName = arrayMatch[1]
88
- if (!fields[baseName]) {
89
- fields[baseName] = []
90
- }
91
- if (Array.isArray(fields[baseName])) {
92
- fields[baseName].push(val)
93
- }
94
- } else {
95
- fields[fieldname] = val
61
+ parser.on('file', (name, stream, { filename, encoding, mimeType }) => {
62
+ stream.on('error', onError)
63
+ if (failure) { stream.resume(); return }
64
+ if (fileNames.has(name)) {
65
+ stream.resume()
66
+ fail(new RestApiPayloadError(`Multiple files for field '${name}' are not supported`, { path: name }))
67
+ return
96
68
  }
97
- })
98
-
99
- // Handle files
100
- busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
69
+ fileNames.add(name)
101
70
  const chunks = []
102
71
  let size = 0
103
-
104
- const filePromise = new Promise((resolve, reject) => {
105
- file.on('data', (chunk) => {
106
- chunks.push(chunk)
107
- size += chunk.length
108
- })
109
-
110
- file.on('limit', () => {
111
- reject(new Error(`File size limit exceeded for field '${fieldname}'`))
112
- })
113
-
114
- file.on('end', () => {
115
- files[fieldname] = {
116
- filename,
117
- mimetype,
118
- encoding,
119
- size,
120
- data: Buffer.concat(chunks)
121
- }
122
- resolve()
123
- })
124
-
125
- file.on('error', reject)
72
+ stream.on('data', chunk => {
73
+ if (failure) return
74
+ size += chunk.length
75
+ if (size > limits.fileSize) { fail(limitError(name)); return }
76
+ chunks.push(chunk)
77
+ })
78
+ stream.on('limit', () => fail(limitError(name)))
79
+ stream.on('end', () => {
80
+ if (!failure) files.set(name, { filename, encoding, mimetype: mimeType, size, data: Buffer.concat(chunks) })
126
81
  })
127
-
128
- filePromises.push(filePromise)
129
- })
130
-
131
- // Handle completion
132
- busboy.on('finish', async () => {
133
- try {
134
- // Wait for all files to be fully read
135
- await Promise.all(filePromises)
136
- resolve({ fields, files })
137
- } catch (error) {
138
- reject(error)
139
- }
140
- })
141
-
142
- // Handle errors
143
- busboy.on('error', (error) => {
144
- reject(error)
145
- })
146
-
147
- // Handle limit errors
148
- busboy.on('partsLimit', () => {
149
- reject(new Error('Parts limit exceeded'))
150
- })
151
-
152
- busboy.on('filesLimit', () => {
153
- reject(new Error('Files limit exceeded'))
154
82
  })
155
-
156
- busboy.on('fieldsLimit', () => {
157
- reject(new Error('Fields limit exceeded'))
83
+ for (const event of ['partsLimit', 'filesLimit', 'fieldsLimit']) parser.on(event, () => fail(limitError(event)))
84
+ parser.on('error', onError)
85
+ parser.on('close', () => {
86
+ cleanup()
87
+ if (!failure) resolve({ fields: Object.fromEntries(fields), files: Object.fromEntries(files) })
158
88
  })
159
-
160
- // Pipe request to busboy
161
- if (req.pipe) {
162
- req.pipe(busboy)
163
- } else if (req.on) {
164
- // For already buffered requests
165
- const stream = new Readable()
166
- stream.push(req.body || req)
167
- stream.push(null)
168
- stream.pipe(busboy)
169
- } else {
170
- reject(new Error('Request object is not a stream'))
171
- }
89
+ req.pipe(parser)
172
90
  })
173
91
  }
174
92
  }