json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,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
  }