json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +733 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +37 -0
  44. package/index.js +3 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +70 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. package/tests/virtual-fields.test.js +0 -404
@@ -1,59 +1,54 @@
1
+ // @ts-check
2
+ /** @import {
3
+ * IdentityContext, LifecycleArguments, ReadContext, 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 { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
8
  import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
9
+ import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
3
10
  import { getRequestedComputedFields, validateRequestedFieldsets } from '../lib/querying-writing/knex-field-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
13
  import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
7
- import { cascadeConfig } from './common.js'
14
+ import { validateRequestedIncludes, filterToOneResponseLinkage } from './common.js'
8
15
 
9
16
  /**
10
- * GET
11
- * Retrieves a single resource by its type and ID.
12
- * @param {(string|number)} id - The unique ID of the resource to fetch.
13
- * @param {object} [queryParams={}] - Optional. An object to customize the query.
14
- * @param {string} [queryParams.include] - A comma-separated string of relationship paths to sideload (e.g., "authors,publisher").
15
- * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets). Keys are resource types, values are comma-separated field names.
16
- * @returns {Promise<object>} A Promise that resolves to the JSON:API response document.
17
+ * Read one resource using params.id and params.queryParams. Missing resources
18
+ * reject; the final resource uses the selected plain or JSON:API representation.
19
+ * @param {LifecycleArguments<Resource>} args
17
20
  */
