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,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 { }