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,101 +1,56 @@
1
- import { requirePackage } from 'hooked-api'
1
+ import { createOrdinaryDataHelpers } from './lib/storage/ordinary-data-helpers.js'
2
+ import { createKnexTransaction } from '../../lib/knex-transaction.js'
3
+ import { createEnhancedLogger } from '../../lib/enhanced-logger.js'
4
+ import { throwMissingPackage } from '../../lib/missing-package.js'
2
5
  import { createSchema } from 'json-rest-schema'
3
- import {
4
- createKnexTable,
5
- addKnexFields,
6
- alterKnexFields,
7
- generateKnexMigration,
8
- generateKnexMigrationDiff
9
- } from './lib/dbTablesOperations.js'
6
+ import { createKnexTable, addKnexFields, alterKnexFields, generateKnexMigration, generateKnexMigrationDiff } from './lib/dbTablesOperations.js'
10
7
  import { introspectKnexTableSnapshot } from './lib/dbIntrospection.js'
11
- import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
12
- import { buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
13
- import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
14
- import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
15
- import { processIncludes } from './lib/querying/knex-process-includes.js'
16
- import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
17
- import {
18
- polymorphicFiltersHook,
19
- crossTableFiltersHook,
20
- basicFiltersHook
21
- } from './lib/querying/knex-query-helpers.js'
22
- import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
23
- import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
24
- import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
25
- import {
26
- calculatePaginationMeta,
27
- generatePaginationLinks,
28
- generateCursorPaginationLinks,
29
- buildCursorMeta,
30
- parseCursor
31
- } from './lib/querying/knex-pagination-helpers.js'
32
- import { getUrlPrefix } from './lib/querying/url-helpers.js'
33
- import { createStorageAdapter } from './lib/storage/storage-adapter.js'
34
- import {
35
- applyCursorPredicate,
36
- applyQueryFieldOrder,
37
- buildEffectiveSortList,
38
- parseSortEntry
39
- } from './lib/querying/query-field-sort-helpers.js'
40
- import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
41
- import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
8
+ import { polymorphicFiltersHook, crossTableFiltersHook, basicFiltersHook } from './lib/querying/knex-query-helpers.js'
9
+ import { getDatabaseCapabilities } from './lib/querying-writing/database-capabilities.js'
10
+ import { createStorageAdapterLookup } from './lib/storage/storage-adapter.js'
11
+ import { assertWritableKnexColumns } from './lib/storage/storage-mapping.js'
12
+ import { assertFieldNameMap } from './lib/querying-writing/field-utils.js'
42
13
 
43
14
  export const RestApiKnexPlugin = {
44
15
  name: 'rest-api-knex',
45
16
  dependencies: ['rest-api'],
46
17
 
47
- async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
48
- // Try to import knex dynamically
18
+ async install ({ helpers, pluginOptions, api, log, scopes, addHook, addResourceMethod }) {
19
+ log = createEnhancedLogger(log)
49
20
  try {
50
21
  await import('knex')
51
22
  } catch (e) {
52
- requirePackage('knex', 'rest-api-knex',
23
+ throwMissingPackage('knex', 'rest-api-knex',
53
24
  'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
54
25
  }
55
26
 
56
- // Get Knex instance from plugin options
57
27
  const knexOptions = pluginOptions || {}
58
28
  const knex = knexOptions.knex
59
29
 
60
- // Expose Knex instance and helpers in a structured way
61
30
  api.knex = {
62
31
  instance: knex,
63
32
  helpers: {}
64
33
  }
65
34
 
66
- const storageAdapters = new Map()
67
-
68
- const getScopeStorageAdapter = (scopeName) => {
69
- if (!scopeName) return null
70
- const resource = api.resources?.[scopeName] || scopes?.[scopeName]
71
- const schemaInfo = resource?.vars?.schemaInfo
72
- if (!schemaInfo) return null
73
-
74
- const cached = storageAdapters.get(scopeName)
75
- if (cached && cached.schemaInfo === schemaInfo) {
76
- return cached.adapter
77
- }
78
-
79
- const adapter = createStorageAdapter({ knex, schemaInfo })
80
- storageAdapters.set(scopeName, { adapter, schemaInfo })
81
- if (resource?.vars) {
82
- resource.vars.storageAdapter = adapter
83
- }
84
- return adapter
85
- }
35
+ const getScopeStorageAdapter = createStorageAdapterLookup({
36
+ knex,
37
+ getResource: scopeName => api.resources?.[scopeName] || scopes?.[scopeName]
38
+ })
86
39
 
87
40
  api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
88
41
  helpers.getStorageAdapter = getScopeStorageAdapter
89
42
 
43
+ addHook('resource:added', 'validate-knex-storage-columns', { afterFunction: 'compileResourceSchemas' }, ({ context }) => {
44
+ assertWritableKnexColumns(context.vars.schemaInfo.storageInfo)
45
+ })
46
+
90
47
  const buildScopeTableSchema = (vars = {}) => {
91
48
  const schemaStructure = vars.schemaInfo?.schemaStructure || {}
92
49
  const filteredSchema = {}
93
50
 
94
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
95
- if (!fieldDef.virtual) {
96
- filteredSchema[fieldName] = fieldDef
97
- }
98
- })
51
+ for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
52
+ if (!fieldDef.virtual) filteredSchema[fieldName] = fieldDef
53
+ }
99
54
 
100
55
  return {
101
56
  structure: filteredSchema,
@@ -106,138 +61,47 @@ export const RestApiKnexPlugin = {
106
61
  }
107
62
  }
108
63
 
109
- const buildLegacySortDescriptors = ({
110
- query,
111
- sort,
112
- schemaInfo,
113
- sortableFields,
114
- storageAdapter,
115
- defaultSort,
116
- queryFieldRuntimeByField = new Map()
117
- }) => {
118
- const effectiveSort = buildEffectiveSortList(sort, { defaultSort, idField: 'id' })
119
- const descriptors = []
120
-
121
- for (const sortEntry of effectiveSort) {
122
- const { field, direction, sqlDirection } = parseSortEntry(sortEntry)
123
-
124
- if (field !== 'id' && sortableFields?.length > 0 && !sortableFields.includes(field)) {
125
- log.warn(`Ignoring non-sortable field: ${field}`)
126
- continue
127
- }
128
-
129
- const queryFieldRuntime = queryFieldRuntimeByField.get(field)
130
- if (queryFieldRuntime) {
131
- applyQueryFieldOrder(query, queryFieldRuntime, sqlDirection)
132
- descriptors.push({
133
- field,
134
- direction: sqlDirection,
135
- queryFieldRuntime,
136
- definition: queryFieldRuntime.definition,
137
- isRelationship: false
138
- })
139
- continue
140
- }
141
-
142
- let dbField = field
143
- const searchField = schemaInfo.searchSchemaStructure?.[field]
144
- if (searchField?.actualField) {
145
- dbField = searchField.actualField
146
- }
147
-
148
- const translatedField = storageAdapter?.translateColumn
149
- ? storageAdapter.translateColumn(dbField)
150
- : dbField
151
-
152
- query.orderBy(translatedField, direction)
153
- descriptors.push({
154
- field,
155
- direction: sqlDirection,
156
- column: translatedField,
157
- definition: schemaInfo.schemaStructure?.[field] || { type: field === 'id' ? 'id' : undefined },
158
- isRelationship: Boolean(searchField?.isRelationship)
159
- })
160
- }
161
-
162
- if (descriptors.length === 0) {
163
- query.orderBy('id', 'asc')
164
- descriptors.push({
165
- field: 'id',
166
- direction: 'ASC',
167
- column: 'id',
168
- definition: { type: 'id' },
169
- isRelationship: false
170
- })
171
- }
172
-
173
- return descriptors
174
- }
175
-
176
- // Check database capabilities
177
- const hasWindowFunctions = await supportsWindowFunctions(knex)
178
- const dbInfo = await getDatabaseInfo(knex)
179
-
180
- // Store capabilities in API instance for access throughout
181
- api.knex.capabilities = {
182
- windowFunctions: hasWindowFunctions,
183
- dbInfo
184
- }
64
+ api.knex.capabilities = await getDatabaseCapabilities(knex, log)
65
+ const { dbInfo, windowFunctions } = api.knex.capabilities
185
66
 
186
67
  log.info('Database capabilities detected:', {
187
68
  database: dbInfo.client,
188
69
  version: dbInfo.version,
189
- windowFunctions: hasWindowFunctions
70
+ windowFunctions
190
71
  })
191
72
 
192
- // Cross-table search functions are now imported directly and used with full signatures
193
-
194
- /* ╔═════════════════════════════════════════════════════════════════════╗
195
- * ║ MAIN QUERY FILTERING HOOK ║
196
- * ║ This is the heart of the filtering system. It processes searchSchema ║
197
- * ║ filters and builds SQL WHERE conditions with proper JOINs ║
198
- * ╚═════════════════════════════════════════════════════════════════════╝ */
73
+ const queryHookDependencies = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
199
74
 
200
- // Register the three separate filter hooks
201
- // Dependencies object for the hooks
202
- const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
203
-
204
- // Register in specific order: polymorphic → cross-table → basic
205
- // This ensures proper field qualification when JOINs are present
206
-
207
- // 1. Polymorphic filters (adds JOINs for polymorphic relationships)
75
+ // Relationship joins must precede basic filters so column names are qualified.
208
76
  addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
209
- async (hookParams) => polymorphicFiltersHook(hookParams, polymorphicFiltersHookParams)
77
+ async (hookParams) => polymorphicFiltersHook(hookParams, queryHookDependencies)
210
78
  )
211
79
 
212
- // 2. Cross-table filters (adds JOINs for cross-table fields)
213
80
  addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
214
- async (hookParams) => crossTableFiltersHook(hookParams, polymorphicFiltersHookParams)
81
+ async (hookParams) => crossTableFiltersHook(hookParams, queryHookDependencies)
215
82
  )
216
83
 
217
- // 3. Basic filters (processes simple main table filters)
218
84
  addHook('knexQueryFiltering', 'basicFiltersHook', {},
219
- async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
85
+ async (hookParams) => basicFiltersHook(hookParams, queryHookDependencies)
220
86
  )
221
87
 
222
- // 3. Basic filters (processes simple main table filters)
223
88
  addHook('release', 'releaseHook', {},
224
89
  async ({ api }) => api.knex.instance.destroy()
225
90
  )
226
91
 
227
- // Helper scope method to get all schema-related information
228
- addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
92
+ addResourceMethod('createKnexTable', async ({ vars, scopeOptions }) => {
229
93
  const tableSchemaInstance = buildScopeTableSchema(vars)
230
94
  await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
231
95
  })
232
96
 
233
- addScopeMethod('introspectKnexTableSnapshot', async ({ vars }) => {
97
+ addResourceMethod('introspectKnexTableSnapshot', async ({ vars }) => {
234
98
  return introspectKnexTableSnapshot(api.knex.instance, {
235
99
  tableName: vars.schemaInfo.tableName,
236
100
  idColumn: vars.schemaInfo.idProperty
237
101
  })
238
102
  })
239
103
 
240
- addScopeMethod('generateKnexMigration', async ({ vars, scopeOptions }) => {
104
+ addResourceMethod('generateKnexMigration', async ({ vars, scopeOptions }) => {
241
105
  return generateKnexMigration(
242
106
  vars.schemaInfo.tableName,
243
107
  buildScopeTableSchema(vars),
@@ -249,7 +113,7 @@ export const RestApiKnexPlugin = {
249
113
  )
250
114
  })
251
115
 
252
- addScopeMethod('generateKnexMigrationDiff', async ({ vars, params }) => {
116
+ addResourceMethod('generateKnexMigrationDiff', async ({ vars, params }) => {
253
117
  const snapshot = await introspectKnexTableSnapshot(api.knex.instance, {
254
118
  tableName: vars.schemaInfo.tableName,
255
119
  idColumn: vars.schemaInfo.idProperty
@@ -267,9 +131,7 @@ export const RestApiKnexPlugin = {
267
131
  )
268
132
  })
269
133
 
270
- // Helper scope method to alter existing fields in a table
271
- addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
272
- // Validate required parameters
134
+ addResourceMethod('alterKnexFields', async ({ vars, params }) => {
273
135
  if (!params.fields || typeof params.fields !== 'object') {
274
136
  throw new Error('fields parameter is required for alterKnexFields')
275
137
  }
@@ -277,913 +139,27 @@ export const RestApiKnexPlugin = {
277
139
  await alterKnexFields(
278
140
  api.knex.instance,
279
141
  vars.schemaInfo.tableName,
280
- params.fields,
281
- { ...params.options, idProperty: vars.schemaInfo.idProperty }
142
+ { structure: params.fields },
143
+ { storage: vars.schemaInfo.storage, ...params.options, idProperty: vars.schemaInfo.idProperty }
282
144
  )
283
145
  })
284
146
 
285
- // Helper scope method to add a field to an existing table
286
- addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
287
- // Create schema object from filtered fields
147
+ addResourceMethod('addKnexFields', async ({ vars, scopeName, params }) => {
148
+ assertFieldNameMap(params.fields, `added fields in '${scopeName}'`)
288
149
  const partialTableSchema = createSchema(params.fields)
289
150
 
290
151
  await addKnexFields(
291
152
  api.knex.instance,
292
153
  vars.schemaInfo.tableName,
293
154
  partialTableSchema,
294
- { idProperty: vars.schemaInfo.idProperty }
155
+ { storage: vars.schemaInfo.storage, idProperty: vars.schemaInfo.idProperty }
295
156
  )
296
157
  })
297
158
 
298
- helpers.newTransaction = async () => {
299
- return knex.transaction()
300
- }
301
-
302
- /* ╔═════════════════════════════════════════════════════════════════════╗
303
- * ║ DATA OPERATION METHODS ║
304
- * ║ Implementation of the storage interface required by REST API plugin║
305
- * ╚═════════════════════════════════════════════════════════════════════╝ */
306
-
307
- /**
308
- * Checks if a resource exists in the database
309
- *
310
- * @param {Object} params - The parameters object
311
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
312
- * @param {Object} params.context - The context object containing request-specific data
313
- * @param {string|number} params.context.id - The ID of the resource to check for existence
314
- * @param {Object} params.context.schemaInfo - Schema information for the resource
315
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
316
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
317
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
318
- * @returns {Promise<boolean>} True if the resource exists, false otherwise
319
- */
320
- helpers.dataExists = async ({ scopeName, context }) => {
321
- const storageAdapter = getScopeStorageAdapter(scopeName)
322
- if (storageAdapter) {
323
- context.storageAdapter = storageAdapter
324
- }
325
-
326
- const id = context.id
327
-
328
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
329
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
330
- const db = context.db || api.knex.instance
331
-
332
- log.debug(`[Knex] EXISTS ${tableName}/${id}`)
333
-
334
- const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
335
-
336
- const record = await db(tableName)
337
- .where(idProperty, id)
338
- .select(selectClause)
339
- .first()
340
-
341
- return !!record
342
- }
343
-
344
- /**
345
- * Retrieves a single resource by ID with support for sparse fieldsets and includes
346
- *
347
- * @param {Object} params - The parameters object
348
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
349
- * @param {Object} params.context - The context object containing request-specific data
350
- * @param {string|number} params.context.id - The ID of the resource to retrieve
351
- * @param {Object} params.context.schemaInfo - Schema information for the resource
352
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
353
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
354
- * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
355
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
356
- * @param {Object} [params.context.queryParams] - Query parameters for sparse fieldsets and includes
357
- * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
358
- * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
359
- * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
360
- * @returns {Promise<Object>} JSON:API formatted response with data and optional included resources
361
- * @throws {RestApiResourceError} When the resource is not found
362
- */
363
- helpers.dataGet = async ({ scopeName, context, runHooks }) => {
364
- const storageAdapter = getScopeStorageAdapter(scopeName)
365
- if (storageAdapter) {
366
- context.storageAdapter = storageAdapter
367
- }
368
- const scope = api.resources[scopeName]
369
- if (!scope) {
370
- log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
371
- throw new Error(`Scope '${scopeName}' not found in api.resources`)
372
- }
373
- if (!scope.scopeName && !scope.name) {
374
- log.debug('[DATA-GET] Scope structure:', {
375
- scopeKeys: Object.keys(scope),
376
- scopeName,
377
- hasVars: !!scope.vars,
378
- varKeys: scope.vars ? Object.keys(scope.vars) : []
379
- })
380
- }
381
- const id = context.id
382
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
383
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
384
- const db = context.db || api.knex.instance
385
-
386
- log.debug(`[Knex] GET ${tableName}/${id}`)
387
-
388
- // Build field selection for sparse fieldsets
389
- // This determines which fields to SELECT from database
390
- // and tracks dependencies needed for computed fields
391
- const fieldSelectionInfo = await buildFieldSelection(
392
- scope,
393
- { context }
394
- )
395
-
396
- // Store dependency info in context for enrichAttributes
397
- // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
398
- // The REST API plugin will use this to remove 'cost' from response if not requested
399
- context.computedDependencies = fieldSelectionInfo.computedDependencies
400
-
401
- // Build query - no filtering hooks for single records
402
- // Permission checks will handle access control
403
- let query = db(tableName).where(idProperty, id)
404
-
405
- const selectionState = await applyFieldSelectionToQuery({
406
- query,
407
- scope,
408
- fieldSelectionInfo,
409
- tableName,
410
- useTablePrefix: false,
411
- storageAdapter,
412
- db,
413
- context,
414
- scopeName
415
- })
416
- query = selectionState.query
417
-
418
- const record = await query.first()
419
-
420
- if (!record) {
421
- throw new RestApiResourceError(
422
- 'Resource not found',
423
- {
424
- subtype: ERROR_SUBTYPES.NOT_FOUND,
425
- resourceType: scopeName,
426
- resourceId: id
427
- }
428
- )
429
- }
430
-
431
- // Load relationship identifiers for all hasMany relationships
432
- const records = [record] // Wrap in array for processing
433
- await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
434
-
435
- // Process includes
436
- const included = await processIncludes(scope, records, {
437
- log,
438
- scopes,
439
- knex,
440
- context
441
- })
442
-
443
- // Build and return response
444
- return buildJsonApiResponse(scope, records, included, true, scopeName, context)
445
- }
446
-
447
- /**
448
- * Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
449
- *
450
- * @param {Object} params - The parameters object
451
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
452
- * @param {Object} params.context - The context object containing request-specific data
453
- * @param {string|number} params.context.id - The ID of the resource to retrieve
454
- * @param {Object} params.context.schemaInfo - Schema information for the resource
455
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
456
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
457
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
458
- * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
459
- */
460
- helpers.dataGetMinimal = async ({
461
- scopeName,
462
- context,
463
- runHooks,
464
- applyQueryFilters,
465
- filters = context.queryParams?.filters,
466
- queryPurpose = 'single'
467
- }) => {
468
- const storageAdapter = getScopeStorageAdapter(scopeName)
469
- if (storageAdapter) {
470
- context.storageAdapter = storageAdapter
471
- }
472
- const scope = api.resources[scopeName]
473
- const id = context.id
474
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
475
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
476
- const db = context.db || api.knex.instance
477
-
478
- log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
479
-
480
- // Build query.
481
- // Single-record lookups still go through knexQueryFiltering when hooks are available,
482
- // so resource scoping plugins can treat GET/PUT/PATCH/DELETE the same way as collections.
483
- let query = db(tableName).where(idProperty, id)
484
-
485
- // Add alias if idProperty is not 'id'
486
- if (idProperty !== 'id') {
487
- query = query.select('*', `${idProperty} as id`)
488
- }
489
-
490
- if (typeof applyQueryFilters === 'function') {
491
- const scopedQueryState = await applyQueryFilters({
492
- query,
493
- filters,
494
- tableName,
495
- db,
496
- scopeName,
497
- queryPurpose,
498
- storageAdapter
499
- })
500
- query = unwrapQueryBuilderState(scopedQueryState, query)
501
- } else if (runHooks) {
502
- const previousKnexQuery = context.knexQuery
503
- context.knexQuery = {
504
- query,
505
- filters,
506
- schemaInfo: context.schemaInfo,
507
- scopeName,
508
- tableName,
509
- db,
510
- queryPurpose,
511
- adapter: storageAdapter,
512
- storageAdapter,
513
- }
514
-
515
- try {
516
- await runHooks('knexQueryFiltering')
517
- query = context.knexQuery?.query || query
518
- } finally {
519
- if (previousKnexQuery === undefined) {
520
- delete context.knexQuery
521
- } else {
522
- context.knexQuery = previousKnexQuery
523
- }
524
- }
525
- }
526
-
527
- // Execute query
528
- const record = await query.first()
529
-
530
- if (!record) {
531
- return null
532
- }
533
-
534
- // Transform to JSON:API format with belongsTo relationships
535
- return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
536
- }
537
-
538
- /**
539
- * Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
540
- *
541
- * @param {Object} params - The parameters object
542
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
543
- * @param {Object} params.context - The context object containing request-specific data
544
- * @param {Object} params.context.schemaInfo - Schema information for the resource
545
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
546
- * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
547
- * @param {Object} params.context.schemaInfo.searchSchemaInstance - Search schema for filtering capabilities
548
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
549
- * @param {Object} params.context.queryParams - Query parameters object
550
- * @param {Object} [params.context.queryParams.filters] - Filter conditions
551
- * @param {Array<string>} [params.context.queryParams.sort] - Sort fields (prefix with - for DESC)
552
- * @param {Object} [params.context.queryParams.page] - Pagination parameters
553
- * @param {number} [params.context.queryParams.page.size] - Page size
554
- * @param {number} [params.context.queryParams.page.number] - Page number (offset pagination)
555
- * @param {string} [params.context.queryParams.page.after] - Cursor for forward pagination
556
- * @param {string} [params.context.queryParams.page.before] - Cursor for backward pagination
557
- * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
558
- * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
559
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
560
- * @param {Array<string>} params.context.sortableFields - Array of fields that can be sorted
561
- * @param {Object} [params.context.knexQuery] - Temporarily set during hooks for query building
562
- * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
563
- * @param {Function} params.runHooks - Function to run hooks (e.g., 'knexQueryFiltering')
564
- * @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
565
- */
566
- helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
567
- const storageAdapter = getScopeStorageAdapter(scopeName)
568
- if (storageAdapter) {
569
- context.storageAdapter = storageAdapter
570
- }
571
- const scope = api.resources[scopeName]
572
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
573
- const schemaInfo = context.schemaInfo
574
- const queryParams = context.queryParams
575
- const db = context.db || api.knex.instance
576
- const sortableFields = context.sortableFields
577
-
578
- log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
579
- log.debug(`[Knex] QUERY ${tableName}`, queryParams)
580
-
581
- // Build field selection for sparse fieldsets
582
- // This determines which fields to SELECT from database
583
- // and tracks dependencies needed for computed fields
584
- const fieldSelectionInfo = await buildFieldSelection(
585
- scope,
586
- { context }
587
- )
588
-
589
- // Store dependency info in context for enrichAttributes
590
- // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
591
- // The REST API plugin will use this to remove 'cost' from response if not requested
592
- context.computedDependencies = fieldSelectionInfo.computedDependencies
593
-
594
- // Start building query with table prefix (for JOIN support)
595
- let query = db(tableName)
596
-
597
- const selectionState = await applyFieldSelectionToQuery({
598
- query,
599
- scope,
600
- fieldSelectionInfo,
601
- tableName,
602
- useTablePrefix: true,
603
- storageAdapter,
604
- db,
605
- context,
606
- scopeName
607
- })
608
- query = selectionState.query
609
- const queryFieldRuntimeByField = selectionState.queryFieldRuntimeByField
610
-
611
- /* ═══════════════════════════════════════════════════════════════════
612
- * FILTERING HOOKS
613
- * This is where the magic happens. The knexQueryFiltering hook is called
614
- * to apply all filter conditions. The searchSchemaFilter hook (registered
615
- * above) will process the searchSchema and apply filters with JOINs.
616
- *
617
- * IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
618
- * to ensure proper grouping and prevent accidental filter bypass.
619
- * ═══════════════════════════════════════════════════════════════════ */
620
-
621
- log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
622
-
623
- log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
624
-
625
- log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
626
-
627
- // YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
628
-
629
- // Store the query data in context where hooks can access it
630
- // This is the proper way to share data between methods and hooks
631
- if (context) {
632
- context.knexQuery = {
633
- query,
634
- filters: queryParams.filters,
635
- schemaInfo,
636
- scopeName,
637
- tableName,
638
- db,
639
- queryPurpose: 'collection',
640
- adapter: storageAdapter,
641
- storageAdapter,
642
- }
159
+ /** @type {import('../../lib/transaction-types.js').TransactionFactory} */
160
+ helpers.newTransaction = context => createKnexTransaction(knex, context)
643
161
 
644
- log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
645
- }
646
-
647
- await runHooks('knexQueryFiltering')
648
-
649
- // Clean up after hook execution
650
- if (context && context.knexQuery) {
651
- delete context.knexQuery
652
- }
653
-
654
- log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
655
-
656
- const sortDescriptors = buildLegacySortDescriptors({
657
- query,
658
- sort: queryParams.sort,
659
- schemaInfo,
660
- sortableFields,
661
- defaultSort: scope.vars.defaultSort,
662
- storageAdapter,
663
- queryFieldRuntimeByField
664
- })
665
-
666
- // Apply pagination
667
- // Check if page object has any actual pagination parameters
668
- const hasPageParams = queryParams.page &&
669
- (queryParams.page.size !== undefined ||
670
- queryParams.page.number !== undefined ||
671
- queryParams.page.after !== undefined ||
672
- queryParams.page.before !== undefined)
673
-
674
- if (hasPageParams) {
675
- const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
676
- const pageSize = Math.min(
677
- requestedSize,
678
- scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
679
- )
680
-
681
- // Validate page size
682
- if (requestedSize <= 0) {
683
- throw new RestApiValidationError(
684
- 'Page size must be greater than 0',
685
- {
686
- fields: ['page.size'],
687
- violations: [{
688
- field: 'page.size',
689
- rule: 'min_value',
690
- message: 'Page size must be a positive number'
691
- }]
692
- }
693
- )
694
- }
695
-
696
- // Offset-based pagination
697
- if (queryParams.page.number !== undefined) {
698
- const pageNumber = queryParams.page.number || 1
699
- query
700
- .limit(pageSize)
701
- .offset((pageNumber - 1) * pageSize)
702
- } else if (queryParams.page.after || queryParams.page.before) {
703
- // Fetch one extra record to determine if there are more
704
- query.limit(pageSize + 1)
705
-
706
- if (queryParams.page.after) {
707
- let cursorData
708
- try {
709
- cursorData = parseCursor(queryParams.page.after)
710
- } catch (error) {
711
- throw new RestApiValidationError(
712
- 'Invalid cursor format in page[after] parameter',
713
- {
714
- fields: ['page.after'],
715
- violations: [{
716
- field: 'page.after',
717
- rule: 'invalid_cursor',
718
- message: 'The cursor value is not valid'
719
- }]
720
- }
721
- )
722
- }
723
-
724
- applyCursorPredicate(
725
- query,
726
- sortDescriptors,
727
- cursorData,
728
- (direction) => (direction === 'DESC' ? '<' : '>'),
729
- (builder, descriptor, operator, value) => builder.where(
730
- descriptor.column,
731
- operator,
732
- storageAdapter?.translateFilterValue
733
- ? storageAdapter.translateFilterValue(descriptor.field, value)
734
- : value
735
- ),
736
- {
737
- onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
738
- }
739
- )
740
- } else if (queryParams.page.before) {
741
- let cursorData
742
- try {
743
- cursorData = parseCursor(queryParams.page.before)
744
- } catch (error) {
745
- throw new RestApiValidationError(
746
- 'Invalid cursor format in page[before] parameter',
747
- {
748
- fields: ['page.before'],
749
- violations: [{
750
- field: 'page.before',
751
- rule: 'invalid_cursor',
752
- message: 'The cursor value is not valid'
753
- }]
754
- }
755
- )
756
- }
757
-
758
- applyCursorPredicate(
759
- query,
760
- sortDescriptors,
761
- cursorData,
762
- (direction) => (direction === 'DESC' ? '>' : '<'),
763
- (builder, descriptor, operator, value) => builder.where(
764
- descriptor.column,
765
- operator,
766
- storageAdapter?.translateFilterValue
767
- ? storageAdapter.translateFilterValue(descriptor.field, value)
768
- : value
769
- ),
770
- {
771
- onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
772
- }
773
- )
774
- }
775
- } else {
776
- // Fetch one extra to detect hasMore
777
- query.limit(pageSize + 1)
778
- }
779
- } else {
780
- // No pagination params provided - apply default limit
781
- const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
782
- query.limit(defaultLimit)
783
- }
784
-
785
- // Execute query
786
- const records = await query
787
-
788
- // Initialize returnMeta namespace for thread-safe metadata
789
- context.returnMeta = context.returnMeta || {}
790
- const queryString = serializeJsonApiQuery(queryParams)
791
- context.returnMeta.queryString = queryString ? `?${queryString}` : ''
792
-
793
- // Execute count query for pagination if offset-based pagination is used
794
- if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
795
- const page = parseInt(queryParams.page?.number) || 1
796
- const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
797
-
798
- // Only execute count query if enabled
799
- if (scope.vars.enablePaginationCounts) {
800
- // Build count query with same filters as main query
801
- let countQuery = db(tableName)
802
-
803
- // Mandatory server-side filters must also run when the client supplied no filters.
804
- // Otherwise pagination metadata can disclose or count rows excluded from the page.
805
- const previousKnexQuery = context.knexQuery
806
- context.knexQuery = {
807
- query: countQuery,
808
- filters: queryParams.filters,
809
- scopeName,
810
- tableName,
811
- schemaInfo,
812
- db,
813
- queryPurpose: 'count',
814
- adapter: storageAdapter,
815
- storageAdapter
816
- }
817
-
818
- try {
819
- await runHooks('knexQueryFiltering')
820
- countQuery = context.knexQuery?.query || countQuery
821
- } finally {
822
- if (previousKnexQuery === undefined) {
823
- delete context.knexQuery
824
- } else {
825
- context.knexQuery = previousKnexQuery
826
- }
827
- }
828
-
829
- // Get total count
830
- const countResult = await countQuery.count('* as total').first()
831
- const total = parseInt(countResult.total)
832
-
833
- // Calculate pagination metadata with total
834
- context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
835
- } else {
836
- // Without count, we can still provide basic pagination info
837
- context.returnMeta.paginationMeta = {
838
- page,
839
- pageSize
840
- // No total, pageCount, or hasMore when counts are disabled
841
- }
842
- }
843
-
844
- // Generate links
845
- const urlPrefix = getUrlPrefix(context, scope)
846
- context.returnMeta.paginationLinks = generatePaginationLinks(
847
- urlPrefix,
848
- scopeName,
849
- queryParams,
850
- context.returnMeta.paginationMeta
851
- )
852
- }
853
-
854
- // Handle cursor-based pagination meta
855
- // Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
856
- if (queryParams.page?.after || queryParams.page?.before ||
857
- (queryParams.page?.size && queryParams.page?.number === undefined)) {
858
- const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
859
-
860
- // Check if there are more records
861
- // We fetched pageSize + 1 records to detect if there are more
862
- const hasMore = records.length > pageSize
863
-
864
- // Remove the extra record if present
865
- if (hasMore) {
866
- records.pop()
867
- }
868
-
869
- const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
870
-
871
- context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields, {
872
- schemaInfo
873
- })
874
- const urlPrefix = getUrlPrefix(context, scope)
875
- context.returnMeta.paginationLinks = generateCursorPaginationLinks(
876
- urlPrefix,
877
- scopeName,
878
- queryParams,
879
- records,
880
- pageSize,
881
- hasMore,
882
- sortFields,
883
- {
884
- schemaInfo
885
- }
886
- )
887
- }
888
-
889
- // Load relationship identifiers for all hasMany relationships
890
- await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
891
-
892
- // Process includes
893
- const included = await processIncludes(scope, records, {
894
- log,
895
- scopes,
896
- knex,
897
- context,
898
- api
899
- })
900
-
901
- // Build and return response
902
- return buildJsonApiResponse(scope, records, included, false, scopeName, context)
903
- }
904
-
905
- /**
906
- * Creates a new resource in the database
907
- *
908
- * @param {Object} params - The parameters object
909
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
910
- * @param {Object} params.context - The context object containing request-specific data
911
- * @param {Object} params.context.schemaInfo - Schema information for the resource
912
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
913
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
914
- * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
915
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
916
- * @param {Object} params.context.inputRecord - JSON:API formatted input record
917
- * @param {Object} params.context.inputRecord.data - The resource data
918
- * @param {Object} params.context.inputRecord.data.attributes - The resource attributes to insert
919
- * @returns {Promise<string|number>} The ID of the newly created resource
920
- */
921
- helpers.dataPost = async ({ scopeName, context }) => {
922
- const storageAdapter = getScopeStorageAdapter(scopeName)
923
- if (storageAdapter) {
924
- context.storageAdapter = storageAdapter
925
- }
926
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
927
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
928
- const db = context.db || api.knex.instance
929
- const inputRecord = context.inputRecord
930
-
931
- log.debug(`[Knex] POST ${tableName}`, inputRecord)
932
-
933
- // Extract attributes from JSON:API format.
934
- // POST may legitimately provide a resource id when the table uses a custom primary key
935
- // (for example, one-to-one rows keyed by user_id/workspace_id). Keep that id in the
936
- // attribute bag so storage translation can write it to the configured id column.
937
- const attributes = {
938
- ...(inputRecord.data.attributes || {}),
939
- ...(inputRecord.data.id !== undefined ? { id: inputRecord.data.id } : {})
940
- }
941
-
942
- // Strip non-database fields (computed and virtual) before insert
943
- const dbAttributes = storageAdapter?.toStorageRow
944
- ? storageAdapter.toStorageRow(attributes, {
945
- context,
946
- operation: 'post'
947
- })
948
- : attributes
949
-
950
- const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
951
-
952
- // Insert and get the new ID
953
- const result = await db(tableName).insert(dbAttributes).returning(idProperty)
954
-
955
- // Extract the ID value. Some dialects ignore .returning() for inserts and can
956
- // yield 0 even when the caller explicitly supplied the primary key.
957
- const returnedId = Array.isArray(result)
958
- ? (result[0]?.[idProperty] ?? result[0])
959
- : (result?.[idProperty] ?? result)
960
-
961
- if (
962
- explicitId !== undefined &&
963
- explicitId !== null &&
964
- (returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
965
- ) {
966
- return explicitId
967
- }
968
-
969
- return returnedId
970
- }
971
-
972
- /**
973
- * Replaces an entire resource (PUT operation) or creates it with a specific ID
974
- *
975
- * @param {Object} params - The parameters object
976
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
977
- * @param {Object} params.context - The context object containing request-specific data
978
- * @param {string|number} params.context.id - The ID of the resource to replace or create
979
- * @param {Object} params.context.schemaInfo - Schema information for the resource
980
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
981
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
982
- * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
983
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
984
- * @param {Object} params.context.inputRecord - JSON:API formatted input record
985
- * @param {Object} params.context.inputRecord.data - The resource data
986
- * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes
987
- * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
988
- * @param {boolean} params.context.isCreate - Whether this is a create operation (true) or update (false)
989
- * @returns {Promise<void>} Resolves when the operation is complete
990
- * @throws {RestApiResourceError} When updating and the resource is not found
991
- */
992
- helpers.dataPut = async ({ scopeName, context }) => {
993
- const storageAdapter = getScopeStorageAdapter(scopeName)
994
- if (storageAdapter) {
995
- context.storageAdapter = storageAdapter
996
- }
997
- const scope = api.resources[scopeName]
998
- const id = context.id
999
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1000
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1001
- const db = context.db || api.knex.instance
1002
- const inputRecord = context.inputRecord
1003
- const isCreate = context.isCreate
1004
-
1005
- log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
1006
-
1007
- // Extract attributes and process relationships using helper
1008
- const attributes = inputRecord.data.attributes || {}
1009
- const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
1010
- const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
1011
-
1012
- // Strip non-database fields (computed and virtual) before database operation
1013
- const finalAttributes = storageAdapter?.toStorageRow
1014
- ? storageAdapter.toStorageRow(mergedAttributes, {
1015
- context,
1016
- operation: 'put'
1017
- })
1018
- : mergedAttributes
1019
-
1020
- // Map 'id' to actual idProperty if needed (for PUT with specific ID)
1021
- if (idProperty !== 'id' && inputRecord.data.id) {
1022
- finalAttributes[idProperty] = inputRecord.data.id
1023
- }
1024
-
1025
- if (isCreate) {
1026
- // Create mode - insert new record with specified ID
1027
- const recordData = {
1028
- ...finalAttributes,
1029
- [idProperty]: id
1030
- }
1031
-
1032
- await db(tableName).insert(recordData)
1033
- } else {
1034
- // Update mode - check if record exists first
1035
- const exists = await db(tableName)
1036
- .where(idProperty, id)
1037
- .first()
1038
-
1039
- if (!exists) {
1040
- throw new RestApiResourceError(
1041
- 'Resource not found',
1042
- {
1043
- subtype: ERROR_SUBTYPES.NOT_FOUND,
1044
- resourceType: scopeName,
1045
- resourceId: id
1046
- }
1047
- )
1048
- }
1049
-
1050
- // Remove the idProperty from attributes to prevent updating the primary key
1051
- delete finalAttributes[idProperty]
1052
-
1053
- // Update the record (replace all fields)
1054
- if (Object.keys(finalAttributes).length > 0) {
1055
- await db(tableName)
1056
- .where(idProperty, id)
1057
- .update(finalAttributes)
1058
- }
1059
- }
1060
- }
1061
-
1062
- /**
1063
- * Partially updates a resource (PATCH operation)
1064
- *
1065
- * @param {Object} params - The parameters object
1066
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1067
- * @param {Object} params.context - The context object containing request-specific data
1068
- * @param {string|number} params.context.id - The ID of the resource to update
1069
- * @param {Object} params.context.schemaInfo - Schema information for the resource
1070
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
1071
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
1072
- * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
1073
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
1074
- * @param {Object} params.context.inputRecord - JSON:API formatted input record with partial updates
1075
- * @param {Object} params.context.inputRecord.data - The resource data
1076
- * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes to update
1077
- * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
1078
- * @returns {Promise<void>} Resolves when the update is complete
1079
- * @throws {RestApiResourceError} When the resource is not found
1080
- */
1081
- helpers.dataPatch = async ({ scopeName, context }) => {
1082
- const storageAdapter = getScopeStorageAdapter(scopeName)
1083
- if (storageAdapter) {
1084
- context.storageAdapter = storageAdapter
1085
- }
1086
- const scope = api.resources[scopeName]
1087
- const id = context.id
1088
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1089
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1090
- const db = context.db || api.knex.instance
1091
- const inputRecord = context.inputRecord
1092
-
1093
- log.debug(`[Knex] PATCH ${tableName}/${id}`)
1094
-
1095
- // Check if record exists
1096
- const exists = await db(tableName)
1097
- .where(idProperty, id)
1098
- .first()
1099
-
1100
- if (!exists) {
1101
- throw new RestApiResourceError(
1102
- 'Resource not found',
1103
- {
1104
- subtype: ERROR_SUBTYPES.NOT_FOUND,
1105
- resourceType: scopeName,
1106
- resourceId: id
1107
- }
1108
- )
1109
- }
1110
-
1111
- // Extract attributes and process relationships using helper
1112
- const attributes = inputRecord.data.attributes || {}
1113
- const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
1114
- const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
1115
-
1116
- // Strip non-database fields (computed and virtual) before database operation
1117
- const finalAttributes = storageAdapter?.toStorageRow
1118
- ? storageAdapter.toStorageRow(mergedAttributes, {
1119
- context,
1120
- operation: 'patch'
1121
- })
1122
- : mergedAttributes
1123
-
1124
- log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
1125
-
1126
- // Remove the idProperty from attributes to prevent updating the primary key
1127
- delete finalAttributes[idProperty]
1128
-
1129
- // Update only if there are changes
1130
- if (Object.keys(finalAttributes).length > 0) {
1131
- await db(tableName)
1132
- .where(idProperty, id)
1133
- .update(finalAttributes)
1134
- }
1135
- }
1136
-
1137
- /**
1138
- * Deletes a resource from the database
1139
- *
1140
- * @param {Object} params - The parameters object
1141
- * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1142
- * @param {Object} params.context - The context object containing request-specific data
1143
- * @param {string|number} params.context.id - The ID of the resource to delete
1144
- * @param {Object} params.context.schemaInfo - Schema information for the resource
1145
- * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
1146
- * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
1147
- * @param {Object} params.context.db - Database connection (knex instance or transaction)
1148
- * @returns {Promise<Object>} Returns { success: true } when deletion is successful
1149
- * @throws {RestApiResourceError} When the resource is not found
1150
- */
1151
- helpers.dataDelete = async ({ scopeName, context }) => {
1152
- const storageAdapter = getScopeStorageAdapter(scopeName)
1153
- if (storageAdapter) {
1154
- context.storageAdapter = storageAdapter
1155
- }
1156
- const id = context.id
1157
-
1158
- const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1159
- const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1160
- const db = context.db || api.knex.instance
1161
-
1162
- log.debug(`[Knex] DELETE ${tableName}/${id}`)
1163
-
1164
- // Check if record exists
1165
- const exists = await db(tableName)
1166
- .where(idProperty, id)
1167
- .first()
1168
-
1169
- if (!exists) {
1170
- throw new RestApiResourceError(
1171
- 'Resource not found',
1172
- {
1173
- subtype: ERROR_SUBTYPES.NOT_FOUND,
1174
- resourceType: scopeName,
1175
- resourceId: id
1176
- }
1177
- )
1178
- }
1179
-
1180
- // Delete the record
1181
- await db(tableName)
1182
- .where(idProperty, id)
1183
- .delete()
1184
-
1185
- return { success: true }
1186
- }
162
+ Object.assign(helpers, createOrdinaryDataHelpers({ api, scopes, knex, log, getScopeStorageAdapter }))
1187
163
 
1188
164
  log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
1189
165
  }