18
21
  export default async function getMethod ({
19
22
  params,
20
- context,
23
+ context: callerContext,
21
24
  vars,
22
25
  helpers,
23
26
  scope,
24
27
  scopes,
25
28
  runHooks,
26
- apiOptions,
27
- pluginOptions,
28
29
  scopeOptions,
29
30
  scopeName,
30
31
  api
31
32
  }) {
32
- context.method = 'get'
33
+ callerContext.method = 'get'
33
34
 
34
- // Determine which simplified setting to use based on transport
35
- const isTransport = params.isTransport === true
35
+ rejectRemovedOptions(params)
36
+ callerContext.format = resolveFormat(params.format, vars.format)
37
+ callerContext.simplified = callerContext.format === 'plain'
36
38
 
37
- // Use vars which automatically cascade from scope to global
38
- const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
39
-
40
- // Get simplified setting - from params only (per-call override) or use default
41
- context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
42
-
43
- // Assign common context properties
44
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
39
+ callerContext.schemaInfo = scope.vars.schemaInfo
45
40
  const rawQueryParams = params.queryParams || {}
46
- context.queryParams = {
47
- fields: cascadeConfig('fields', [rawQueryParams], {}),
48
- include: cascadeConfig('include', [rawQueryParams], [])
41
+ callerContext.queryParams = {
42
+ fields: rawQueryParams?.fields ?? {},
43
+ ...(rawQueryParams.include == null ? {} : { include: rawQueryParams.include })
49
44
  }
50
45
 
51
- context.transaction = params.transaction
52
- context.db = context.transaction || api.knex.instance
46
+ callerContext.transaction = params.transaction
47
+ callerContext.db = callerContext.transaction || api.knex.instance
53
48
 
54
- context.scopeName = scopeName
49
+ callerContext.scopeName = scopeName
50
+ const context = /** @type {ReadContext<Resource>} */ (callerContext)
55
51
 
56
- // These are just shortcuts used in this function and will be returned
57
52
  const schemaStructure = context.schemaInfo.schemaInstance.structure
58
53
  const schemaRelationships = context.schemaInfo.schemaRelationships
59
54
 
@@ -80,10 +75,12 @@ export default async function getMethod ({
80
75
  context.id = validatedRequest.id
81
76
  context.queryParams = validatedRequest.queryParams || {}
82
77
 
78
+ const identityContext = /** @type {IdentityContext} */ (context)
79
+
83
80
  // Fetch minimal record for authorization checks
84
81
  const minimalRecord = await helpers.dataGetMinimal({
85
82
  scopeName,
86
- context,
83
+ context: identityContext,
87
84
  runHooks
88
85
  })
89
86
 
@@ -100,24 +97,22 @@ export default async function getMethod ({
100
97
 
101
98
  context.minimalRecord = minimalRecord
102
99
 
103
- // Centralised checkPermissions function
104
100
  await scope.checkPermissions({
105
101
  method: 'get',
106
102
  originalContext: context,
107
103
  })
108
104
 
109
105
  await validateRequestedFieldsets(context, scopes)
106
+ validateRequestedIncludes(context, scopes)
110
107
  await runHooks('beforeData')
111
108
  await runHooks('beforeDataGet')
112
109
 
113
110
  context.record = await helpers.dataGet({
114
111
  scopeName,
115
- context,
112
+ context: identityContext,
116
113
  runHooks
117
114
  })
118
115
 
119
- // Check if record was found - storage layer returns null/undefined for non-existent records.
120
- // This generates a proper 404 error with JSON:API error format instead of returning empty data.
121
116
  if (!context.record || !context.record.data) {
122
117
  throw new RestApiResourceError(
123
118
  'Resource not found',
@@ -129,73 +124,46 @@ export default async function getMethod ({
129
124
  )
130
125
  }
131
126
 
132
- // Normalize database values (e.g., convert 1/0 to true/false for booleans)
133
127
  context.record = normalizeRecordAttributes(context.record, scopes)
134
128
 
135
129
  await runHooks('checkDataPermissions')
136
130
  await runHooks('checkDataPermissionsGet')
137
131
 
138
- // Make a backup
139
132
  context.originalRecord = structuredClone(context.record)
140
133
 
141
- // This will enhance record, which is the WHOLE JSON:API record
142
134
  await runHooks('enrichRecord')
143
135
 
144
- // Get computed field information for main resource
145
136
  const computedFields = scope.vars.schemaInfo?.computed || {}
146
- const requestedFields = context.queryParams.fields?.[scopeName]
137
+ const requestedFields = getResourceFieldset(context.queryParams.fields, scopeName)
147
138
  const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
148
139
 
149
- // Enrich attributes for the main resource
150
- // Pass computedDependencies from context (set by dataGet in Knex plugin)
151
- // This tells enrichAttributes which fields to remove after computation
152
140
  context.record.data.attributes = await scope.enrichAttributes({
141
+ id: context.record.data.id,
153
142
  attributes: context.record.data.attributes,
154
143
  parentContext: context,
155
144
  requestedComputedFields,
156
145
  isMainResource: true,
157
- computedDependencies: context.computedDependencies // Fields to remove if not requested
146
+ computedDependencies: context.computedDependencies
158
147
  })
159
148
 
160
- for (const entry of (context.record.included || [])) {
161
- const entryScope = scopes[entry.type]
162
- const entryComputed = entryScope.vars.schemaInfo?.computed || {}
163
- const entryRequestedFields = context.queryParams.fields?.[entry.type]
164
- const entryRequestedComputed = getRequestedComputedFields(
165
- entry.type,
166
- entryRequestedFields,
167
- entryComputed
168
- )
169
-
170
- entry.attributes = await entryScope.enrichAttributes({
171
- attributes: entry.attributes,
172
- parentContext: context,
173
- requestedComputedFields: entryRequestedComputed,
174
- isMainResource: false,
175
- computedDependencies: entry.__$jsonrestapi_computed_deps$__
176
- })
177
- }
149
+ await enrichIncludedAttributes(context, scopes)
178
150
 
179
151
  await runHooks('enrichRecordWithRelationships')
180
152
 
181
- // The called hooks should NOT change context.record
182
153
  await runHooks('finish')
183
154
  await runHooks('finishGet')
155
+ await filterToOneResponseLinkage(context, scopes)
184
156
 
185
157
  // Enrichers and finish hooks run after the database pass and may introduce
186
158
  // native values. Reassert the public temporal contract at the return boundary.
187
159
  context.record = normalizeRecordAttributes(context.record, scopes, {
188
160
  source: 'response'
189
161
  })
162
+ filterResponseFields(context.record, context.queryParams.fields)
190
163
 
191
- // Get schema info for transformation
192
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
164
+ context.schemaInfo = scope.vars.schemaInfo
193
165
 
194
- // Transform output if in simplified mode
195
166
  if (context.simplified) {
196
- // Convert JSON:API response back to simplified format
197
- // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
198
- // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
199
167
  return transformJsonApiToSimplified(
200
168
  { record: context.record },
201
169
  { context: { schemaStructure, schemaRelationships, scopes } }
@@ -0,0 +1,225 @@
1
+ import type { TransactionContext, TransactionFactory, OwnedTransaction } from '../../../lib/transaction-types.js'
2
+ import type { ResourceFormat, WriteReturning, JsonApiDocument, JsonApiResource, ResourceIdentifier } from '../../../types/representations.js'
3
+ import type { RestApiPluginOptions } from '../../../types/plugin-options.js'
4
+ import type { RuntimeLogger } from '../../../types/runtime.js'
5
+ import type { QueryParams } from '../../../types/resource-methods.js'
6
+ import type {
7
+ StorageDatabase, StorageSchemaInfo, StorageFieldDefinition, StorageRow, QueryFilteringState,
8
+ DataWriteHelpers, CanonicalDataReadHelpers, DataRelatedIdsQuery, CanonicalLinkHelpers, QueryBuilderResult
9
+ } from '../lib/storage/storage-types.js'
10
+ import type { MandatoryQueryConstraint } from '../lib/querying/query-constraint.js'
11
+ import { queryConstraint } from '../lib/querying/query-constraint.js'
12
+ import { writePrecondition } from './common.js'
13
+
14
+ export type Identifier = ResourceIdentifier
15
+ export type RelationshipData = Identifier | Identifier[] | null
16
+ export interface Relationship { data?: RelationshipData }
17
+ export interface InputDocument {
18
+ data: {
19
+ type?: string
20
+ id?: string | number
21
+ attributes?: StorageRow
22
+ relationships?: Record<string, Relationship>
23
+ }
24
+ }
25
+ export interface Resource extends JsonApiResource { __$jsonrestapi_computed_deps$__?: string[] }
26
+ export type Document<Data = Resource | Resource[] | null> = Omit<JsonApiDocument<Data>, 'included'> & { included?: Resource[] }
27
+ export interface Field extends StorageFieldDefinition {
28
+ compute?: (args: {
29
+ id?: string | number
30
+ attributes: StorageRow
31
+ record: StorageRow
32
+ context?: LifecycleContext
33
+ helpers: LifecycleHelpers
34
+ api: LifecycleApi
35
+ }) => unknown
36
+ belongsToPolymorphic?: { typeField: string; idField: string; types: string[] }
37
+ through?: string
38
+ foreignKey?: string
39
+ otherKey?: string
40
+ target?: string
41
+ via?: string
42
+ }
43
+ export interface Schema {
44
+ structure: Record<string, Field>
45
+ create(attributes: StorageRow): Promise<ValidationResult>
46
+ patch(attributes: StorageRow): Promise<ValidationResult>
47
+ replace(attributes: StorageRow): Promise<ValidationResult>
48
+ }
49
+ export interface ValidationResult {
50
+ validatedObject: StorageRow & { id?: string | number }
51
+ errors: Record<string, { code?: string; message: string }>
52
+ }
53
+ export interface FieldCallbackContext {
54
+ id?: string | number
55
+ attributes: StorageRow
56
+ fieldName: string
57
+ originalValue: unknown
58
+ originalAttributes: StorageRow
59
+ record?: StorageRow
60
+ parentContext?: LifecycleContext
61
+ scopeName: string
62
+ method?: string
63
+ api: LifecycleApi
64
+ helpers: LifecycleHelpers
65
+ auth?: unknown
66
+ isMainResource?: boolean
67
+ }
68
+ export interface LifecycleSchema extends StorageSchemaInfo {
69
+ idProperty: string
70
+ readDependencies?: Record<string, readonly string[]>
71
+ schemaInstance: Schema
72
+ schemaStructure: Record<string, Field>
73
+ schemaRelationships: Record<string, Field>
74
+ outputRelationships: Record<string, Field>
75
+ outputFields: Record<string, Field>
76
+ computed: Record<string, Field>
77
+ foreignKeyFields: Set<string>
78
+ fieldSetters: Record<string, {
79
+ setter(value: unknown, context: FieldCallbackContext): unknown
80
+ runSetterAfter?: string[]
81
+ }>
82
+ fieldGetters: Record<string, { getter(value: unknown, context: FieldCallbackContext): unknown }>
83
+ sortedSetterFields: string[]
84
+ sortedGetterFields: string[]
85
+ sortedComputedFields: string[]
86
+ }
87
+
88
+ // Library-owned mutable working state, populated by each method before its stage runs.
89
+ // Attribute values stay unknown until a schema or user callback interprets them.
90
+ export interface LifecycleContext<Data = Resource | Resource[] | null, Input = InputDocument> extends TransactionContext {
91
+ method?: string
92
+ scopeName?: string
93
+ schemaInfo?: LifecycleSchema
94
+ db?: StorageDatabase
95
+ inputRecord?: Input
96
+ record?: Document<Data> | Record<string, never> | null
97
+ queryParams?: QueryParams
98
+ params?: LifecycleParams
99
+ id?: string | number
100
+ relationshipName?: string
101
+ format?: ResourceFormat
102
+ simplified?: boolean
103
+ returning?: WriteReturning
104
+ minimalRecord?: (Omit<Resource, 'id'> & { id?: string }) | null
105
+ originalMinimalRecord?: Omit<Resource, 'id'> & { id?: string }
106
+ originalInputAttributes?: StorageRow
107
+ originalRecord?: Document
108
+ responseRecord?: Document | StorageRow
109
+ isCreate?: boolean
110
+ isUpdate?: boolean
111
+ exists?: boolean
112
+ computedDependencies?: string[]
113
+ returnMeta?: Record<string, unknown>
114
+ sortableFields?: string[]
115
+ auth?: unknown
116
+ [customProperty: string]: unknown
117
+ knexQuery?: QueryFilteringState | null
118
+ [queryConstraint]?: MandatoryQueryConstraint
119
+ }
120
+ export interface LifecycleParams extends Record<string, unknown> {
121
+ id?: unknown
122
+ data?: StorageRow
123
+ document?: InputDocument
124
+ format?: ResourceFormat
125
+ returning?: WriteReturning
126
+ transaction?: OwnedTransaction
127
+ queryParams?: QueryParams
128
+ expectedVersion?: unknown
129
+ relationshipName?: string
130
+ relationshipData?: RelationshipData
131
+ [queryConstraint]?: MandatoryQueryConstraint
132
+ [writePrecondition]?: () => unknown
133
+ }
134
+ export interface LifecycleVars extends RestApiPluginOptions {
135
+ schemaInfo: LifecycleSchema
136
+ defaultSort?: string | string[]
137
+ sortableFields?: string[]
138
+ }
139
+ export interface LifecycleResource {
140
+ scopeOptions: RestApiPluginOptions
141
+ vars: LifecycleVars
142
+ get(params: { id?: string | number; queryParams?: QueryParams; transaction?: OwnedTransaction; format: 'jsonapi' }, context?: LifecycleContext): Promise<Document<Resource>>
143
+ get(params: { id?: string | number; queryParams?: QueryParams; transaction?: OwnedTransaction; format: ResourceFormat }, context?: LifecycleContext): Promise<Document<Resource> | StorageRow>
144
+ query(params: { queryParams?: QueryParams; transaction?: OwnedTransaction; format: 'jsonapi'; [queryConstraint]?: MandatoryQueryConstraint }, context?: LifecycleContext): Promise<Document<Resource[]>>
145
+ patch(params: { id?: string | number; document: InputDocument; transaction?: OwnedTransaction; expectedVersion?: unknown; format: 'jsonapi'; returning: 'none' }, context?: LifecycleContext): Promise<void>
146
+ checkPermissions(params: { method: string; originalContext: LifecycleContext }): Promise<unknown>
147
+ applyQueryFilters(params: QueryFilteringState, context: LifecycleContext): Promise<{ query: QueryBuilderResult } | null | undefined>
148
+ enrichAttributes(params: EnrichmentParams): Promise<StorageRow>
149
+ }
150
+ export interface LifecycleApi {
151
+ resources: Record<string, LifecycleResource>
152
+ knex: { instance: StorageDatabase }
153
+ anyapi?: { links?: CanonicalLinkHelpers }
154
+ }
155
+ export interface LifecycleHelpers extends DataWriteHelpers<unknown, unknown>, CanonicalDataReadHelpers {
156
+ newTransaction: TransactionFactory
157
+ db: StorageDatabase
158
+ dataRelatedIdsQuery: DataRelatedIdsQuery
159
+ }
160
+ export type RunHooks = (event: string) => Promise<unknown>
161
+ export interface LifecycleArguments<Data = Resource | Resource[] | null> {
162
+ params: LifecycleParams
163
+ context: LifecycleContext<Data>
164
+ vars: LifecycleVars
165
+ helpers: LifecycleHelpers
166
+ scope: LifecycleResource
167
+ scopes: Record<string, LifecycleResource>
168
+ runHooks: RunHooks
169
+ scopeOptions: RestApiPluginOptions
170
+ scopeName: string
171
+ api: LifecycleApi
172
+ log: RuntimeLogger
173
+ }
174
+ export interface EnrichmentParams {
175
+ id?: string | number
176
+ attributes?: StorageRow
177
+ parentContext?: LifecycleContext
178
+ requestedComputedFields?: string[]
179
+ isMainResource?: boolean
180
+ computedDependencies?: string[]
181
+ }
182
+ export interface EnrichmentArguments extends Omit<LifecycleArguments, 'params' | 'context'> {
183
+ params: EnrichmentParams
184
+ context: Record<string, unknown> & { attributes?: StorageRow }
185
+ }
186
+
187
+ export interface RelationshipPlan {
188
+ belongsToUpdates: StorageRow
189
+ belongsToTargets: Identifier[]
190
+ manyToManyRelationships: Array<{ relName: string; relDef: PivotField; relData: Identifier[] }>
191
+ reverseRelationships: Array<{ relName: string; relDef: Field; relData: RelationshipData }>
192
+ }
193
+
194
+ // These are type views of the same object after the named stage establishes its fields.
195
+ export type ResourceContext<Input = InputDocument> = LifecycleContext<Resource | Resource[] | null, Input> & {
196
+ method: string
197
+ scopeName: string
198
+ schemaInfo: LifecycleSchema
199
+ db: StorageDatabase
200
+ }
201
+ export type IdentityContext = ResourceContext & { id: string | number }
202
+ export type RelationshipContext = IdentityContext & { id: string; relationshipName: string }
203
+ export type WriteRelationshipContext = RelationshipContext & { transaction: OwnedTransaction }
204
+ export type ReadContext<Data = Resource | Resource[] | null> = ResourceContext & {
205
+ format: ResourceFormat
206
+ simplified: boolean
207
+ queryParams: QueryParams
208
+ record?: Document<Data> | null
209
+ }
210
+ export type QueryContext = ReadContext<Resource[]> & { queryParams: QueryParams & { sort: string[] } }
211
+ export type ProcessingContext = ResourceContext<unknown> & {
212
+ inputRecord: object
213
+ params: LifecycleParams
214
+ queryParams: QueryParams
215
+ format: ResourceFormat
216
+ simplified: boolean
217
+ returning: WriteReturning
218
+ transaction: OwnedTransaction
219
+ }
220
+ export type WriteContext = ProcessingContext & { inputRecord: InputDocument }
221
+ export type CompletedWriteContext = WriteContext & { id: string | number }
222
+ export type PivotField = Field & { through: string; foreignKey: string; otherKey: string }
223
+
224
+ export type RelationshipArguments = LifecycleArguments & { params: LifecycleParams & { relationshipName: string } }
225
+ export type RelationshipWriteArguments<Data = RelationshipData> = RelationshipArguments & { params: LifecycleParams & { relationshipData: Data } }
@@ -1,3 +1,9 @@
1
+ // @ts-check
2
+ /** @import {
3
+ * RelationshipWriteArguments, WriteRelationshipContext
4
+ * } from './lifecycle-types.js' */
5
+ import { beginWriteTransaction } from '../../../lib/error-context.js'
6
+ import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
1
7
  import {
2
8
  commitOwnedTransaction,
3
9
  handleWriteMethodError,
@@ -6,30 +12,28 @@ import {
6
12
  import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
7
13
 
8
14
  /**
9
- * PATCH RELATIONSHIP
10
- * Completely replaces a relationship
11
- * PATCH /api/articles/1/relationships/author
12
- *
13
- * @param {string} id - The ID of the resource
14
- * @param {string} relationshipName - The name of the relationship
15
- * @param {object|array} relationshipData - New relationship data
16
- * @returns {Promise<void>} 204 No Content
15
+ * Replace the selected relationship with params.relationshipData.
16
+ * Validate relationship permission/cardinality, then delegate the data mutation
17
+ * and version handling to PATCH using the same transaction. This outer method
18
+ * runs its own finish hooks and completes the transaction only when it owns it.
19
+ * @param {RelationshipWriteArguments} args
17
20
  */
18
- export default async function patchRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
19
- context.method = 'patchRelationship'
20
- context.id = requireExistingResourceId(params.id, {
21
+ export default async function patchRelationshipMethod ({ params, context: callerContext, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
22
+ rejectRemovedOptions(params)
23
+ if (params.format !== undefined) resolveFormat(params.format)
24
+ callerContext.method = 'patchRelationship'
25
+ callerContext.scopeName = scopeName
26
+ callerContext.id = requireExistingResourceId(params.id, {
21
27
  scopeOptions,
22
28
  vars,
23
29
  scopeName
24
30
  })
25
- context.relationshipName = params.relationshipName
26
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
31
+ callerContext.relationshipName = params.relationshipName
32
+ callerContext.schemaInfo = scope.vars.schemaInfo
27
33
 
28
- // Transaction handling
29
- context.transaction = params.transaction ||
30
- (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
31
- context.shouldCommit = !params.transaction && !!context.transaction
32
- context.db = context.transaction || api.knex.instance
34
+ await beginWriteTransaction(callerContext, params.transaction, helpers.newTransaction, runHooks)
35
+ callerContext.db = callerContext.transaction || api.knex.instance
36
+ const context = /** @type {WriteRelationshipContext} */ (callerContext)
33
37
 
34
38
  try {
35
39
  // Check permissions
@@ -47,7 +51,7 @@ export default async function patchRelationshipMethod ({ params, context, vars,
47
51
  // Reuse existing patch with relationship data
48
52
  await scope.patch({
49
53
  id: context.id,
50
- inputRecord: {
54
+ document: {
51
55
  data: {
52
56
  type: scopeName,
53
57
  id: context.id,
@@ -57,17 +61,16 @@ export default async function patchRelationshipMethod ({ params, context, vars,
57
61
  }
58
62
  },
59
63
  transaction: context.transaction,
60
- simplified: false,
61
- isTransport: params.isTransport
64
+ expectedVersion: params.expectedVersion,
65
+ format: 'jsonapi',
66
+ returning: 'none',
62
67
  }, { ...context })
63
68
 
64
69
  await runHooks('finish')
65
70
  await runHooks('finishPatchRelationship')
66
71
 
67
- await commitOwnedTransaction(context, runHooks)
68
-
69
- // 204 No Content
72
+ await commitOwnedTransaction(context)
70
73
  } catch (error) {
71
- await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log, runHooks)
74
+ await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log)
72
75
  }
73
76
  }