json-rest-api 1.0.29 → 2.1.0

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