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
@@ -0,0 +1,710 @@
1
+ // @ts-check
2
+ /** @import { DataMinimalRequest, DataReadContext, DataRelatedIdsQuery, DataResource, DataSortDescriptor, DataSortRequest, OrdinaryDataDependencies, OrdinaryDataReadHelpers, OrdinaryDataWriteHelpers, StorageRow } from './storage-types.js' */
3
+ import { prepareReferenceSortColumns } from '../querying/knex-query-helpers.js'
4
+ import { applySortDescriptorOrder, buildEffectiveSortList, parseSortEntry, resolveSortField } from '../querying/query-field-sort-helpers.js'
5
+ import { deletePivotReferences, invalidateDeletedPivotTargets } from '../writing/many-to-many-manipulations.js'
6
+ import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
7
+ import { buildJsonApiResponse } from '../querying/knex-json-api-transformers-querying.js'
8
+ import { toJsonApiRecordWithBelongsTo } from '../querying-writing/knex-json-api-transformers.js'
9
+ import { processIncludes } from '../querying/knex-process-includes.js'
10
+ import { loadRelationshipIdentifiers } from '../querying/relationship-identifiers.js'
11
+ import { applyQueryConstraint } from '../querying/query-constraint.js'
12
+ import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
13
+ import { getOperationDiagnosticContext } from '../../../../lib/error-context.js'
14
+ import { applyInsertReturning } from '../querying-writing/database-capabilities.js'
15
+ import { ERROR_SUBTYPES } from '../querying-writing/knex-constants.js'
16
+ import { calculatePaginationMeta, generatePaginationLinks, generateCursorPaginationLinks, buildCursorMeta, applyPaginationToQuery } from '../querying/knex-pagination-helpers.js'
17
+ import { getUrlPrefix } from '../querying/url-helpers.js'
18
+ import { unwrapQueryBuilderState, withQueryFilteringContext } from '../querying/query-builder-utils.js'
19
+ import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
20
+ import { applyDatabaseReadOptions, databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
21
+
22
+ /** @param {OrdinaryDataDependencies} dependencies */
23
+ export function createOrdinaryDataHelpers ({ api, scopes, knex, log, getScopeStorageAdapter: findStorageAdapter }) {
24
+ /** @param {string} scopeName */
25
+ const getScopeStorageAdapter = scopeName => {
26
+ const adapter = findStorageAdapter(scopeName)
27
+ if (!adapter) throw new Error(`Storage adapter for resource '${scopeName}' not found`)
28
+ return adapter
29
+ }
30
+ /** @param {string} scopeName */
31
+ const getScope = scopeName => {
32
+ const scope = api.resources[scopeName]
33
+ if (!scope) throw new Error(`Scope '${scopeName}' not found in api.resources`)
34
+ return scope
35
+ }
36
+ /** @param {DataSortRequest} request @returns {Promise<DataSortDescriptor[]>} */
37
+ const buildSortDescriptors = async ({
38
+ query,
39
+ sort,
40
+ schemaInfo,
41
+ sortableFields,
42
+ storageAdapter,
43
+ defaultSort,
44
+ scopeName,
45
+ context,
46
+ before = false,
47
+ queryFieldRuntimeByField = new Map()
48
+ }) => {
49
+ const effectiveSort = buildEffectiveSortList(sort, { defaultSort, idField: 'id', schemaInfo })
50
+ /** @type {DataSortDescriptor[]} */
51
+ const descriptors = []
52
+ const referenceColumns = await prepareReferenceSortColumns({
53
+ query,
54
+ fields: effectiveSort.map(entry => parseSortEntry(entry).field).filter(field =>
55
+ !queryFieldRuntimeByField.has(field) && (field === 'id' || !sortableFields?.length || sortableFields.includes(field))),
56
+ scopeName,
57
+ tableAlias: storageAdapter.getTableName(),
58
+ context
59
+ }, { scopes, knex, getStorageAdapter: getScopeStorageAdapter })
60
+
61
+ for (const sortEntry of effectiveSort) {
62
+ const { field, direction, sqlDirection } = parseSortEntry(sortEntry)
63
+
64
+ if (field !== 'id' && sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
65
+ try {
66
+ await log.warn('Ignoring non-sortable field', {
67
+ ...getOperationDiagnosticContext(context, { phase: 'querySort', scopeName, backend: knex.client.config.client }), field
68
+ })
69
+ } catch { /* Diagnostics must preserve the skipped sort. */ }
70
+ continue
71
+ }
72
+
73
+ const queryFieldRuntime = queryFieldRuntimeByField.get(field)
74
+ if (queryFieldRuntime) {
75
+ applySortDescriptorOrder(query, { queryFieldRuntime, direction }, { before })
76
+ descriptors.push({
77
+ field,
78
+ direction: sqlDirection,
79
+ queryFieldRuntime,
80
+ definition: queryFieldRuntime.definition,
81
+ isRelationship: false
82
+ })
83
+ continue
84
+ }
85
+
86
+ const dbField = resolveSortField(field, schemaInfo)
87
+ const searchField = schemaInfo.searchSchemaStructure?.[field]
88
+
89
+ const storageColumn = storageAdapter.translateColumn(dbField)
90
+ const reference = referenceColumns.get(field)
91
+ const translatedField = reference?.column || (storageColumn.includes('.')
92
+ ? storageColumn
93
+ : `${storageAdapter.getTableName()}.${storageColumn}`)
94
+
95
+ applySortDescriptorOrder(query, { column: translatedField, direction }, { before })
96
+ descriptors.push({
97
+ field,
98
+ direction: sqlDirection,
99
+ column: translatedField,
100
+ actualField: dbField,
101
+ resultColumn: field === 'id' ? 'id' : storageColumn,
102
+ ...(reference || {}),
103
+ definition: schemaInfo.schemaStructure?.[dbField] || { type: field === 'id' ? 'id' : undefined },
104
+ isRelationship: Boolean(schemaInfo.schemaStructure?.[dbField]?.belongsTo || (dbField !== field && searchField?.isRelationship))
105
+ })
106
+ }
107
+
108
+ if (descriptors.length === 0) {
109
+ query.orderBy('id', 'asc')
110
+ descriptors.push({
111
+ field: 'id',
112
+ direction: 'ASC',
113
+ column: 'id',
114
+ definition: { type: 'id' },
115
+ isRelationship: false
116
+ })
117
+ }
118
+
119
+ return descriptors
120
+ }
121
+
122
+ /** @type {DataRelatedIdsQuery} */
123
+ const dataRelatedIdsQuery = async ({ context, relDef }) => {
124
+ const pivotScope = scopes[relDef.through]
125
+ const pivotAdapter = getScopeStorageAdapter(relDef.through)
126
+ if (!pivotScope || !relDef.foreignKey || !relDef.otherKey) {
127
+ throw new RestApiResourceError('Invalid many-to-many pivot definition', { subtype: 'pivot_table_not_found' })
128
+ }
129
+ const tableName = pivotAdapter.getTableName()
130
+ const pivotContext = {
131
+ ...context,
132
+ method: 'query',
133
+ scopeName: relDef.through,
134
+ schemaInfo: pivotScope.vars.schemaInfo,
135
+ storageAdapter: pivotAdapter,
136
+ queryParams: {}
137
+ }
138
+ await pivotScope.checkPermissions({ method: 'query', originalContext: pivotContext })
139
+ const query = pivotAdapter.buildBaseQuery({ transaction: context.transaction })
140
+ .where({ [`${tableName}.${pivotAdapter.translateColumn(relDef.foreignKey)}`]: pivotAdapter.translateFilterValue(relDef.foreignKey, context.id) })
141
+ const state = await pivotScope.applyQueryFilters({
142
+ query,
143
+ filters: undefined,
144
+ scopeName: relDef.through,
145
+ tableName,
146
+ db: context.db,
147
+ schemaInfo: pivotScope.vars.schemaInfo,
148
+ storageAdapter: pivotAdapter,
149
+ queryPurpose: 'collection'
150
+ }, pivotContext)
151
+ return {
152
+ query: unwrapQueryBuilderState(state, query).clearSelect().select(`${tableName}.${pivotAdapter.translateColumn(relDef.otherKey)}`)
153
+ }
154
+ }
155
+
156
+ /** @type {OrdinaryDataWriteHelpers['dataExists']} */
157
+ const dataExists = async ({ scopeName, context }) => {
158
+ const storageAdapter = getScopeStorageAdapter(scopeName)
159
+ context.storageAdapter = storageAdapter
160
+
161
+ const id = context.id
162
+
163
+ const tableName = storageAdapter.getTableName()
164
+ const idProperty = storageAdapter.getIdColumn()
165
+ const db = context.db || api.knex.instance
166
+
167
+ log.debug(`[Knex] EXISTS ${tableName}/${id}`)
168
+
169
+ const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
170
+
171
+ const record = await db(tableName)
172
+ .where(idProperty, id)
173
+ .select(selectClause)
174
+ .first()
175
+
176
+ return !!record
177
+ }
178
+
179
+ /** @type {OrdinaryDataReadHelpers['dataGet']} */
180
+ const dataGet = async ({ scopeName, context }) => {
181
+ const storageAdapter = getScopeStorageAdapter(scopeName)
182
+ context.storageAdapter = storageAdapter
183
+ const scope = getScope(scopeName)
184
+ const id = context.id
185
+ const tableName = storageAdapter.getTableName()
186
+ const idProperty = storageAdapter.getIdColumn()
187
+ const db = context.db || api.knex.instance
188
+
189
+ log.debug(`[Knex] GET ${tableName}/${id}`)
190
+
191
+ const fieldSelectionInfo = await buildFieldSelection(scope, { context })
192
+ // Enrichment removes fetched dependencies that the caller did not request.
193
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
194
+
195
+ // The preceding minimal read has already applied permission and row filters.
196
+ let query = db(tableName).where(idProperty, id)
197
+
198
+ const selectionState = await applyFieldSelectionToQuery({
199
+ query,
200
+ scope,
201
+ fieldSelectionInfo,
202
+ tableName,
203
+ useTablePrefix: false,
204
+ storageAdapter,
205
+ db,
206
+ context,
207
+ scopeName
208
+ })
209
+ query = selectionState.query
210
+
211
+ const record = await applyDatabaseReadOptions(query).first()
212
+
213
+ if (!record) {
214
+ throw new RestApiResourceError(
215
+ 'Resource not found',
216
+ {
217
+ subtype: ERROR_SUBTYPES.NOT_FOUND,
218
+ resourceType: scopeName,
219
+ resourceId: id
220
+ }
221
+ )
222
+ }
223
+
224
+ const records = [record]
225
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
226
+
227
+ const included = await processIncludes(scope, records, {
228
+ log,
229
+ scopes,
230
+ knex,
231
+ context,
232
+ api
233
+ })
234
+
235
+ return buildJsonApiResponse(scope, records, included, true, scopeName, context)
236
+ }
237
+
238
+ // Supplying ids returns minimal records for the caller's bounded validation batch.
239
+ /** @overload @param {DataMinimalRequest & { ids: Array<string | number> }} request @returns {Promise<DataResource[]>} */
240
+ /** @overload @param {DataMinimalRequest & { ids?: undefined, context: DataReadContext & {id: string | number} }} request @returns {Promise<DataResource | null>} */
241
+ /** @param {DataMinimalRequest & { ids?: Array<string | number> }} request */
242
+ async function dataGetMinimal ({
243
+ scopeName,
244
+ context,
245
+ ids,
246
+ runHooks,
247
+ applyQueryFilters,
248
+ filters = context.queryParams?.filters,
249
+ queryPurpose = 'single'
250
+ }) {
251
+ const storageAdapter = getScopeStorageAdapter(scopeName)
252
+ context.storageAdapter = storageAdapter
253
+ const scope = getScope(scopeName)
254
+ const id = context.id
255
+ const tableName = storageAdapter.getTableName()
256
+ const idProperty = storageAdapter.getIdColumn()
257
+ const db = context.db || api.knex.instance
258
+
259
+ log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
260
+
261
+ // Knex types exclude unknown bindings; the configured driver still validates serializer results.
262
+ let query = ids === undefined
263
+ ? db(tableName).where(idProperty, id)
264
+ : db(tableName).whereIn(idProperty, /** @type {import('knex').Knex.Value[]} */ (ids.map(value => storageAdapter.translateFilterValue('id', value))))
265
+ .distinct(`${tableName}.${idProperty}`)
266
+
267
+ if (typeof applyQueryFilters === 'function') {
268
+ const scopedQueryState = await applyQueryFilters({
269
+ query,
270
+ filters,
271
+ tableName,
272
+ db,
273
+ scopeName,
274
+ queryPurpose,
275
+ storageAdapter
276
+ })
277
+ query = unwrapQueryBuilderState(scopedQueryState, query)
278
+ } else if (runHooks) {
279
+ const filteredState = await withQueryFilteringContext(context, {
280
+ query,
281
+ filters,
282
+ schemaInfo: context.schemaInfo,
283
+ scopeName,
284
+ tableName,
285
+ db,
286
+ queryPurpose,
287
+ adapter: storageAdapter,
288
+ storageAdapter,
289
+ }, async () => {
290
+ await runHooks('knexQueryFiltering')
291
+ })
292
+ query = filteredState.query
293
+ }
294
+
295
+ const identityFields = new Set([...context.schemaInfo.foreignKeyFields, 'id'])
296
+ const identities = Object.fromEntries([...identityFields].map(field => {
297
+ const column = storageAdapter.translateColumn(field)
298
+ return [field === 'id' ? 'id' : column, databaseIdentityExpression(db, `${tableName}.${column}`)]
299
+ }))
300
+ if (ids !== undefined) {
301
+ // Filtering owns predicates; the validation batch owns its identity projection.
302
+ query.clearSelect().distinct(`${tableName}.${idProperty}`)
303
+ // Deduplicate policy joins by ID, without comparing JSON or other row values.
304
+ /** @type {StorageRow[]} */
305
+ const records = await applyDatabaseReadOptions(storageAdapter.buildBaseQuery({ transaction: db }).whereIn(idProperty, query).select(`${tableName}.*`, identities))
306
+ return records.map(record => toJsonApiRecordWithBelongsTo(scope, record, scopeName))
307
+ }
308
+ const record = await applyDatabaseReadOptions(query.select(`${tableName}.*`, identities)).first()
309
+
310
+ if (!record) {
311
+ return null
312
+ }
313
+
314
+ return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
315
+ }
316
+
317
+ /** @type {OrdinaryDataReadHelpers['dataQuery']} */
318
+ const dataQuery = async ({ scopeName, context, runHooks }) => {
319
+ const storageAdapter = getScopeStorageAdapter(scopeName)
320
+ context.storageAdapter = storageAdapter
321
+ const scope = getScope(scopeName)
322
+ const tableName = storageAdapter.getTableName()
323
+ const schemaInfo = context.schemaInfo
324
+ const queryParams = context.queryParams
325
+ const db = context.db || api.knex.instance
326
+ const sortableFields = context.sortableFields
327
+
328
+ log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
329
+ log.debug(`[Knex] QUERY ${tableName}`)
330
+
331
+ const fieldSelectionInfo = await buildFieldSelection(scope, { context })
332
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
333
+
334
+ let query = db(tableName)
335
+ applyQueryConstraint({ query, context, scopeName, storageAdapter, tableName })
336
+
337
+ const selectionState = await applyFieldSelectionToQuery({
338
+ query,
339
+ scope,
340
+ fieldSelectionInfo,
341
+ tableName,
342
+ useTablePrefix: true,
343
+ storageAdapter,
344
+ db,
345
+ context,
346
+ scopeName
347
+ })
348
+ query = selectionState.query
349
+ const queryFieldRuntimeByField = selectionState.queryFieldRuntimeByField
350
+
351
+ log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
352
+
353
+ const filteredState = await withQueryFilteringContext(context, {
354
+ query,
355
+ filters: queryParams.filters,
356
+ schemaInfo,
357
+ scopeName,
358
+ tableName,
359
+ db,
360
+ queryPurpose: 'collection',
361
+ adapter: storageAdapter,
362
+ storageAdapter,
363
+ }, async () => {
364
+ await runHooks('knexQueryFiltering')
365
+ })
366
+ query = filteredState.query
367
+
368
+ log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
369
+
370
+ const sortDescriptors = await buildSortDescriptors({
371
+ query,
372
+ sort: queryParams.sort,
373
+ schemaInfo,
374
+ sortableFields,
375
+ defaultSort: scope.vars.defaultSort,
376
+ scopeName,
377
+ context,
378
+ before: Boolean(queryParams.page?.before),
379
+ storageAdapter,
380
+ queryFieldRuntimeByField
381
+ })
382
+
383
+ const paginationInfo = applyPaginationToQuery({
384
+ query,
385
+ page: queryParams.page,
386
+ vars: scope.vars,
387
+ sortDescriptors,
388
+ storageAdapter
389
+ })
390
+ const { pageSize } = paginationInfo
391
+
392
+ /** @type {StorageRow[]} */
393
+ const records = await applyDatabaseReadOptions(query)
394
+
395
+ context.returnMeta = context.returnMeta || {}
396
+ const queryString = serializeJsonApiQuery(queryParams)
397
+ context.returnMeta.queryString = queryString ? `?${queryString}` : ''
398
+
399
+ // Execute count query for pagination if offset-based pagination is used
400
+ if (paginationInfo.mode === 'offset') {
401
+ const { page } = paginationInfo
402
+
403
+ // Only execute count query if enabled
404
+ if (scope.vars.enablePaginationCounts) {
405
+ // Build count query with same filters as main query
406
+ let countQuery = db(tableName)
407
+ applyQueryConstraint({ query: countQuery, context, scopeName, storageAdapter, tableName })
408
+
409
+ // Mandatory server-side filters must also run when the client supplied no filters.
410
+ // Otherwise pagination metadata can disclose or count rows excluded from the page.
411
+ const filteredState = await withQueryFilteringContext(context, {
412
+ query: countQuery,
413
+ filters: queryParams.filters,
414
+ scopeName,
415
+ tableName,
416
+ schemaInfo,
417
+ db,
418
+ queryPurpose: 'count',
419
+ adapter: storageAdapter,
420
+ storageAdapter
421
+ }, async () => {
422
+ await runHooks('knexQueryFiltering')
423
+ })
424
+ countQuery = filteredState.query
425
+
426
+ // Get total count
427
+ const countResult = await countQuery.clearSelect().clearOrder()
428
+ .countDistinct({ total: `${tableName}.${storageAdapter.getIdColumn()}` }).first()
429
+ const total = Number(countResult?.total ?? 0)
430
+
431
+ // Calculate pagination metadata with total
432
+ context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
433
+ } else {
434
+ // Without count, we can still provide basic pagination info
435
+ context.returnMeta.paginationMeta = {
436
+ page,
437
+ pageSize
438
+ // No total, pageCount, or hasMore when counts are disabled
439
+ }
440
+ }
441
+
442
+ // Generate links
443
+ const urlPrefix = getUrlPrefix(context, scope)
444
+ context.returnMeta.paginationLinks = generatePaginationLinks(
445
+ urlPrefix,
446
+ scopeName,
447
+ queryParams,
448
+ context.returnMeta.paginationMeta
449
+ )
450
+ }
451
+
452
+ const referenceSortDescriptors = sortDescriptors.filter(descriptor => descriptor.referenceField)
453
+ if (paginationInfo.mode === 'cursor') {
454
+ // Pagination fetches one extra row to detect a following page.
455
+ const hasMore = records.length > pageSize
456
+
457
+ if (hasMore) {
458
+ records.pop()
459
+ }
460
+ if (paginationInfo.before) records.reverse()
461
+
462
+ const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
463
+ const cursorRecords = records.map(record => {
464
+ const cursorRecord = { ...record }
465
+ for (const { field, resultColumn } of sortDescriptors) {
466
+ if (resultColumn) cursorRecord[field] = record[resultColumn]
467
+ }
468
+ return cursorRecord
469
+ })
470
+ const cursorOptions = {
471
+ schemaInfo,
472
+ definitions: Object.fromEntries(sortDescriptors.map(({ field, definition }) => [field, definition])),
473
+ before: paginationInfo.before
474
+ }
475
+
476
+ context.returnMeta.paginationMeta = buildCursorMeta(cursorRecords, pageSize, hasMore, sortFields, cursorOptions)
477
+ const urlPrefix = getUrlPrefix(context, scope)
478
+ context.returnMeta.paginationLinks = generateCursorPaginationLinks(
479
+ urlPrefix,
480
+ scopeName,
481
+ queryParams,
482
+ cursorRecords,
483
+ pageSize,
484
+ hasMore,
485
+ sortFields,
486
+ cursorOptions
487
+ )
488
+ }
489
+
490
+ for (const record of records) {
491
+ for (const { resultColumn } of referenceSortDescriptors) {
492
+ if (resultColumn) delete record[resultColumn]
493
+ }
494
+ }
495
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
496
+
497
+ const included = await processIncludes(scope, records, {
498
+ log,
499
+ scopes,
500
+ knex,
501
+ context,
502
+ api
503
+ })
504
+
505
+ return buildJsonApiResponse(scope, records, included, false, scopeName, context)
506
+ }
507
+
508
+ /** @type {OrdinaryDataWriteHelpers['dataPost']} */
509
+ const dataPost = async ({ scopeName, context }) => {
510
+ const storageAdapter = getScopeStorageAdapter(scopeName)
511
+ context.storageAdapter = storageAdapter
512
+ const tableName = storageAdapter.getTableName()
513
+ const idProperty = storageAdapter.getIdColumn()
514
+ const db = context.db || api.knex.instance
515
+ const inputRecord = context.inputRecord
516
+
517
+ log.debug(`[Knex] POST ${tableName}`)
518
+
519
+ // Extract attributes from JSON:API format.
520
+ // POST may legitimately provide a resource id when the table uses a custom primary key
521
+ // (for example, one-to-one rows keyed by user_id/workspace_id). Keep that id in the
522
+ // attribute bag so storage translation can write it to the configured id column.
523
+ const attributes = {
524
+ ...(inputRecord.data.attributes || {}),
525
+ ...(inputRecord.data.id !== undefined ? { id: inputRecord.data.id } : {})
526
+ }
527
+
528
+ // Strip non-database fields (computed and virtual) before insert
529
+ /** @type {StorageRow} */
530
+ const dbAttributes = storageAdapter.toStorageRow(attributes, {
531
+ context,
532
+ operation: 'post'
533
+ })
534
+
535
+ const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
536
+
537
+ // Insert and get the new ID
538
+ const result = await applyDatabaseReadOptions(applyInsertReturning(db(tableName).insert(dbAttributes), { [idProperty]: databaseIdentityExpression(db, idProperty) }))
539
+
540
+ // Extract the ID value. Some dialects ignore .returning() for inserts and can
541
+ // yield 0 even when the caller explicitly supplied the primary key.
542
+ const returnedId = Array.isArray(result)
543
+ ? (result[0]?.[idProperty] ?? result[0])
544
+ : (result?.[idProperty] ?? result)
545
+
546
+ if (
547
+ explicitId !== undefined &&
548
+ explicitId !== null &&
549
+ (returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
550
+ ) {
551
+ return explicitId
552
+ }
553
+
554
+ return returnedId
555
+ }
556
+
557
+ /** @type {OrdinaryDataWriteHelpers['dataPut']} */
558
+ const dataPut = async ({ scopeName, context }) => {
559
+ const storageAdapter = getScopeStorageAdapter(scopeName)
560
+ context.storageAdapter = storageAdapter
561
+ const id = context.id
562
+ const tableName = storageAdapter.getTableName()
563
+ const idProperty = storageAdapter.getIdColumn()
564
+ const db = context.db || api.knex.instance
565
+ const inputRecord = context.inputRecord
566
+ const isCreate = context.isCreate
567
+
568
+ log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
569
+
570
+ // Core has already merged relationship values and applied setters.
571
+ const attributes = inputRecord.data.attributes || {}
572
+
573
+ // Strip non-database fields (computed and virtual) before database operation
574
+ const finalAttributes = storageAdapter.toStorageRow(attributes, {
575
+ context,
576
+ operation: 'put'
577
+ })
578
+
579
+ if (isCreate) {
580
+ await db(tableName).insert({
581
+ ...finalAttributes,
582
+ [idProperty]: id
583
+ })
584
+ return
585
+ }
586
+
587
+ const exists = await db(tableName)
588
+ .where(idProperty, id)
589
+ .first()
590
+
591
+ if (!exists) {
592
+ throw new RestApiResourceError(
593
+ 'Resource not found',
594
+ {
595
+ subtype: ERROR_SUBTYPES.NOT_FOUND,
596
+ resourceType: scopeName,
597
+ resourceId: id
598
+ }
599
+ )
600
+ }
601
+
602
+ // Updates cannot replace the primary key.
603
+ delete finalAttributes[idProperty]
604
+ if (Object.keys(finalAttributes).length > 0) {
605
+ await db(tableName)
606
+ .where(idProperty, id)
607
+ .update(finalAttributes)
608
+ }
609
+ }
610
+
611
+ /** @type {OrdinaryDataWriteHelpers['dataPatch']} */
612
+ const dataPatch = async ({ scopeName, context }) => {
613
+ const storageAdapter = getScopeStorageAdapter(scopeName)
614
+ context.storageAdapter = storageAdapter
615
+ const id = context.id
616
+ const tableName = storageAdapter.getTableName()
617
+ const idProperty = storageAdapter.getIdColumn()
618
+ const db = context.db || api.knex.instance
619
+ const inputRecord = context.inputRecord
620
+
621
+ log.debug(`[Knex] PATCH ${tableName}/${id}`)
622
+
623
+ // Check if record exists
624
+ const exists = await db(tableName)
625
+ .where(idProperty, id)
626
+ .first()
627
+
628
+ if (!exists) {
629
+ throw new RestApiResourceError(
630
+ 'Resource not found',
631
+ {
632
+ subtype: ERROR_SUBTYPES.NOT_FOUND,
633
+ resourceType: scopeName,
634
+ resourceId: id
635
+ }
636
+ )
637
+ }
638
+
639
+ // Core has already merged relationship values and applied setters.
640
+ const attributes = inputRecord.data.attributes || {}
641
+
642
+ // Strip non-database fields (computed and virtual) before database operation
643
+ const finalAttributes = storageAdapter.toStorageRow(attributes, {
644
+ context,
645
+ operation: 'patch'
646
+ })
647
+
648
+ log.debug(`[Knex] PATCH attributes prepared for ${tableName}`)
649
+
650
+ // Remove the idProperty from attributes to prevent updating the primary key
651
+ delete finalAttributes[idProperty]
652
+
653
+ // Update only if there are changes
654
+ if (Object.keys(finalAttributes).length > 0) {
655
+ await db(tableName)
656
+ .where(idProperty, id)
657
+ .update(finalAttributes)
658
+ }
659
+ }
660
+
661
+ /** @type {OrdinaryDataWriteHelpers['dataDelete']} */
662
+ const dataDelete = async ({ scopeName, context }) => {
663
+ const storageAdapter = getScopeStorageAdapter(scopeName)
664
+ context.storageAdapter = storageAdapter
665
+ const id = context.id
666
+
667
+ const tableName = storageAdapter.getTableName()
668
+ const idProperty = storageAdapter.getIdColumn()
669
+ const db = context.db || api.knex.instance
670
+
671
+ log.debug(`[Knex] DELETE ${tableName}/${id}`)
672
+
673
+ // Check if record exists
674
+ const exists = await db(tableName)
675
+ .where(idProperty, id)
676
+ .first()
677
+
678
+ if (!exists) {
679
+ throw new RestApiResourceError(
680
+ 'Resource not found',
681
+ {
682
+ subtype: ERROR_SUBTYPES.NOT_FOUND,
683
+ resourceType: scopeName,
684
+ resourceId: id
685
+ }
686
+ )
687
+ }
688
+
689
+ // Delete the record
690
+ await invalidateDeletedPivotTargets(api, scopeName, context)
691
+ await db(tableName)
692
+ .where(idProperty, id)
693
+ .delete()
694
+ await deletePivotReferences(api, scopeName, context)
695
+
696
+ return { success: true }
697
+ }
698
+
699
+ return {
700
+ dataRelatedIdsQuery,
701
+ dataExists,
702
+ dataGet,
703
+ dataQuery,
704
+ dataPost,
705
+ dataPut,
706
+ dataPatch,
707
+ dataDelete,
708
+ dataGetMinimal
709
+ }
710
+ }