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,14 +1,17 @@
1
- import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
2
- import { processRelationships } from '../lib/writing/relationship-processor.js'
1
+ // @ts-check
2
+ /** @import {
3
+ * CompletedWriteContext, LifecycleArguments, PivotField
4
+ * } from './lifecycle-types.js' */
5
+ import { applyResourceVersion, captureInverseVersions, invalidateInverseVersions } from '../lib/writing/resource-version.js'
6
+ import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
7
+ import { lockRelationshipParent, lockRelationshipTargets, processRelationships } from '../lib/writing/relationship-processor.js'
8
+ import { updateReverseRelationship } from '../lib/writing/reverse-relationship-manipulations.js'
3
9
  import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
4
10
  import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
5
- import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
6
- import {
7
- requireDocumentResourceId,
8
- requireExistingResourceId
9
- } from '../lib/querying-writing/resource-id-normalization.js'
11
+ import { validateUpdateRequest } from '../lib/querying-writing/request-contracts.js'
10
12
  import {
11
13
  setupCommonRequest,
14
+ writePrecondition,
12
15
  validateCompleteReplacePayload,
13
16
  validateResourceAttributesBeforeWrite,
14
17
  validateRelationshipAccess,
@@ -20,109 +23,51 @@ import {
20
23
  } from './common.js'
21
24
 
22
25
  /**
23
- * PUT
24
- * Updates an existing top-level resource by completely replacing it.
25
- * This method supports updating both attributes and relationships (1:1 and n:n).
26
- * Existing persisted values cannot be silently dropped: if a stored attribute or belongsTo
27
- * relationship already has a value, PUT must include it explicitly. Relationship collections
28
- * still follow replacement semantics when a relationships object is provided.
29
- * This method does NOT support creating new related resources via an `included` array.
26
+ * Create or replace a resource at the selected ID. Replacement requires existing
27
+ * stored values to be explicitly represented; relationship collections follow
28
+ * replacement semantics when a relationships object is supplied. This operation
29
+ * links existing related resources rather than creating an included graph.
30
+ * @param {LifecycleArguments} args
30
31
  */
31
32
  export default async function putMethod ({
32
33
  params,
33
- context,
34
+ context: callerContext,
34
35
  vars,
35
36
  helpers,
36
37
  scope,
37
38
  scopes,
38
39
  runHooks,
39
- apiOptions,
40
- pluginOptions,
41
40
  scopeOptions,
42
41
  scopeName,
43
42
  api,
44
43
  log
45
44
  }) {
46
- context.method = 'put'
45
+ callerContext.method = 'put'
47
46
 
48
47
  try {
49
- const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
48
+ const { schema, schemaStructure, schemaRelationships, versionState, context: preparedContext } = await setupCommonRequest({
50
49
  params,
51
- context,
50
+ context: callerContext,
52
51
  vars,
53
52
  scopes,
54
- scopeOptions,
55
53
  scopeName,
56
54
  api,
57
- helpers
55
+ helpers,
56
+ runHooks
58
57
  })
59
- // Run early hooks for pre-processing (e.g., file handling)
60
58
  await runHooks('beforeProcessing')
61
59
  await runHooks('beforeProcessingPut')
62
60
 
63
- const requestContracts = getRequestContracts({
64
- scopeName,
65
- schemaInfo: context.schemaInfo,
66
- includeDepthLimit: vars.includeDepthLimit,
67
- sortableFields: vars.sortableFields
68
- })
69
- const normalizedPathId = params.id === undefined
70
- ? null
71
- : requireExistingResourceId(params.id, {
72
- scopeOptions,
73
- vars,
74
- scopeName
75
- })
76
-
77
- if (normalizedPathId && !context.inputRecord?.data?.id) {
78
- context.inputRecord = {
79
- ...context.inputRecord,
80
- data: {
81
- ...(context.inputRecord?.data || {}),
82
- id: normalizedPathId
83
- }
84
- }
85
- }
86
-
87
- context.inputRecord = validateRequestContractOrThrow(
88
- requestContracts.put,
89
- context.inputRecord,
90
- 'PUT request body is invalid'
91
- )
92
- const normalizedBodyId = requireDocumentResourceId(context.inputRecord.data.id, {
93
- scopeOptions,
94
- vars
95
- })
96
-
97
- if (normalizedPathId && normalizedPathId !== normalizedBodyId) {
98
- throw new RestApiValidationError(
99
- `ID mismatch. URL path ID '${normalizedPathId}' does not match request body ID '${normalizedBodyId}'`,
100
- {
101
- fields: ['data.id'],
102
- violations: [{
103
- field: 'data.id',
104
- rule: 'id_consistency',
105
- message: 'Request body ID must match URL path ID when both are provided'
106
- }]
107
- }
108
- )
109
- }
110
- context.inputRecord.data.id = normalizedBodyId
111
- context.id = normalizedPathId || normalizedBodyId
61
+ validateUpdateRequest({ method: 'put', params, context: preparedContext, vars, scopeOptions, scopeName })
62
+ const context = /** @type {CompletedWriteContext} */ (preparedContext)
112
63
 
113
- // Validate that user has read access to all related resources
114
- // This ensures users can only create relationships to resources they can access
115
64
  await validateRelationshipAccess(context, context.inputRecord, helpers, api)
116
65
 
117
- // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
118
- // Example: relationships.author -> author_id: '123' for storage
119
- // Example: relationships.tags -> array of pivot records to create later
120
- const { belongsToUpdates, manyToManyRelationships } = processRelationships(
66
+ const { belongsToUpdates, belongsToTargets, manyToManyRelationships, reverseRelationships } = processRelationships(
121
67
  scope,
122
68
  { context }
123
69
  )
124
70
 
125
- // Check existence first
126
71
  context.exists = await helpers.dataExists({
127
72
  scopeName,
128
73
  context
@@ -142,7 +87,7 @@ export default async function putMethod ({
142
87
  if (!minimalRecord) {
143
88
  throw new RestApiResourceError(
144
89
  `Resource not found: ${scopeName}/${context.id}`,
145
- ERROR_SUBTYPES.NOT_FOUND
90
+ { subtype: ERROR_SUBTYPES.NOT_FOUND }
146
91
  )
147
92
  }
148
93
 
@@ -161,53 +106,26 @@ export default async function putMethod ({
161
106
  runHooks
162
107
  })
163
108
 
164
- // For PUT, we also need to handle relationships that are NOT provided
165
- // (they should be set to null/empty when a relationships object is provided)
166
- const allRelationships = {}
167
-
168
- // Collect all defined relationships for this resource
169
- for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
170
- if (relDef.type === 'manyToMany') {
171
- allRelationships[relName] = {
172
- type: 'manyToMany',
173
- relDef: {
174
- through: relDef.through,
175
- foreignKey: relDef.foreignKey,
176
- otherKey: relDef.otherKey
177
- }
178
- }
179
- }
180
- }
181
-
182
- // Also check schema fields for belongsTo relationships
183
- for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
184
- if (fieldDef.as && fieldDef.belongsTo) {
185
- allRelationships[fieldDef.as] = {
186
- type: 'belongsTo',
187
- fieldName,
188
- fieldDef
109
+ // Supplying relationships makes PUT clear omitted linkage; omitting the object preserves it.
110
+ if (context.inputRecord.data.relationships !== undefined) {
111
+ const provided = new Set(Object.keys(context.inputRecord.data.relationships))
112
+ for (const [relName, relDef] of Object.entries(schemaRelationships)) {
113
+ if (provided.has(relName)) continue
114
+ if (relDef.type === 'hasOne' || relDef.type === 'hasMany') {
115
+ reverseRelationships.push({ relName, relDef, relData: relDef.type === 'hasOne' ? null : [] })
116
+ } else if (relDef.type === 'manyToMany') {
117
+ manyToManyRelationships.push({
118
+ relName,
119
+ relDef: /** @type {PivotField} */ ({
120
+ through: relDef.through, foreignKey: relDef.foreignKey, otherKey: relDef.otherKey
121
+ }),
122
+ relData: []
123
+ })
189
124
  }
190
125
  }
191
- }
192
-
193
- // Process missing relationships (PUT should null them out only if relationships object exists)
194
- const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
195
- const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
196
-
197
- // Only null out missing relationships if a relationships object was provided
198
- if (hasRelationshipsObject) {
199
- for (const [relName, relInfo] of Object.entries(allRelationships)) {
200
- if (!providedRelationships.has(relName)) {
201
- if (relInfo.type === 'belongsTo') {
202
- belongsToUpdates[relInfo.fieldName] = null
203
- } else if (relInfo.type === 'manyToMany') {
204
- // Add to manyToManyRelationships with empty array
205
- manyToManyRelationships.push({
206
- relName,
207
- relDef: relInfo.relDef,
208
- relData: [] // Empty array means delete all
209
- })
210
- }
126
+ for (const [field, definition] of Object.entries(schemaStructure)) {
127
+ if (definition.belongsTo && definition.as && !provided.has(definition.as)) {
128
+ belongsToUpdates[field] = null
211
129
  }
212
130
  }
213
131
  }
@@ -220,31 +138,34 @@ export default async function putMethod ({
220
138
  }
221
139
  }
222
140
 
223
- // Centralised checkPermissions function
224
141
  await scope.checkPermissions({
225
142
  method: 'put',
226
143
  originalContext: context,
227
144
  })
228
145
 
146
+ await params[writePrecondition]?.()
147
+
229
148
  await runHooks('beforeDataCall')
230
149
  await runHooks('beforeDataCallPut')
231
150
 
232
- // Apply field setters after validation and before storage
233
- if (context.inputRecord?.data?.attributes) {
234
- context.inputRecord.data.attributes = await applyFieldSetters(
235
- context.inputRecord.data.attributes,
236
- context.schemaInfo,
237
- context,
238
- api,
239
- helpers
240
- )
151
+ await lockRelationshipTargets(api, context.transaction, belongsToTargets)
152
+
153
+ if (!context.isCreate && (manyToManyRelationships.length || reverseRelationships.length)) {
154
+ await lockRelationshipParent({ context, helpers, scopeName })
241
155
  }
242
156
 
243
- // Pass the operation type to the helper
157
+ await applyFieldSetters(context, api, helpers)
158
+ const inverseVersions = await captureInverseVersions({ api, helpers, context, scopeName, isCreate: context.isCreate })
159
+ await applyResourceVersion({ state: versionState, context, helpers, scopeName, isCreate: context.isCreate })
160
+
161
+ const putContext = /** @type {CompletedWriteContext & { isCreate: boolean }} */ (context)
162
+
244
163
  await helpers.dataPut({
245
164
  scopeName,
246
- context
165
+ context: putContext
247
166
  })
167
+ await invalidateInverseVersions({ state: inverseVersions, context, helpers, api })
168
+
248
169
  await runHooks('afterDataCallPut')
249
170
  await runHooks('afterDataCall')
250
171
 
@@ -268,6 +189,7 @@ export default async function putMethod ({
268
189
  await updateManyToManyRelationship(null, {
269
190
  api,
270
191
  context: {
192
+ ...context,
271
193
  resourceId: context.id,
272
194
  relDef,
273
195
  relData,
@@ -275,28 +197,27 @@ export default async function putMethod ({
275
197
  }
276
198
  })
277
199
  } else if (relData.length > 0) {
278
- await createPivotRecords(api, context.id, relDef, relData, context.transaction, context)
200
+ await createPivotRecords(api, context.id, relDef, relData, context.transaction)
279
201
  }
280
202
  }
281
203
 
282
- const ret = await handleRecordReturnAfterWrite({
204
+ for (const { relDef, relData } of reverseRelationships) {
205
+ await updateReverseRelationship({ api, helpers, context, scopeName, relDef, relData })
206
+ }
207
+
208
+ const response = await handleRecordReturnAfterWrite({
283
209
  context,
284
210
  scopeName,
285
211
  api,
286
212
  scopes,
287
- schemaStructure,
288
- schemaRelationships,
289
- scopeOptions,
290
- vars,
291
213
  runHooks,
292
- helpers,
293
- log
214
+ helpers
294
215
  })
295
216
 
296
- await commitOwnedTransaction(context, runHooks)
217
+ await commitOwnedTransaction(context)
297
218
 
298
- return ret
219
+ return response
299
220
  } catch (error) {
300
- await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks)
221
+ await handleWriteMethodError(error, callerContext, 'PUT', scopeName, log)
301
222
  }
302
223
  }
@@ -1,72 +1,61 @@
1
+ // @ts-check
2
+ /** @import {
3
+ * LifecycleArguments, QueryContext, Resource
4
+ * } from './lifecycle-types.js' */
5
+ import { enrichIncludedAttributes } from './enrich-attributes.js'
6
+ import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
1
7
  import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
8
+ import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
2
9
  import { getRequestedComputedFields, validateRequestedFieldsets } from '../lib/querying-writing/knex-field-helpers.js'
3
10
  import { getEffectiveSortableFields } from '../lib/querying/query-field-sort-helpers.js'
4
11
  import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
5
12
  import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
6
- import { cascadeConfig } from './common.js'
13
+ import { validateRequestedIncludes, filterToOneResponseLinkage } from './common.js'
14
+ import { queryConstraint } from '../lib/querying/query-constraint.js'
7
15
 
8
16
  /**
9
- * QUERY
10
- * Retrieves a collection of resources (e.g., a list of articles) based on provided criteria.
11
- * This function sends a GET request to /api/{resourceType}.
12
- *
13
- * @param {string} resourceType - The type of resource collection to fetch (e.g., "articles").
14
- * @param {object} [queryParams={}] - Optional. An object to customize the query for the collection.
15
- * @param {string[]} [queryParams.include=[]] - An optional array of relationship paths to sideload for each resource in the collection. These paths will be converted to a comma-separated string for the URL (e.g., `['author', 'comments.user']` becomes `author,comments.user`). Supports deep relationships (e.g., "publisher.country").
16
- * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets) for each resource in the collection and its included relationships. Keys are resource types, values are comma-separated field names.
17
- * @param {object} [queryParams.filters] - An object to filter the collection. Keys are filter parameters (specific to your API's implementation, e.g., 'status', 'title'), values are the filter criteria.
18
- * @param {string[]} [queryParams.sort=[]] - An optional array of fields to sort the collection by. Each string represents a field; prefix with '-' for descending order (e.g., `['title', '-published-date']` becomes `title,-published-date`).
19
- * @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
20
- * @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
17
+ * Read a filtered collection using params.queryParams and the selected format.
18
+ * Preserve authorization and field selection through collection execution.
19
+ * Pagination metadata belongs to the result; this method sends no HTTP request.
20
+ * @param {LifecycleArguments<Resource[]>} args
21
21
  */
22
22
  export default async function queryMethod ({
23
23
  params,
24
- context,
24
+ context: callerContext,
25
25
  vars,
26
26
  helpers,
27
27
  scope,
28
28
  scopes,
29
29
  runHooks,
30
- apiOptions,
31
- pluginOptions,
32
- scopeOptions,
33
30
  scopeName,
34
- log,
35
31
  api
36
32
  }) {
37
- context.method = 'query'
33
+ callerContext.method = 'query'
34
+ // Nested queries only inherit membership when it is explicitly supplied again.
35
+ callerContext[queryConstraint] = params[queryConstraint]
38
36
 
39
- // Determine which simplified setting to use based on transport
40
- const isTransport = params.isTransport === true
37
+ rejectRemovedOptions(params)
38
+ callerContext.format = resolveFormat(params.format, vars.format)
39
+ callerContext.simplified = callerContext.format === 'plain'
41
40
 
42
- // Use vars which automatically cascade from scope to global
43
- const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
41
+ callerContext.schemaInfo = scope.vars.schemaInfo // This is the object variable created by compileSchemas
42
+ callerContext.queryParams = params.queryParams || {}
44
43
 
45
- // Get simplified setting - from params only (per-call override) or use default
46
- context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
44
+ callerContext.queryParams.fields = params.queryParams?.fields ?? {}
45
+ if (callerContext.queryParams.include == null) delete callerContext.queryParams.include
46
+ callerContext.queryParams.sort = params.queryParams?.sort ?? []
47
+ callerContext.queryParams.page = params.queryParams?.page ?? {}
47
48
 
48
- // Assign common context properties
49
- context.schemaInfo = scopes[scopeName].vars.schemaInfo // This is the object variable created by compileSchemas
50
- context.queryParams = params.queryParams || {}
49
+ callerContext.transaction = params.transaction
50
+ callerContext.db = callerContext.transaction || api.knex.instance
51
51
 
52
- // These only make sense as parameter per query
53
- context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {})
54
- context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
55
- context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
56
- context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
52
+ callerContext.scopeName = scopeName
53
+ const context = /** @type {QueryContext} */ (callerContext)
57
54
 
58
- context.transaction = params.transaction
59
- context.db = context.transaction || api.knex.instance
60
-
61
- context.scopeName = scopeName
62
-
63
- // These are just shortcuts used in this function and will be returned
64
55
  const schemaStructure = context.schemaInfo.schemaInstance.structure
65
56
  const schemaRelationships = context.schemaInfo.schemaRelationships
66
57
 
67
- // Sortable fields and sort (mab)
68
58
  context.sortableFields = getEffectiveSortableFields(vars)
69
- // Apply default sort if no sort specified
70
59
  if (context.queryParams.sort.length === 0 && vars.defaultSort) {
71
60
  context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
72
61
  }
@@ -84,13 +73,13 @@ export default async function queryMethod ({
84
73
  )
85
74
  context.queryParams = validatedRequest.queryParams || {}
86
75
 
87
- // Centralised checkPermissions function
88
76
  await scope.checkPermissions({
89
77
  method: 'query',
90
78
  originalContext: context,
91
79
  })
92
80
 
93
81
  await validateRequestedFieldsets(context, scopes)
82
+ validateRequestedIncludes(context, scopes)
94
83
  await runHooks('beforeData')
95
84
  await runHooks('beforeDataQuery')
96
85
  context.record = await helpers.dataQuery({
@@ -99,32 +88,19 @@ export default async function queryMethod ({
99
88
  runHooks
100
89
  })
101
90
 
102
- // Normalize database values (e.g., convert 1/0 to true/false for booleans)
103
91
  context.record = normalizeRecordAttributes(context.record, scopes)
104
92
 
105
- // Make a backup
106
- try {
107
- context.originalRecord = structuredClone(context.record)
108
- } catch (e) {
109
- log.error('Failed to clone record:', {
110
- error: e.message,
111
- recordKeys: Object.keys(context.record || {}),
112
- hasHttpRequest: !!context.raw?.req
113
- })
114
- throw e
115
- }
93
+ context.originalRecord = structuredClone(context.record)
116
94
 
117
- // This will enhance record, which is the WHOLE JSON:API record
118
95
  await runHooks('enrichRecord')
119
96
 
120
- // Get computed field information for main resource
121
97
  const computedFields = scope.vars.schemaInfo?.computed || {}
122
- const requestedFields = context.queryParams.fields?.[scopeName]
98
+ const requestedFields = getResourceFieldset(context.queryParams.fields, scopeName)
123
99
  const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
124
100
 
125
- // Run enrichAttributes for every single set of attribute, calling it from the right scope
126
101
  for (const entry of context.record.data) {
127
102
  entry.attributes = await scope.enrichAttributes({
103
+ id: entry.id,
128
104
  attributes: entry.attributes,
129
105
  parentContext: context,
130
106
  requestedComputedFields,
@@ -132,40 +108,20 @@ export default async function queryMethod ({
132
108
  computedDependencies: context.computedDependencies
133
109
  })
134
110
  }
135
- for (const entry of (context.record.included || [])) {
136
- const entryScope = scopes[entry.type]
137
- const entryComputed = entryScope.vars.schemaInfo?.computed || {}
138
- const entryRequestedFields = context.queryParams.fields?.[entry.type]
139
- const entryRequestedComputed = getRequestedComputedFields(
140
- entry.type,
141
- entryRequestedFields,
142
- entryComputed
143
- )
144
-
145
- entry.attributes = await entryScope.enrichAttributes({
146
- attributes: entry.attributes,
147
- parentContext: context,
148
- requestedComputedFields: entryRequestedComputed,
149
- isMainResource: false,
150
- computedDependencies: entry.__$jsonrestapi_computed_deps$__
151
- })
152
- }
111
+ await enrichIncludedAttributes(context, scopes)
153
112
 
154
- // The called hooks should NOT change context.record
155
113
  await runHooks('finish')
156
114
  await runHooks('finishQuery')
115
+ await filterToOneResponseLinkage(context, scopes)
157
116
 
158
117
  // Enrichers and finish hooks run after the database pass and may introduce
159
118
  // native values. Reassert the public temporal contract at the return boundary.
160
119
  context.record = normalizeRecordAttributes(context.record, scopes, {
161
120
  source: 'response'
162
121
  })
122
+ filterResponseFields(context.record, context.queryParams.fields)
163
123
 
164
- // Transform output if in simplified mode
165
124
  if (context.simplified) {
166
- // Convert JSON:API response back to simplified format
167
- // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
168
- // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
169
125
  return transformJsonApiToSimplified(
170
126
  { record: context.record },
171
127
  { context: { schemaStructure, schemaRelationships, scopes } }
@@ -1,9 +1,10 @@
1
+ // @ts-check
1
2
  /**
2
3
  * release
3
4
  * Runs the hook that tells plugins to release resources
4
5
  */
6
+ /** @param {import('../../../types/runtime.js').RuntimeArguments} args */
5
7
  export default async ({ runHooks }) => {
6
- // Run the addRoute hook to notify transport plugins
7
8
  await runHooks('release')
8
9
 
9
10
  return { }