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