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,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'
3
- import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
9
+ import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
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,23 +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
 
105
+ await validateRequestedFieldsets(context, scopes)
106
+ validateRequestedIncludes(context, scopes)
109
107
  await runHooks('beforeData')
110
108
  await runHooks('beforeDataGet')
111
109
 
112
110
  context.record = await helpers.dataGet({
113
111
  scopeName,
114
- context,
112
+ context: identityContext,
115
113
  runHooks
116
114
  })
117
115
 
118
- // Check if record was found - storage layer returns null/undefined for non-existent records.
119
- // This generates a proper 404 error with JSON:API error format instead of returning empty data.
120
116
  if (!context.record || !context.record.data) {
121
117
  throw new RestApiResourceError(
122
118
  'Resource not found',
@@ -128,73 +124,46 @@ export default async function getMethod ({
128
124
  )
129
125
  }
130
126
 
131
- // Normalize database values (e.g., convert 1/0 to true/false for booleans)
132
127
  context.record = normalizeRecordAttributes(context.record, scopes)
133
128
 
134
129
  await runHooks('checkDataPermissions')
135
130
  await runHooks('checkDataPermissionsGet')
136
131
 
137
- // Make a backup
138
132
  context.originalRecord = structuredClone(context.record)
139
133
 
140
- // This will enhance record, which is the WHOLE JSON:API record
141
134
  await runHooks('enrichRecord')
142
135
 
143
- // Get computed field information for main resource
144
136
  const computedFields = scope.vars.schemaInfo?.computed || {}
145
- const requestedFields = context.queryParams.fields?.[scopeName]
137
+ const requestedFields = getResourceFieldset(context.queryParams.fields, scopeName)
146
138
  const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
147
139
 
148
- // Enrich attributes for the main resource
149
- // Pass computedDependencies from context (set by dataGet in Knex plugin)
150
- // This tells enrichAttributes which fields to remove after computation
151
140
  context.record.data.attributes = await scope.enrichAttributes({
141
+ id: context.record.data.id,
152
142
  attributes: context.record.data.attributes,
153
143
  parentContext: context,
154
144
  requestedComputedFields,
155
145
  isMainResource: true,
156
- computedDependencies: context.computedDependencies // Fields to remove if not requested
146
+ computedDependencies: context.computedDependencies
157
147
  })
158
148
 
159
- for (const entry of (context.record.included || [])) {
160
- const entryScope = scopes[entry.type]
161
- const entryComputed = entryScope.vars.schemaInfo?.computed || {}
162
- const entryRequestedFields = context.queryParams.fields?.[entry.type]
163
- const entryRequestedComputed = getRequestedComputedFields(
164
- entry.type,
165
- entryRequestedFields,
166
- entryComputed
167
- )
168
-
169
- entry.attributes = await entryScope.enrichAttributes({
170
- attributes: entry.attributes,
171
- parentContext: context,
172
- requestedComputedFields: entryRequestedComputed,
173
- isMainResource: false,
174
- computedDependencies: entry.__$jsonrestapi_computed_deps$__
175
- })
176
- }
149
+ await enrichIncludedAttributes(context, scopes)
177
150
 
178
151
  await runHooks('enrichRecordWithRelationships')
179
152
 
180
- // The called hooks should NOT change context.record
181
153
  await runHooks('finish')
182
154
  await runHooks('finishGet')
155
+ await filterToOneResponseLinkage(context, scopes)
183
156
 
184
157
  // Enrichers and finish hooks run after the database pass and may introduce
185
158
  // native values. Reassert the public temporal contract at the return boundary.
186
159
  context.record = normalizeRecordAttributes(context.record, scopes, {
187
160
  source: 'response'
188
161
  })
162
+ filterResponseFields(context.record, context.queryParams.fields)
189
163
 
190
- // Get schema info for transformation
191
- context.schemaInfo = scopes[scopeName].vars.schemaInfo
164
+ context.schemaInfo = scope.vars.schemaInfo
192
165
 
193
- // Transform output if in simplified mode
194
166
  if (context.simplified) {
195
- // Convert JSON:API response back to simplified format
196
- // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
197
- // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
198
167
  return transformJsonApiToSimplified(
199
168
  { record: context.record },
200
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
  }