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,7 +1,15 @@
1
+ // @ts-check
2
+ /** @import {
3
+ * Identifier, LifecycleResource, RelationshipWriteArguments, WriteRelationshipContext
4
+ * } from './lifecycle-types.js' */
5
+ import { lockRelationshipParent } from '../lib/writing/relationship-processor.js'
6
+ import { advanceResourceVersion } from '../lib/writing/resource-version.js'
7
+ import { beginWriteTransaction } from '../../../lib/error-context.js'
8
+ import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
9
+ import { findRelationshipDefinition } from '../lib/querying-writing/relationship-contracts.js'
1
10
  import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
2
11
  import {
3
12
  commitOwnedTransaction,
4
- findRelationshipDefinition,
5
13
  getVisibleRelationshipParent,
6
14
  handleWriteMethodError,
7
15
  validateRelationshipRoutePayload
@@ -10,32 +18,33 @@ import {
10
18
  normalizeRelationshipIdentifiers,
11
19
  requireExistingResourceId
12
20
  } from '../lib/querying-writing/resource-id-normalization.js'
21
+ import { updateReverseRelationship } from '../lib/writing/reverse-relationship-manipulations.js'
22
+ import { invalidateRemovedPivotVersions } from '../lib/writing/many-to-many-manipulations.js'
23
+ import { getStorageColumn } from '../lib/storage/storage-mapping.js'
24
+ import { RELATIONSHIP_WRITE_BATCH_SIZE } from '../lib/querying-writing/knex-constants.js'
13
25
 
14
26
  /**
15
- * DELETE RELATIONSHIP
16
- * Removes specific members from a to-many relationship
17
- * DELETE /api/articles/1/relationships/tags
18
- *
19
- * @param {string} id - The ID of the resource
20
- * @param {string} relationshipName - The name of the relationship
21
- * @param {array} relationshipData - Array of resource identifiers to remove
22
- * @returns {Promise<void>} 204 No Content
27
+ * Remove the identifiers in params.relationshipData from a to-many relationship.
28
+ * Authorization and parent/version locking precede mutation. The method
29
+ * returns no resource and completes only an owned transaction.
30
+ * @param {RelationshipWriteArguments<Identifier[]>} args
23
31
  */
24
- export default async function deleteRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
25
- context.method = 'deleteRelationship'
26
- context.id = requireExistingResourceId(params.id, {
32
+ export default async function deleteRelationshipMethod ({ params, context: callerContext, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
33
+ rejectRemovedOptions(params)
34
+ if (params.format !== undefined) resolveFormat(params.format)
35
+ callerContext.method = 'deleteRelationship'
36
+ callerContext.scopeName = scopeName
37
+ callerContext.id = requireExistingResourceId(params.id, {
27
38
  scopeOptions,
28
39
  vars,
29
40
  scopeName
30
41
  })
31
- context.relationshipName = params.relationshipName
32
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
42
+ callerContext.relationshipName = params.relationshipName
43
+ callerContext.schemaInfo = scope.vars.schemaInfo
33
44
 
34
- // Transaction handling
35
- context.transaction = params.transaction ||
36
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
37
- context.shouldCommit = !params.transaction && !!context.transaction
38
- context.db = context.transaction || api.knex.instance
45
+ await beginWriteTransaction(callerContext, params.transaction, helpers.newTransaction, runHooks)
46
+ callerContext.db = callerContext.transaction || api.knex.instance
47
+ const context = /** @type {WriteRelationshipContext} */ (callerContext)
39
48
 
40
49
  try {
41
50
  // Validate
@@ -61,7 +70,7 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
61
70
  operation: 'deleteRelationship',
62
71
  relationshipData: params.relationshipData
63
72
  })
64
- params.relationshipData = normalizeRelationshipIdentifiers(params.relationshipData, { api })
73
+ params.relationshipData = /** @type {Identifier[]} */ (normalizeRelationshipIdentifiers(params.relationshipData, { api }))
65
74
 
66
75
  // Check permissions
67
76
  await runHooks('checkPermissions')
@@ -78,6 +87,14 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
78
87
  if (!parentRecord) {
79
88
  throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
80
89
  }
90
+ context.minimalRecord = parentRecord
91
+ context.originalMinimalRecord = parentRecord
92
+
93
+ await runHooks('beforeDataCall')
94
+ await runHooks('beforeDataCallDeleteRelationship')
95
+
96
+ const revision = await advanceResourceVersion({ scopeName, context, helpers, expectedVersion: params.expectedVersion })
97
+ if (revision === undefined) await lockRelationshipParent({ context, helpers, scopeName })
81
98
 
82
99
  // Remove relationships
83
100
  if (relDef?.through) {
@@ -92,45 +109,30 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
92
109
  } else {
93
110
  const knex = api.knex?.instance || helpers.db
94
111
  const pivotResource = relDef.through
95
- const pivotScope = api.resources[pivotResource]
112
+ const pivotScope = /** @type {LifecycleResource} */ (api.resources[pivotResource])
96
113
  const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource
97
- const localKey = relDef.foreignKey
98
- const foreignKey = relDef.otherKey
114
+ const localKey = getStorageColumn(pivotScope.vars.schemaInfo, /** @type {string} */ (relDef.foreignKey))
115
+ const foreignKey = getStorageColumn(pivotScope.vars.schemaInfo, /** @type {string} */ (relDef.otherKey))
99
116
 
100
- for (const identifier of params.relationshipData) {
101
- await knex(pivotTable)
117
+ const ids = [...new Set(params.relationshipData.map(identifier => identifier.id))]
118
+ for (let offset = 0; offset < ids.length; offset += RELATIONSHIP_WRITE_BATCH_SIZE) {
119
+ const removals = knex(pivotTable)
102
120
  .where(localKey, context.id)
103
- .where(foreignKey, identifier.id)
104
- .delete()
121
+ .whereIn(foreignKey, ids.slice(offset, offset + RELATIONSHIP_WRITE_BATCH_SIZE))
105
122
  .transacting(context.transaction)
123
+ await invalidateRemovedPivotVersions(api, relDef, removals, context.transaction)
124
+ await removals.delete()
106
125
  }
107
126
  }
108
127
  } else {
109
- // Null out foreign keys for hasMany
110
- const targetType = relDef.target
111
- for (const identifier of params.relationshipData) {
112
- await api.resources[targetType].patch({
113
- id: identifier.id,
114
- inputRecord: {
115
- data: {
116
- type: targetType,
117
- id: identifier.id,
118
- attributes: { [relDef.foreignKey]: null }
119
- }
120
- },
121
- transaction: context.transaction,
122
- simplified: false
123
- }, { ...context })
124
- }
128
+ await updateReverseRelationship({ api, helpers, context, scopeName, relDef, relData: params.relationshipData, operation: 'remove' })
125
129
  }
126
130
 
127
131
  await runHooks('finish')
128
132
  await runHooks('finishDeleteRelationship')
129
133
 
130
- await commitOwnedTransaction(context, runHooks)
131
-
132
- // 204 No Content
134
+ await commitOwnedTransaction(context)
133
135
  } catch (error) {
134
- await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log, runHooks)
136
+ await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log)
135
137
  }
136
138
  }
