json-rest-api 1.0.28 → 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 (289) 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 +746 -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} +90 -31
  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 -174
  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 +67 -330
  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 +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
  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 +111 -287
  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 +109 -44
  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 +142 -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 -2509
  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 -1069
  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 -385
  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 -145
  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 +38 -69
  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 +39 -82
  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 -1238
  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 -150
  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 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  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-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. 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'
2
- import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
8
+ import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
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,12 +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
 
81
+ await validateRequestedFieldsets(context, scopes)
82
+ validateRequestedIncludes(context, scopes)
93
83
  await runHooks('beforeData')
94
84
  await runHooks('beforeDataQuery')
95
85
  context.record = await helpers.dataQuery({
@@ -98,32 +88,19 @@ export default async function queryMethod ({
98
88
  runHooks
99
89
  })
100
90
 
101
- // Normalize database values (e.g., convert 1/0 to true/false for booleans)
102
91
  context.record = normalizeRecordAttributes(context.record, scopes)
103
92
 
104
- // Make a backup
105
- try {
106
- context.originalRecord = structuredClone(context.record)
107
- } catch (e) {
108
- log.error('Failed to clone record:', {
109
- error: e.message,
110
- recordKeys: Object.keys(context.record || {}),
111
- hasHttpRequest: !!context.raw?.req
112
- })
113
- throw e
114
- }
93
+ context.originalRecord = structuredClone(context.record)
115
94
 
116
- // This will enhance record, which is the WHOLE JSON:API record
117
95
  await runHooks('enrichRecord')
118
96
 
119
- // Get computed field information for main resource
120
97
  const computedFields = scope.vars.schemaInfo?.computed || {}
121
- const requestedFields = context.queryParams.fields?.[scopeName]
98
+ const requestedFields = getResourceFieldset(context.queryParams.fields, scopeName)
122
99
  const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
123
100
 
124
- // Run enrichAttributes for every single set of attribute, calling it from the right scope
125
101
  for (const entry of context.record.data) {
126
102
  entry.attributes = await scope.enrichAttributes({
103
+ id: entry.id,
127
104
  attributes: entry.attributes,
128
105
  parentContext: context,
129
106
  requestedComputedFields,
@@ -131,40 +108,20 @@ export default async function queryMethod ({
131
108
  computedDependencies: context.computedDependencies
132
109
  })
133
110
  }
134
- for (const entry of (context.record.included || [])) {
135
- const entryScope = scopes[entry.type]
136
- const entryComputed = entryScope.vars.schemaInfo?.computed || {}
137
- const entryRequestedFields = context.queryParams.fields?.[entry.type]
138
- const entryRequestedComputed = getRequestedComputedFields(
139
- entry.type,
140
- entryRequestedFields,
141
- entryComputed
142
- )
143
-
144
- entry.attributes = await entryScope.enrichAttributes({
145
- attributes: entry.attributes,
146
- parentContext: context,
147
- requestedComputedFields: entryRequestedComputed,
148
- isMainResource: false,
149
- computedDependencies: entry.__$jsonrestapi_computed_deps$__
150
- })
151
- }
111
+ await enrichIncludedAttributes(context, scopes)
152
112
 
153
- // The called hooks should NOT change context.record
154
113
  await runHooks('finish')
155
114
  await runHooks('finishQuery')
115
+ await filterToOneResponseLinkage(context, scopes)
156
116
 
157
117
  // Enrichers and finish hooks run after the database pass and may introduce
158
118
  // native values. Reassert the public temporal contract at the return boundary.
159
119
  context.record = normalizeRecordAttributes(context.record, scopes, {
160
120
  source: 'response'
161
121
  })
122
+ filterResponseFields(context.record, context.queryParams.fields)
162
123
 
163
- // Transform output if in simplified mode
164
124
  if (context.simplified) {
165
- // Convert JSON:API response back to simplified format
166
- // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
167
- // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
168
125
  return transformJsonApiToSimplified(
169
126
  { record: context.record },
170
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 { }