@@ -1,52 +1,56 @@
1
+ // @ts-check
2
+ /** @import {
3
+ * IdentityContext, LifecycleArguments
4
+ * } from './lifecycle-types.js' */
5
+ import { initializeResourceVersion, applyResourceVersion, captureInverseVersions, invalidateInverseVersions } from '../lib/writing/resource-version.js'
6
+ import { beginWriteTransaction } from '../../../lib/error-context.js'
7
+ import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
1
8
  import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
- import { commitOwnedTransaction, handleWriteMethodError } from './common.js'
9
+ import { commitOwnedTransaction, handleWriteMethodError, writePrecondition } from './common.js'
3
10
  import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
4
11
 
5
12
  /**
6
- * DELETE
7
- * Permanently deletes a resource.
8
- * Returns 204 No Content on success, indicating the resource has been removed.
9
- * This method does not return the deleted resource.
13
+ * Delete an existing resource and complete only an owned transaction.
14
+ * The programmatic method returns no resource; connectors choose HTTP status.
15
+ * @param {LifecycleArguments} args
10
16
  */
11
17
  export default async function deleteMethod ({
12
18
  params,
13
- context,
19
+ context: callerContext,
14
20
  vars,
15
21
  helpers,
16
22
  scope,
17
23
  scopes,
18
24
  runHooks,
19
- apiOptions,
20
- pluginOptions,
21
25
  scopeOptions,
22
26
  scopeName,
23
27
  api,
24
28
  log
25
29
  }) {
26
30
  // Make the method available to all hooks
27
- context.method = 'delete'
31
+ rejectRemovedOptions(params)
32
+ if (params.format !== undefined) resolveFormat(params.format)
33
+ callerContext.method = 'delete'
28
34
 
29
35
  // Set the ID in context
30
- context.id = requireExistingResourceId(params.id, {
36
+ callerContext.id = requireExistingResourceId(params.id, {
31
37
  scopeOptions,
32
38
  vars,
33
39
  scopeName
34
40
  })
35
41
 
36
42
  // Set scopeName in context (needed for broadcasting)
37
- context.scopeName = scopeName
43
+ callerContext.scopeName = scopeName
38
44
 
39
45
  // Set schema info even for DELETE (needed by storage layer)
40
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
46
+ callerContext.schemaInfo = scope.vars.schemaInfo
41
47
 
42
- // Transaction handling
43
- context.transaction = params.transaction ||
44
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
45
- context.shouldCommit = !params.transaction && !!context.transaction
46
- context.db = context.transaction || api.knex.instance
48
+ await beginWriteTransaction(callerContext, params.transaction, helpers.newTransaction, runHooks)
49
+ callerContext.db = callerContext.transaction || api.knex.instance
50
+ const context = /** @type {IdentityContext} */ (callerContext)
47
51
 
48
52
  try {
49
- // No payload validation needed for DELETE
53
+ const versionState = initializeResourceVersion({ context, expectedVersion: params.expectedVersion })
50
54
 
51
55
  // Fetch minimal record for authorization and logging
52
56
  const minimalRecord = await helpers.dataGetMinimal({
@@ -69,12 +73,13 @@ export default async function deleteMethod ({
69
73
  context.originalMinimalRecord = minimalRecord
70
74
  context.minimalRecord = minimalRecord
71
75
 
72
- // Centralised checkPermissions function
73
76
  await scope.checkPermissions({
74
77
  method: 'delete',
75
78
  originalContext: context,
76
79
  })
77
80
 
81
+ await params[writePrecondition]?.()
82
+
78
83
  // Before data operations
79
84
  await runHooks('beforeDataCall')
80
85
  await runHooks('beforeDataCallDelete')
@@ -82,24 +87,25 @@ export default async function deleteMethod ({
82
87
  // Initialize record context for hooks
83
88
  context.record = {}
84
89
 
90
+ const inverseVersions = await captureInverseVersions({ api, helpers, context, scopeName })
91
+ await applyResourceVersion({ state: versionState, context, helpers, scopeName })
92
+
85
93
  // Call the storage helper
86
94
  await helpers.dataDelete({
87
95
  scopeName,
88
96
  context
89
97
  })
90
98
 
99
+ await invalidateInverseVersions({ state: inverseVersions, context, helpers, api, isDelete: true })
100
+
91
101
  await runHooks('afterDataCallDelete')
92
102
  await runHooks('afterDataCall')
93
103
 
94
- // No return record for DELETE (204 No Content)
95
-
96
104
  await runHooks('finish')
97
105
  await runHooks('finishDelete')
98
106
 
99
- await commitOwnedTransaction(context, runHooks)
100
-
101
- // DELETE typically returns void/undefined (204 No Content)
107
+ await commitOwnedTransaction(context)
102
108
  } catch (error) {
103
- await handleWriteMethodError(error, context, 'DELETE', scopeName, log, runHooks)
109
+ await handleWriteMethodError(error, context, 'DELETE', scopeName, log)
104
110
  }
105
111
  }
@@ -1,56 +1,69 @@
1
- import { filterHiddenFields } from '../lib/querying-writing/field-utils.js'
2
-
3
- /**
4
- * enrichAttributes
5
- * Runs the enrichAttributes hook for a specific scope to allow plugins to modify attributes
6
- * before they are returned to the client. This is a scope method so each resource type
7
- * can have its own attribute enrichment logic.
8
- *
9
- *
10
- */
1
+ // @ts-check
2
+ /** @import {
3
+ * EnrichmentArguments, Field, LifecycleContext, LifecycleResource, LifecycleSchema
4
+ * } from './lifecycle-types.js' */
5
+ import { filterHiddenFields, getResourceFieldset, parseFieldset } from '../lib/querying-writing/field-utils.js'
6
+ import { wrapUnexpectedError } from '../../../lib/error-context.js'
7
+ import { getFieldDependencyClosure } from '../lib/querying-writing/schema-helpers.js'
8
+ import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
9
+
10
+ // Resolve field dependencies before filtering and running resource enrichment hooks.
11
+ /** @param {EnrichmentArguments} args */
11
12
  export default async function enrichAttributesMethod ({ context, params, runHooks, scopeName, scopes, api, helpers }) {
12
- // Extract parameters passed to enrichAttributes
13
- // - attributes: The raw attributes from database
14
- // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
15
- // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
16
- // - isMainResource: Whether this is the main resource or an included one
17
- // - computedDependencies: Fields fetched only for computation (to be removed)
18
- // Extract parameters passed to enrichAttributes
19
- // - attributes: The raw attributes from database
20
- // - parentContext: The context from the calling method (has queryParams, transaction, etc.)
21
- // - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
22
- // - isMainResource: Whether this is the main resource or an included one
23
- // - computedDependencies: Fields fetched only for computation (to be removed)
24
- const { attributes, parentContext, requestedComputedFields, isMainResource, computedDependencies } = params || {}
25
-
26
- // Return empty object if no attributes provided
13
+ const { id, attributes, parentContext, requestedComputedFields, isMainResource } = params || {}
14
+
27
15
  if (!attributes) {
28
16
  return {}
29
17
  }
30
18
 
31
- // Get schema, computed field definitions
32
- const schemaStructure = scopes[scopeName]?.vars?.schemaInfo?.schemaStructure || {}
33
- const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {}
34
- const fieldGetters = scopes[scopeName]?.vars?.schemaInfo?.fieldGetters || {}
35
- const sortedGetterFields = scopes[scopeName]?.vars?.schemaInfo?.sortedGetterFields || []
36
- const queryFields = scopes[scopeName]?.vars?.queryFields || {}
19
+ const schemaInfo = scopes[scopeName]?.vars?.schemaInfo || /** @type {Partial<LifecycleSchema>} */ ({})
20
+ const schemaStructure = schemaInfo.schemaStructure || {}
21
+ const computedFields = schemaInfo.computed || {}
22
+ const fieldGetters = schemaInfo.fieldGetters || {}
23
+ const sortedGetterFields = schemaInfo.sortedGetterFields || []
37
24
 
38
- // Make a copy of attributes for transformation
39
25
  const transformedAttributes = { ...attributes }
26
+ const resourceFieldset = getResourceFieldset(parentContext?.queryParams?.fields, scopeName)
27
+ const requestedFields = parseFieldset(resourceFieldset)
28
+ const selectedComputedFields = requestedComputedFields ?? getRequestedComputedFields(
29
+ scopeName, resourceFieldset, computedFields
30
+ )
31
+ const selectedVirtualFields = Object.keys(schemaStructure).filter(name => {
32
+ const definition = /** @type {Field} */ (schemaStructure[name])
33
+ return definition.virtual === true && definition.hidden !== true &&
34
+ (requestedFields === null ? definition.normallyHidden !== true : requestedFields.includes(name))
35
+ })
36
+ const neededFields = getFieldDependencyClosure(schemaInfo, [
37
+ ...Object.keys(attributes), ...selectedComputedFields, ...selectedVirtualFields
38
+ ])
39
+
40
+ // Virtual input belongs only to the resource that was written.
41
+ const inputData = parentContext?.inputRecord?.data
42
+ if (isMainResource !== false && inputData?.type === scopeName &&
43
+ inputData.id !== undefined && String(inputData.id) === String(id) && inputData.attributes) {
44
+ const inputAttrs = inputData.attributes
45
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
46
+ if (fieldDef.virtual === true && neededFields.has(fieldName) && Object.hasOwn(inputAttrs, fieldName) && inputAttrs[fieldName] !== undefined && inputAttrs[fieldName] !== null) {
47
+ if (!Object.hasOwn(transformedAttributes, fieldName)) {
48
+ transformedAttributes[fieldName] = inputAttrs[fieldName]
49
+ }
50
+ }
51
+ })
52
+ }
40
53
 
41
- // STEP 1: Apply field getters in dependency order (before filtering)
42
- // This ensures getters see all fields including hidden ones
54
+ const originalAttributes = { ...transformedAttributes }
55
+ // Getters run in dependency order while hidden dependencies are still available.
43
56
  for (const fieldName of sortedGetterFields) {
44
- // Only process if field exists in attributes
45
- if (fieldName in transformedAttributes) {
46
- const getterInfo = fieldGetters[fieldName]
57
+ if (Object.hasOwn(transformedAttributes, fieldName)) {
58
+ const getterInfo = /** @type {NonNullable<LifecycleSchema['fieldGetters'][string]>} */ (fieldGetters[fieldName])
47
59
  try {
48
60
  const getterContext = {
49
- attributes: transformedAttributes, // Current state with previous getters applied
61
+ id,
62
+ attributes: transformedAttributes,
50
63
  fieldName,
51
- originalValue: attributes[fieldName], // Original untransformed value
52
- originalAttributes: attributes, // All original attributes
53
- record: transformedAttributes, // Alias for compatibility
64
+ originalValue: originalAttributes[fieldName],
65
+ originalAttributes,
66
+ record: transformedAttributes,
54
67
  parentContext,
55
68
  scopeName,
56
69
  api,
@@ -58,126 +71,55 @@ export default async function enrichAttributesMethod ({ context, params, runHook
58
71
  isMainResource: isMainResource !== false
59
72
  }
60
73
 
61
- // Apply getter (can be async)
62
74
  transformedAttributes[fieldName] = await getterInfo.getter(
63
75
  transformedAttributes[fieldName],
64
76
  getterContext
65
77
  )
66
78
  } catch (error) {
67
- console.error(`Error in getter for field '${fieldName}' in ${scopeName}:`, error)
68
- // Keep current value on error (don't break the whole request)
79
+ throw wrapUnexpectedError(error, {
80
+ message: `Getter for field '${fieldName}' failed`,
81
+ context: { scopeName, fieldName, phase: 'getter' }
82
+ })
69
83
  }
70
84
  }
71
85
  }
72
86
 
73
- // Filter hidden fields from attributes based on visibility rules
74
- // This removes hidden:true fields and normallyHidden:true fields (unless requested)
75
- const requestedFields = parentContext?.queryParams?.fields?.[scopeName]
76
- const filteredAttributes = filterHiddenFields(
77
- transformedAttributes,
78
- { structure: { ...schemaStructure, ...queryFields } },
79
- requestedFields
80
- )
81
-
82
- // Handle virtual fields - they need to be added from parent context if available
83
- // Virtual fields come from input and need to be preserved in responses
84
- if (parentContext && parentContext.inputRecord?.data?.attributes) {
85
- const inputAttrs = parentContext.inputRecord.data.attributes
86
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
87
- if (fieldDef.virtual === true && fieldName in inputAttrs && inputAttrs[fieldName] !== undefined && inputAttrs[fieldName] !== null) {
88
- // Add virtual field to filtered attributes if not already there
89
- if (!(fieldName in filteredAttributes)) {
90
- filteredAttributes[fieldName] = inputAttrs[fieldName]
91
- }
92
- }
93
- })
94
- }
95
-
96
- // Now handle sparse fieldsets for virtual fields
97
- if (requestedFields && requestedFields.length > 0) {
98
- const requestedFieldsList = typeof requestedFields === 'string'
99
- ? requestedFields.split(',').map(f => f.trim())
100
- : requestedFields
87
+ const fieldsToCompute = (schemaInfo.sortedComputedFields || []).filter(field => neededFields.has(field))
101
88
 
102
- // Remove virtual fields that weren't requested
103
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
104
- if (fieldDef.virtual === true && fieldName in filteredAttributes) {
105
- if (!requestedFieldsList.includes(fieldName)) {
106
- delete filteredAttributes[fieldName]
107
- }
108
- }
109
- })
110
- }
111
-
112
- // Determine which computed fields to calculate
113
- // We only compute fields that are requested to optimize performance
114
- let fieldsToCompute = []
115
- if (requestedComputedFields) {
116
- // Explicit list provided (from sparse fieldsets)
117
- // Example: ?fields[products]=name,profit_margin -> only compute profit_margin
118
- fieldsToCompute = requestedComputedFields
119
- } else if (isMainResource || !parentContext?.queryParams?.fields) {
120
- // No sparse fieldsets or this is the main resource - compute all fields
121
- // This ensures all computed fields are available when no filtering is applied
122
- fieldsToCompute = Object.keys(computedFields)
123
- }
124
-
125
- // Create compute context with all available resources
126
- // IMPORTANT: We pass the TRANSFORMED attributes (after getters) to compute functions
127
- // This ensures computed fields see the getter-processed values
128
- // Example: if price getter converts string to number, profit_margin sees the number
89
+ // Computations receive getter-transformed values and earlier computed results.
129
90
  const computeContext = {
130
- attributes: transformedAttributes, // Transformed attributes after getters
131
- record: { ...transformedAttributes }, // Full record for convenience
91
+ id,
92
+ attributes: transformedAttributes,
93
+ record: { ...transformedAttributes },
132
94
  context: parentContext,
133
- helpers, // API helpers for complex operations
134
- api, // Full API instance
95
+ helpers,
96
+ api,
135
97
  }
136
98
 
137
- // Auto-compute fields that have compute functions
138
99
  for (const fieldName of fieldsToCompute) {
139
100
  const fieldDef = computedFields[fieldName]
140
101
  if (fieldDef && fieldDef.compute) {
141
102
  try {
142
- // Call the compute function with full context
143
- // Example: profit_margin compute gets { attributes: { price: 100, cost: 60 } }
144
- // and returns: ((100 - 60) / 100 * 100) = "40.00"
145
- filteredAttributes[fieldName] = await fieldDef.compute(computeContext)
103
+ const value = await fieldDef.compute(computeContext)
104
+ transformedAttributes[fieldName] = value
105
+ computeContext.record[fieldName] = value
146
106
  } catch (error) {
147
- // Log error but don't fail the request - computed fields shouldn't break API
148
- console.error(`Error computing field '${fieldName}' for ${scopeName}:`, error)
149
- filteredAttributes[fieldName] = null
107
+ throw wrapUnexpectedError(error, {
108
+ message: `Computation for field '${fieldName}' failed`,
109
+ context: { scopeName, fieldName, phase: 'computed' }
110
+ })
150
111
  }
151
112
  }
152
113
  }
153
114
 
154
- // Remove fields that were only fetched as dependencies
155
- // This is the key to the dependency resolution feature:
156
- // 1. We fetched dependencies from DB (e.g., 'cost' for profit_margin)
157
- // 2. We used them in compute functions
158
- // 3. Now we remove them if they weren't explicitly requested
159
- // Example: User requests profit_margin, we fetch cost, compute, then remove cost
160
- const finalAttributes = { ...filteredAttributes }
161
- if (requestedFields && computedDependencies && computedDependencies.length > 0) {
162
- // Parse requested fields if it's a string
163
- const requested = typeof requestedFields === 'string'
164
- ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
165
- : requestedFields
166
-
167
- for (const dep of computedDependencies) {
168
- // Only remove if it wasn't explicitly requested
169
- // Example: 'cost' is removed unless user explicitly asked for it
170
- if (!requested.includes(dep)) {
171
- delete finalAttributes[dep]
172
- }
173
- }
174
- }
175
-
176
- if (requestedFields && requestedFields.length > 0) {
177
- const requestedList = typeof requestedFields === 'string'
178
- ? requestedFields.split(',').map((field) => field.trim()).filter((field) => field)
179
- : requestedFields
180
- const allowed = new Set(requestedList)
115
+ // Remove private fields and values fetched only for computation.
116
+ const finalAttributes = filterHiddenFields(
117
+ transformedAttributes,
118
+ { structure: schemaInfo.outputFields },
119
+ requestedFields
120
+ )
121
+ if (requestedFields !== null) {
122
+ const allowed = new Set(requestedFields)
181
123
  Object.keys(finalAttributes).forEach((key) => {
182
124
  if (!allowed.has(key)) {
183
125
  delete finalAttributes[key]
@@ -185,20 +127,41 @@ export default async function enrichAttributesMethod ({ context, params, runHook
185
127
  })
186
128
  }
187
129
 
188
- // Create context for enrichAttributes hooks
189
130
  Object.assign(context, {
190
131
  parentContext,
191
- attributes: finalAttributes, // Use the final attributes after dependency removal
132
+ attributes: finalAttributes,
192
133
  computedFields,
193
- requestedComputedFields: fieldsToCompute,
134
+ requestedComputedFields: selectedComputedFields,
194
135
  scopeName,
195
136
  helpers,
196
137
  api
197
138
  })
198
139
 
199
- // Run enrichAttributes hooks for additional/override computations
200
140
  await runHooks('enrichAttributes')
201
141
 
202
- // Return the attributes from context, which hooks may have modified
203
- return context.attributes
204
- };
142
+ return /** @type {import('../lib/storage/storage-types.js').StorageRow} */ (context.attributes)
143
+ }
144
+
145
+ // Preserve include order and each resource's enrichment hooks and fieldset.
146
+ /** @param {LifecycleContext} context @param {Record<string, LifecycleResource>} scopes */
147
+ export async function enrichIncludedAttributes (context, scopes) {
148
+ for (const entry of (context.record?.included || [])) {
149
+ const entryScope = /** @type {LifecycleResource} */ (scopes[entry.type])
150
+ const entryComputed = entryScope.vars.schemaInfo?.computed || {}
151
+ const entryRequestedFields = getResourceFieldset(context.queryParams?.fields, entry.type)
152
+ const entryRequestedComputed = getRequestedComputedFields(
153
+ entry.type,
154
+ entryRequestedFields,
155
+ entryComputed
156
+ )
157
+
158
+ entry.attributes = await entryScope.enrichAttributes({
159
+ id: entry.id,
160
+ attributes: entry.attributes,
161
+ parentContext: context,
162
+ requestedComputedFields: entryRequestedComputed,
163
+ isMainResource: false,
164
+ computedDependencies: entry.__$jsonrestapi_computed_deps$__
165
+ })
166
+ }
167
+ }