json-rest-api 1.0.22 → 1.0.23

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 (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -1,1135 +0,0 @@
1
- 1 import { requirePackage } from 'hooked-api'
2
- 2 import { createSchema } from 'json-rest-schema'
3
- 3 import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
4
- 4 import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
5
- 5 import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
6
- 6 import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
7
- 7 import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
8
- 8 import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
9
- 9 import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
10
- 10 import { processIncludes } from './lib/querying/knex-process-includes.js'
11
- 11 import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
12
- 12 import {
13
- 13 polymorphicFiltersHook,
14
- 14 crossTableFiltersHook,
15
- 15 basicFiltersHook
16
- 16 } from './lib/querying/knex-query-helpers.js'
17
- 17 import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
18
- 18 import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
19
- 19 import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
20
- 20 import {
21
- 21 calculatePaginationMeta,
22
- 22 generatePaginationLinks,
23
- 23 generateCursorPaginationLinks,
24
- 24 buildCursorMeta,
25
- 25 parseCursor
26
- 26 } from './lib/querying/knex-pagination-helpers.js'
27
- 27 import { getUrlPrefix } from './lib/querying/url-helpers.js'
28
- 28 import { createStorageAdapter } from './lib/storage/storage-adapter.js'
29
- 29
30
- 30 /**
31
- 31 * Strips non-database fields (computed and virtual) from attributes before database operations
32
- 32 * @param {Object} attributes - The attributes object
33
- 33 * @param {Object} schemaInfo - The schema info containing computed field definitions and schema structure
34
- 34 * @returns {Object} Attributes with computed and virtual fields removed
35
- 35 */
36
- 36 const stripNonDatabaseFields = (attributes, schemaInfo) => {
37
- 37 if (!attributes || !schemaInfo) return attributes || {}
38
- 38
39
- 39 const { computed = {}, schemaStructure = {} } = schemaInfo
40
- 40 return Object.entries(attributes)
41
- 41 .filter(([key]) => {
42
- 42 // Remove computed fields
43
- 43 if (key in computed) return false
44
- 44 // Remove fields marked as virtual in the schema
45
- 45 const fieldDef = schemaStructure[key]
46
- 46 if (fieldDef && fieldDef.virtual === true) {
47
- 47 return false
48
- 48 }
49
- 49 return true
50
- 50 })
51
- 51 .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
52
- 52 }
53
- 53
54
- 54 export const RestApiKnexPlugin = {
55
- 55 name: 'rest-api-knex',
56
- 56 dependencies: ['rest-api'],
57
- 57
58
- 58 async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
59
- 59 // Try to import knex dynamically
60
- 60 let knexLib
61
- 61 try {
62
- 62 knexLib = await import('knex')
63
- 63 } catch (e) {
64
- 64 requirePackage('knex', 'rest-api-knex',
65
- 65 'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
66
- 66 }
67
- 67
68
- 68 // Get Knex instance from plugin options
69
- 69 const knexOptions = pluginOptions || {}
70
- 70 const knex = knexOptions.knex
71
- 71
72
- 72 // Expose Knex instance and helpers in a structured way
73
- 73 api.knex = {
74
- 74 instance: knex,
75
- 75 helpers: {}
76
- 76 }
77
- 77
78
- 78 const storageAdapters = new Map()
79
- 79
80
- 80 const getScopeStorageAdapter = (scopeName) => {
81
- 81 if (!scopeName) return null
82
- 82 const resource = api.resources?.[scopeName] || scopes?.[scopeName]
83
- 83 const schemaInfo = resource?.vars?.schemaInfo
84
- 84 if (!schemaInfo) return null
85
- 85
86
- 86 const cached = storageAdapters.get(scopeName)
87
- 87 if (cached && cached.schemaInfo === schemaInfo) {
88
- 88 return cached.adapter
89
- 89 }
90
- 90
91
- 91 const adapter = createStorageAdapter({ knex, schemaInfo })
92
- 92 storageAdapters.set(scopeName, { adapter, schemaInfo })
93
- 93 if (resource?.vars) {
94
- 94 resource.vars.storageAdapter = adapter
95
- 95 }
96
- 96 return adapter
97
- 97 }
98
- 98
99
- 99 api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
100
- 100 helpers.getStorageAdapter = getScopeStorageAdapter
101
- 101
102
- 102 const createSelectTranslator = (adapter) => {
103
- 103 if (!adapter) return null
104
- 104 return (field, alias) => {
105
- 105 if (field === '*') {
106
- 106 return alias ? `${alias}.*` : '*'
107
- 107 }
108
- 108 const translated = adapter.translateColumn(field)
109
- 109 const result = translated || field
110
- 110 if (!alias) {
111
- 111 return result
112
- 112 }
113
- 113 if (result.includes('.')) {
114
- 114 return result
115
- 115 }
116
- 116 return `${alias}.${result}`
117
- 117 }
118
- 118 }
119
- 119
120
- 120 // Check database capabilities
121
- 121 const hasWindowFunctions = await supportsWindowFunctions(knex)
122
- 122 const dbInfo = await getDatabaseInfo(knex)
123
- 123
124
- 124 // Store capabilities in API instance for access throughout
125
- 125 api.knex.capabilities = {
126
- 126 windowFunctions: hasWindowFunctions,
127
- 127 dbInfo
128
- 128 }
129
- 129
130
- 130 log.info('Database capabilities detected:', {
131
- 131 database: dbInfo.client,
132
- 132 version: dbInfo.version,
133
- 133 windowFunctions: hasWindowFunctions
134
- 134 })
135
- 135
136
- 136 // Cross-table search functions are now imported directly and used with full signatures
137
- 137
138
- 138 /* ╔═════════════════════════════════════════════════════════════════════╗
139
- 139 * ║ MAIN QUERY FILTERING HOOK ║
140
- 140 * ║ This is the heart of the filtering system. It processes searchSchema ║
141
- 141 * ║ filters and builds SQL WHERE conditions with proper JOINs ║
142
- 142 * ╚═════════════════════════════════════════════════════════════════════╝ */
143
- 143
144
- 144 // Register the three separate filter hooks
145
- 145 // Dependencies object for the hooks
146
- 146 const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
147
- 147
148
- 148 // Register in specific order: polymorphic → cross-table → basic
149
- 149 // This ensures proper field qualification when JOINs are present
150
- 150
151
- 151 // 1. Polymorphic filters (adds JOINs for polymorphic relationships)
152
- 152 addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
153
- 153 async (hookParams) => polymorphicFiltersHook(hookParams, polymorphicFiltersHookParams)
154
- 154 )
155
- 155
156
- 156 // 2. Cross-table filters (adds JOINs for cross-table fields)
157
- 157 addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
158
- 158 async (hookParams) => crossTableFiltersHook(hookParams, polymorphicFiltersHookParams)
159
- 159 )
160
- 160
161
- 161 // 3. Basic filters (processes simple main table filters)
162
- 162 addHook('knexQueryFiltering', 'basicFiltersHook', {},
163
- 163 async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
164
- 164 )
165
- 165
166
- 166 // 3. Basic filters (processes simple main table filters)
167
- 167 addHook('release', 'releaseHook', {},
168
- 168 async ({ api }) => api.knex.instance.destroy()
169
- 169 )
170
- 170
171
- 171 // Helper scope method to get all schema-related information
172
- 172 addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
173
- 173 // Create a filtered schema that excludes virtual fields
174
- 174 const schemaStructure = vars.schemaInfo.schemaStructure || {}
175
- 175 const filteredSchema = {}
176
- 176
177
- 177 // Copy only non-virtual fields to the filtered schema
178
- 178 Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
179
- 179 if (!fieldDef.virtual) {
180
- 180 filteredSchema[fieldName] = fieldDef
181
- 181 }
182
- 182 })
183
- 183
184
- 184 // Create schema object from filtered fields
185
- 185 const tableSchemaInstance = createSchema(filteredSchema)
186
- 186
187
- 187 await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
188
- 188 })
189
- 189
190
- 190 // Helper scope method to alter existing fields in a table
191
- 191 addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
192
- 192 // Validate required parameters
193
- 193 if (!params.fields || typeof params.fields !== 'object') {
194
- 194 throw new Error('fields parameter is required for alterKnexFields')
195
- 195 }
196
- 196
197
- 197 await alterKnexFields(
198
- 198 api.knex.instance,
199
- 199 vars.schemaInfo.tableName,
200
- 200 params.fields,
201
- 201 params.options // Pass through any additional options
202
- 202 )
203
- 203 })
204
- 204
205
- 205 // Helper scope method to add a field to an existing table
206
- 206 addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
207
- 207 // Create schema object from filtered fields
208
- 208 const partialTableSchema = createSchema(params.fields)
209
- 209
210
- 210 await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
211
- 211 })
212
- 212
213
- 213 helpers.newTransaction = async () => {
214
- 214 return knex.transaction()
215
- 215 }
216
- 216
217
- 217 /* ╔═════════════════════════════════════════════════════════════════════╗
218
- 218 * ║ DATA OPERATION METHODS ║
219
- 219 * ║ Implementation of the storage interface required by REST API plugin║
220
- 220 * ╚═════════════════════════════════════════════════════════════════════╝ */
221
- 221
222
- 222 /**
223
- 223 * Checks if a resource exists in the database
224
- 224 *
225
- 225 * @param {Object} params - The parameters object
226
- 226 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
227
- 227 * @param {Object} params.context - The context object containing request-specific data
228
- 228 * @param {string|number} params.context.id - The ID of the resource to check for existence
229
- 229 * @param {Object} params.context.schemaInfo - Schema information for the resource
230
- 230 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
231
- 231 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
232
- 232 * @param {Object} params.context.db - Database connection (knex instance or transaction)
233
- 233 * @returns {Promise<boolean>} True if the resource exists, false otherwise
234
- 234 */
235
- 235 helpers.dataExists = async ({ scopeName, context }) => {
236
- 236 const storageAdapter = getScopeStorageAdapter(scopeName)
237
- 237 if (storageAdapter) {
238
- 238 context.storageAdapter = storageAdapter
239
- 239 }
240
- 240
241
- 241 const id = context.id
242
- 242
243
- 243 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
244
- 244 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
245
- 245 const db = context.db || api.knex.instance
246
- 246
247
- 247 log.debug(`[Knex] EXISTS ${tableName}/${id}`)
248
- 248
249
- 249 const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
250
- 250
251
- 251 const record = await db(tableName)
252
- 252 .where(idProperty, id)
253
- 253 .select(selectClause)
254
- 254 .first()
255
- 255
256
- 256 return !!record
257
- 257 }
258
- 258
259
- 259 /**
260
- 260 * Retrieves a single resource by ID with support for sparse fieldsets and includes
261
- 261 *
262
- 262 * @param {Object} params - The parameters object
263
- 263 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
264
- 264 * @param {Object} params.context - The context object containing request-specific data
265
- 265 * @param {string|number} params.context.id - The ID of the resource to retrieve
266
- 266 * @param {Object} params.context.schemaInfo - Schema information for the resource
267
- 267 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
268
- 268 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
269
- 269 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
270
- 270 * @param {Object} params.context.db - Database connection (knex instance or transaction)
271
- 271 * @param {Object} [params.context.queryParams] - Query parameters for sparse fieldsets and includes
272
- 272 * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
273
- 273 * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
274
- 274 * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
275
- 275 * @returns {Promise<Object>} JSON:API formatted response with data and optional included resources
276
- 276 * @throws {RestApiResourceError} When the resource is not found
277
- 277 */
278
- 278 helpers.dataGet = async ({ scopeName, context, runHooks }) => {
279
- 279 const storageAdapter = getScopeStorageAdapter(scopeName)
280
- 280 if (storageAdapter) {
281
- 281 context.storageAdapter = storageAdapter
282
- 282 }
283
- 283 const scope = api.resources[scopeName]
284
- 284 if (!scope) {
285
- 285 log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
286
- 286 throw new Error(`Scope '${scopeName}' not found in api.resources`)
287
- 287 }
288
- 288 if (!scope.scopeName && !scope.name) {
289
- 289 log.debug('[DATA-GET] Scope structure:', {
290
- 290 scopeKeys: Object.keys(scope),
291
- 291 scopeName,
292
- 292 hasVars: !!scope.vars,
293
- 293 varKeys: scope.vars ? Object.keys(scope.vars) : []
294
- 294 })
295
- 295 }
296
- 296 const id = context.id
297
- 297 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
298
- 298 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
299
- 299 const db = context.db || api.knex.instance
300
- 300
301
- 301 log.debug(`[Knex] GET ${tableName}/${id}`)
302
- 302
303
- 303 // Build field selection for sparse fieldsets
304
- 304 // This determines which fields to SELECT from database
305
- 305 // and tracks dependencies needed for computed fields
306
- 306 const fieldSelectionInfo = await buildFieldSelection(
307
- 307 scope,
308
- 308 { context }
309
- 309 )
310
- 310
311
- 311 // Store dependency info in context for enrichAttributes
312
- 312 // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
313
- 313 // The REST API plugin will use this to remove 'cost' from response if not requested
314
- 314 context.computedDependencies = fieldSelectionInfo.computedDependencies
315
- 315
316
- 316 // Build query - no filtering hooks for single records
317
- 317 // Permission checks will handle access control
318
- 318 let query = db(tableName).where(idProperty, id)
319
- 319
320
- 320 // Apply field selection
321
- 321 const selectTranslator = createSelectTranslator(storageAdapter)
322
- 322 query = buildQuerySelection(
323
- 323 query,
324
- 324 tableName,
325
- 325 fieldSelectionInfo.fieldsToSelect,
326
- 326 false,
327
- 327 selectTranslator ? { translateColumn: selectTranslator } : undefined
328
- 328 )
329
- 329
330
- 330 const record = await query.first()
331
- 331
332
- 332 if (!record) {
333
- 333 throw new RestApiResourceError(
334
- 334 'Resource not found',
335
- 335 {
336
- 336 subtype: ERROR_SUBTYPES.NOT_FOUND,
337
- 337 resourceType: scopeName,
338
- 338 resourceId: id
339
- 339 }
340
- 340 )
341
- 341 }
342
- 342
343
- 343 // Load relationship identifiers for all hasMany relationships
344
- 344 const records = [record] // Wrap in array for processing
345
- 345 await loadRelationshipIdentifiers(records, scopeName, scopes, db)
346
- 346
347
- 347 // Process includes
348
- 348 const included = await processIncludes(scope, records, {
349
- 349 log,
350
- 350 scopes,
351
- 351 knex,
352
- 352 context
353
- 353 })
354
- 354
355
- 355 // Build and return response
356
- 356 return buildJsonApiResponse(scope, records, included, true, scopeName, context)
357
- 357 }
358
- 358
359
- 359 /**
360
- 360 * Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
361
- 361 *
362
- 362 * @param {Object} params - The parameters object
363
- 363 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
364
- 364 * @param {Object} params.context - The context object containing request-specific data
365
- 365 * @param {string|number} params.context.id - The ID of the resource to retrieve
366
- 366 * @param {Object} params.context.schemaInfo - Schema information for the resource
367
- 367 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
368
- 368 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
369
- 369 * @param {Object} params.context.db - Database connection (knex instance or transaction)
370
- 370 * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
371
- 371 */
372
- 372 helpers.dataGetMinimal = async ({ scopeName, context }) => {
373
- 373 const storageAdapter = getScopeStorageAdapter(scopeName)
374
- 374 if (storageAdapter) {
375
- 375 context.storageAdapter = storageAdapter
376
- 376 }
377
- 377 const scope = api.resources[scopeName]
378
- 378 const id = context.id
379
- 379 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
380
- 380 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
381
- 381 const db = context.db || api.knex.instance
382
- 382
383
- 383 log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
384
- 384
385
- 385 // Build query - no filtering hooks for single records
386
- 386 // Permission checks will handle access control
387
- 387 let query = db(tableName).where(idProperty, id)
388
- 388
389
- 389 // Add alias if idProperty is not 'id'
390
- 390 if (idProperty !== 'id') {
391
- 391 query = query.select('*', `${idProperty} as id`)
392
- 392 }
393
- 393
394
- 394 // Execute query
395
- 395 const record = await query.first()
396
- 396
397
- 397 if (!record) {
398
- 398 return null
399
- 399 }
400
- 400
401
- 401 // Transform to JSON:API format with belongsTo relationships
402
- 402 return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
403
- 403 }
404
- 404
405
- 405 /**
406
- 406 * Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
407
- 407 *
408
- 408 * @param {Object} params - The parameters object
409
- 409 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
410
- 410 * @param {Object} params.context - The context object containing request-specific data
411
- 411 * @param {Object} params.context.schemaInfo - Schema information for the resource
412
- 412 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
413
- 413 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
414
- 414 * @param {Object} params.context.schemaInfo.searchSchemaInstance - Search schema for filtering capabilities
415
- 415 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
416
- 416 * @param {Object} params.context.queryParams - Query parameters object
417
- 417 * @param {Object} [params.context.queryParams.filters] - Filter conditions
418
- 418 * @param {Array<string>} [params.context.queryParams.sort] - Sort fields (prefix with - for DESC)
419
- 419 * @param {Object} [params.context.queryParams.page] - Pagination parameters
420
- 420 * @param {number} [params.context.queryParams.page.size] - Page size
421
- 421 * @param {number} [params.context.queryParams.page.number] - Page number (offset pagination)
422
- 422 * @param {string} [params.context.queryParams.page.after] - Cursor for forward pagination
423
- 423 * @param {string} [params.context.queryParams.page.before] - Cursor for backward pagination
424
- 424 * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
425
- 425 * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
426
- 426 * @param {Object} params.context.db - Database connection (knex instance or transaction)
427
- 427 * @param {Array<string>} params.context.sortableFields - Array of fields that can be sorted
428
- 428 * @param {Object} [params.context.knexQuery] - Temporarily set during hooks for query building
429
- 429 * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
430
- 430 * @param {Function} params.runHooks - Function to run hooks (e.g., 'knexQueryFiltering')
431
- 431 * @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
432
- 432 */
433
- 433 helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
434
- 434 const storageAdapter = getScopeStorageAdapter(scopeName)
435
- 435 if (storageAdapter) {
436
- 436 context.storageAdapter = storageAdapter
437
- 437 }
438
- 438 const scope = api.resources[scopeName]
439
- 439 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
440
- 440 const schemaInfo = context.schemaInfo
441
- 441 const queryParams = context.queryParams
442
- 442 const db = context.db || api.knex.instance
443
- 443 const sortableFields = context.sortableFields
444
- 444
445
- 445 log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
446
- 446 log.debug(`[Knex] QUERY ${tableName}`, queryParams)
447
- 447
448
- 448 // Build field selection for sparse fieldsets
449
- 449 // This determines which fields to SELECT from database
450
- 450 // and tracks dependencies needed for computed fields
451
- 451 const fieldSelectionInfo = await buildFieldSelection(
452
- 452 scope,
453
- 453 { context }
454
- 454 )
455
- 455
456
- 456 // Store dependency info in context for enrichAttributes
457
- 457 // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
458
- 458 // The REST API plugin will use this to remove 'cost' from response if not requested
459
- 459 context.computedDependencies = fieldSelectionInfo.computedDependencies
460
- 460
461
- 461 // Start building query with table prefix (for JOIN support)
462
- 462 let query = db(tableName)
463
- 463
464
- 464 const selectTranslator = createSelectTranslator(storageAdapter)
465
- 465 query = buildQuerySelection(
466
- 466 query,
467
- 467 tableName,
468
- 468 fieldSelectionInfo.fieldsToSelect,
469
- 469 true,
470
- 470 selectTranslator ? { translateColumn: selectTranslator } : undefined
471
- 471 )
472
- 472
473
- 473 /* ═══════════════════════════════════════════════════════════════════
474
- 474 * FILTERING HOOKS
475
- 475 * This is where the magic happens. The knexQueryFiltering hook is called
476
- 476 * to apply all filter conditions. The searchSchemaFilter hook (registered
477
- 477 * above) will process the searchSchema and apply filters with JOINs.
478
- 478 *
479
- 479 * IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
480
- 480 * to ensure proper grouping and prevent accidental filter bypass.
481
- 481 * ═══════════════════════════════════════════════════════════════════ */
482
- 482
483
- 483 log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
484
- 484
485
- 485 log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
486
- 486
487
- 487 log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
488
- 488
489
- 489 // YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
490
- 490
491
- 491 // Store the query data in context where hooks can access it
492
- 492 // This is the proper way to share data between methods and hooks
493
- 493 if (context) {
494
- 494 context.knexQuery = {
495
- 495 query,
496
- 496 filters: queryParams.filters,
497
- 497 schemaInfo,
498
- 498 scopeName,
499
- 499 tableName,
500
- 500 db,
501
- 501 adapter: storageAdapter,
502
- 502 }
503
- 503
504
- 504 log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
505
- 505 }
506
- 506
507
- 507 await runHooks('knexQueryFiltering')
508
- 508
509
- 509 // Clean up after hook execution
510
- 510 if (context && context.knexQuery) {
511
- 511 delete context.knexQuery
512
- 512 }
513
- 513
514
- 514 log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
515
- 515
516
- 516 // Apply sorting directly (no hooks)
517
- 517 if (queryParams.sort && queryParams.sort.length > 0) {
518
- 518 queryParams.sort.forEach(sortField => {
519
- 519 const desc = sortField.startsWith('-')
520
- 520 const field = desc ? sortField.substring(1) : sortField
521
- 521
522
- 522 // Check if field is sortable
523
- 523 if (sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
524
- 524 log.warn(`Ignoring non-sortable field: ${field}`)
525
- 525 return // Skip non-sortable fields
526
- 526 }
527
- 527
528
- 528 // Map relationship names to actual database columns for sorting
529
- 529 // Check if this is a relationship field that needs to be mapped
530
- 530 let dbField = field
531
- 531 const searchField = schemaInfo.searchSchemaStructure?.[field]
532
- 532 if (searchField?.actualField) {
533
- 533 // This is a relationship field, use the actual database column
534
- 534 dbField = searchField.actualField
535
- 535 }
536
- 536 const translatedField = storageAdapter?.translateColumn
537
- 537 ? storageAdapter.translateColumn(dbField)
538
- 538 : dbField
539
- 539
540
- 540 query.orderBy(translatedField, desc ? 'desc' : 'asc')
541
- 541 })
542
- 542 }
543
- 543
544
- 544 // Apply pagination
545
- 545 // Check if page object has any actual pagination parameters
546
- 546 const hasPageParams = queryParams.page &&
547
- 547 (queryParams.page.size !== undefined ||
548
- 548 queryParams.page.number !== undefined ||
549
- 549 queryParams.page.after !== undefined ||
550
- 550 queryParams.page.before !== undefined)
551
- 551
552
- 552 if (hasPageParams) {
553
- 553 const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
554
- 554 const pageSize = Math.min(
555
- 555 requestedSize,
556
- 556 scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
557
- 557 )
558
- 558
559
- 559 // Validate page size
560
- 560 if (requestedSize <= 0) {
561
- 561 throw new RestApiValidationError(
562
- 562 'Page size must be greater than 0',
563
- 563 {
564
- 564 fields: ['page.size'],
565
- 565 violations: [{
566
- 566 field: 'page.size',
567
- 567 rule: 'min_value',
568
- 568 message: 'Page size must be a positive number'
569
- 569 }]
570
- 570 }
571
- 571 )
572
- 572 }
573
- 573
574
- 574 // Offset-based pagination
575
- 575 if (queryParams.page.number !== undefined) {
576
- 576 const pageNumber = queryParams.page.number || 1
577
- 577 query
578
- 578 .limit(pageSize)
579
- 579 .offset((pageNumber - 1) * pageSize)
580
- 580 }
581
- 581 // Cursor-based pagination
582
- 582 else if (queryParams.page.after || queryParams.page.before) {
583
- 583 // Fetch one extra record to determine if there are more
584
- 584 query.limit(pageSize + 1)
585
- 585
586
- 586 // Get the configured ID property
587
- 587 const idProperty = context.schemaInfo.idProperty || 'id'
588
- 588
589
- 589 if (queryParams.page.after) {
590
- 590 let cursorData
591
- 591 try {
592
- 592 cursorData = parseCursor(queryParams.page.after)
593
- 593 } catch (error) {
594
- 594 throw new RestApiValidationError(
595
- 595 'Invalid cursor format in page[after] parameter',
596
- 596 {
597
- 597 fields: ['page.after'],
598
- 598 violations: [{
599
- 599 field: 'page.after',
600
- 600 rule: 'invalid_cursor',
601
- 601 message: 'The cursor value is not valid'
602
- 602 }]
603
- 603 }
604
- 604 )
605
- 605 }
606
- 606
607
- 607 // Build sort fields array with directions
608
- 608 const sortFields = []
609
- 609 if (queryParams.sort && queryParams.sort.length > 0) {
610
- 610 queryParams.sort.forEach(sortField => {
611
- 611 const desc = sortField.startsWith('-')
612
- 612 const field = desc ? sortField.substring(1) : sortField
613
- 613 sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
614
- 614 })
615
- 615 } else if (scope.vars.defaultSort) {
616
- 616 sortFields.push({
617
- 617 field: scope.vars.defaultSort.field || idProperty,
618
- 618 direction: scope.vars.defaultSort.direction || 'ASC'
619
- 619 })
620
- 620 } else {
621
- 621 sortFields.push({ field: idProperty, direction: 'ASC' })
622
- 622 }
623
- 623
624
- 624 // Build compound WHERE clause for multi-field cursor
625
- 625 query.where(function () {
626
- 626 sortFields.forEach((sortInfo, index) => {
627
- 627 const { field, direction } = sortInfo
628
- 628 const qualifiedField = `${tableName}.${field}`
629
- 629 const cursorValue = cursorData[field]
630
- 630
631
- 631 if (cursorValue === undefined) {
632
- 632 log.warn(`Cursor missing value for sort field: ${field}`)
633
- 633 return
634
- 634 }
635
- 635
636
- 636 // Build condition for this level and all previous levels
637
- 637 this.orWhere(function () {
638
- 638 // All previous fields must be equal
639
- 639 for (let i = 0; i < index; i++) {
640
- 640 const prevField = sortFields[i].field
641
- 641 const prevQualifiedField = `${tableName}.${prevField}`
642
- 642 const prevValue = cursorData[prevField]
643
- 643 if (prevValue !== undefined) {
644
- 644 this.where(prevQualifiedField, '=', prevValue)
645
- 645 }
646
- 646 }
647
- 647
648
- 648 // Current field must be greater/less than cursor value
649
- 649 if (direction === 'DESC') {
650
- 650 this.where(qualifiedField, '<', cursorValue)
651
- 651 } else {
652
- 652 this.where(qualifiedField, '>', cursorValue)
653
- 653 }
654
- 654 })
655
- 655 })
656
- 656 })
657
- 657 } else if (queryParams.page.before) {
658
- 658 let cursorData
659
- 659 try {
660
- 660 cursorData = parseCursor(queryParams.page.before)
661
- 661 } catch (error) {
662
- 662 throw new RestApiValidationError(
663
- 663 'Invalid cursor format in page[before] parameter',
664
- 664 {
665
- 665 fields: ['page.before'],
666
- 666 violations: [{
667
- 667 field: 'page.before',
668
- 668 rule: 'invalid_cursor',
669
- 669 message: 'The cursor value is not valid'
670
- 670 }]
671
- 671 }
672
- 672 )
673
- 673 }
674
- 674
675
- 675 // Build sort fields array with directions
676
- 676 const sortFields = []
677
- 677 if (queryParams.sort && queryParams.sort.length > 0) {
678
- 678 queryParams.sort.forEach(sortField => {
679
- 679 const desc = sortField.startsWith('-')
680
- 680 const field = desc ? sortField.substring(1) : sortField
681
- 681 sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
682
- 682 })
683
- 683 } else if (scope.vars.defaultSort) {
684
- 684 sortFields.push({
685
- 685 field: scope.vars.defaultSort.field || idProperty,
686
- 686 direction: scope.vars.defaultSort.direction || 'ASC'
687
- 687 })
688
- 688 } else {
689
- 689 sortFields.push({ field: idProperty, direction: 'ASC' })
690
- 690 }
691
- 691
692
- 692 // Build compound WHERE clause for multi-field cursor (reversed for before)
693
- 693 query.where(function () {
694
- 694 sortFields.forEach((sortInfo, index) => {
695
- 695 const { field, direction } = sortInfo
696
- 696 const qualifiedField = `${tableName}.${field}`
697
- 697 const cursorValue = cursorData[field]
698
- 698
699
- 699 if (cursorValue === undefined) {
700
- 700 log.warn(`Cursor missing value for sort field: ${field}`)
701
- 701 return
702
- 702 }
703
- 703
704
- 704 // Build condition for this level and all previous levels
705
- 705 this.orWhere(function () {
706
- 706 // All previous fields must be equal
707
- 707 for (let i = 0; i < index; i++) {
708
- 708 const prevField = sortFields[i].field
709
- 709 const prevQualifiedField = `${tableName}.${prevField}`
710
- 710 const prevValue = cursorData[prevField]
711
- 711 if (prevValue !== undefined) {
712
- 712 this.where(prevQualifiedField, '=', prevValue)
713
- 713 }
714
- 714 }
715
- 715
716
- 716 // Current field must be less/greater than cursor value (reversed)
717
- 717 if (direction === 'DESC') {
718
- 718 this.where(qualifiedField, '>', cursorValue)
719
- 719 } else {
720
- 720 this.where(qualifiedField, '<', cursorValue)
721
- 721 }
722
- 722 })
723
- 723 })
724
- 724 })
725
- 725 }
726
- 726 }
727
- 727 // Default pagination if only size is specified - treat as cursor-based for "load more"
728
- 728 else {
729
- 729 // Fetch one extra to detect hasMore
730
- 730 query.limit(pageSize + 1)
731
- 731 }
732
- 732 } else {
733
- 733 // No pagination params provided - apply default limit
734
- 734 const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
735
- 735 query.limit(defaultLimit)
736
- 736 }
737
- 737
738
- 738 // Execute query
739
- 739 const records = await query
740
- 740
741
- 741 // Store query string for response building
742
- 742 const queryParts = []
743
- 743 Object.entries(queryParams).forEach(([key, value]) => {
744
- 744 if (Array.isArray(value)) {
745
- 745 // Handle arrays (like sort, include)
746
- 746 if (value.length > 0) {
747
- 747 queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
748
- 748 }
749
- 749 } else if (typeof value === 'object' && value !== null) {
750
- 750 // Handle nested objects (like filters, fields, page)
751
- 751 Object.entries(value).forEach(([subKey, subValue]) => {
752
- 752 if (subValue !== undefined && subValue !== null) {
753
- 753 if (typeof subValue === 'object' && !Array.isArray(subValue)) {
754
- 754 // Handle deeply nested objects (like filters[country][code])
755
- 755 Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
756
- 756 if (subSubValue !== undefined && subSubValue !== null) {
757
- 757 queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
758
- 758 }
759
- 759 })
760
- 760 } else {
761
- 761 queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
762
- 762 }
763
- 763 }
764
- 764 })
765
- 765 } else if (value !== undefined && value !== null) {
766
- 766 queryParts.push(`${key}=${encodeURIComponent(value)}`)
767
- 767 }
768
- 768 })
769
- 769
770
- 770 // Initialize returnMeta namespace for thread-safe metadata
771
- 771 context.returnMeta = context.returnMeta || {}
772
- 772 context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
773
- 773
774
- 774 // Execute count query for pagination if offset-based pagination is used
775
- 775 if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
776
- 776 const page = parseInt(queryParams.page?.number) || 1
777
- 777 const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
778
- 778
779
- 779 // Only execute count query if enabled
780
- 780 if (scope.vars.enablePaginationCounts) {
781
- 781 // Build count query with same filters as main query
782
- 782 const countQuery = db(tableName)
783
- 783
784
- 784 // Apply filters through hooks (same as main query)
785
- 785 if (queryParams.filters && Object.keys(queryParams.filters).length > 0) {
786
- 786 // Store query data in context for hooks
787
- 787 if (context) {
788
- 788 context.knexQuery = {
789
- 789 query: countQuery,
790
- 790 filters: queryParams.filters,
791
- 791 scopeName,
792
- 792 tableName,
793
- 793 schemaInfo,
794
- 794 db
795
- 795 }
796
- 796 }
797
- 797
798
- 798 // Run the same filtering hooks
799
- 799 await runHooks('knexQueryFiltering')
800
- 800
801
- 801 // Clean up
802
- 802 if (context && context.knexQuery) {
803
- 803 delete context.knexQuery
804
- 804 }
805
- 805 }
806
- 806
807
- 807 // Get total count
808
- 808 const countResult = await countQuery.count('* as total').first()
809
- 809 const total = parseInt(countResult.total)
810
- 810
811
- 811 // Calculate pagination metadata with total
812
- 812 context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
813
- 813 } else {
814
- 814 // Without count, we can still provide basic pagination info
815
- 815 context.returnMeta.paginationMeta = {
816
- 816 page,
817
- 817 pageSize
818
- 818 // No total, pageCount, or hasMore when counts are disabled
819
- 819 }
820
- 820 }
821
- 821
822
- 822 // Generate links
823
- 823 const urlPrefix = getUrlPrefix(context, scope)
824
- 824 context.returnMeta.paginationLinks = generatePaginationLinks(
825
- 825 urlPrefix,
826
- 826 scopeName,
827
- 827 queryParams,
828
- 828 context.returnMeta.paginationMeta
829
- 829 )
830
- 830 }
831
- 831
832
- 832 // Handle cursor-based pagination meta
833
- 833 // Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
834
- 834 if (queryParams.page?.after || queryParams.page?.before ||
835
- 835 (queryParams.page?.size && queryParams.page?.number === undefined)) {
836
- 836 const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
837
- 837
838
- 838 // Check if there are more records
839
- 839 // We fetched pageSize + 1 records to detect if there are more
840
- 840 const hasMore = records.length > pageSize
841
- 841
842
- 842 // Remove the extra record if present
843
- 843 if (hasMore) {
844
- 844 records.pop()
845
- 845 }
846
- 846
847
- 847 // Determine sort fields for cursor
848
- 848 const idProperty = context.schemaInfo.idProperty || 'id'
849
- 849 let sortFields = [idProperty]
850
- 850 if (queryParams.sort && queryParams.sort.length > 0) {
851
- 851 sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
852
- 852 }
853
- 853
854
- 854 context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
855
- 855 const urlPrefix = getUrlPrefix(context, scope)
856
- 856 context.returnMeta.paginationLinks = generateCursorPaginationLinks(
857
- 857 urlPrefix,
858
- 858 scopeName,
859
- 859 queryParams,
860
- 860 records,
861
- 861 pageSize,
862
- 862 hasMore,
863
- 863 sortFields
864
- 864 )
865
- 865 }
866
- 866
867
- 867 // Load relationship identifiers for all hasMany relationships
868
- 868 await loadRelationshipIdentifiers(records, scopeName, scopes, db)
869
- 869
870
- 870 // Process includes
871
- 871 const included = await processIncludes(scope, records, {
872
- 872 log,
873
- 873 scopes,
874
- 874 knex,
875
- 875 context,
876
- 876 api
877
- 877 })
878
- 878
879
- 879 // Build and return response
880
- 880 return buildJsonApiResponse(scope, records, included, false, scopeName, context)
881
- 881 }
882
- 882
883
- 883 /**
884
- 884 * Creates a new resource in the database
885
- 885 *
886
- 886 * @param {Object} params - The parameters object
887
- 887 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
888
- 888 * @param {Object} params.context - The context object containing request-specific data
889
- 889 * @param {Object} params.context.schemaInfo - Schema information for the resource
890
- 890 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
891
- 891 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
892
- 892 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
893
- 893 * @param {Object} params.context.db - Database connection (knex instance or transaction)
894
- 894 * @param {Object} params.context.inputRecord - JSON:API formatted input record
895
- 895 * @param {Object} params.context.inputRecord.data - The resource data
896
- 896 * @param {Object} params.context.inputRecord.data.attributes - The resource attributes to insert
897
- 897 * @returns {Promise<string|number>} The ID of the newly created resource
898
- 898 */
899
- 899 helpers.dataPost = async ({ scopeName, context }) => {
900
- 900 const storageAdapter = getScopeStorageAdapter(scopeName)
901
- 901 if (storageAdapter) {
902
- 902 context.storageAdapter = storageAdapter
903
- 903 }
904
- 904 const scope = api.resources[scopeName]
905
- 905 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
906
- 906 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
907
- 907 const db = context.db || api.knex.instance
908
- 908 const inputRecord = context.inputRecord
909
- 909
910
- 910 log.debug(`[Knex] POST ${tableName}`, inputRecord)
911
- 911
912
- 912 // Extract attributes from JSON:API format
913
- 913 const attributes = inputRecord.data.attributes
914
- 914
915
- 915 // Strip non-database fields (computed and virtual) before insert
916
- 916 const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
917
- 917
918
- 918 // Insert and get the new ID
919
- 919 const result = await db(tableName).insert(dbAttributes).returning(idProperty)
920
- 920
921
- 921 // Extract the ID value (SQLite returns array of objects)
922
- 922 const id = result[0]?.[idProperty] || result[0]
923
- 923 return id
924
- 924 }
925
- 925
926
- 926 /**
927
- 927 * Replaces an entire resource (PUT operation) or creates it with a specific ID
928
- 928 *
929
- 929 * @param {Object} params - The parameters object
930
- 930 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
931
- 931 * @param {Object} params.context - The context object containing request-specific data
932
- 932 * @param {string|number} params.context.id - The ID of the resource to replace or create
933
- 933 * @param {Object} params.context.schemaInfo - Schema information for the resource
934
- 934 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
935
- 935 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
936
- 936 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
937
- 937 * @param {Object} params.context.db - Database connection (knex instance or transaction)
938
- 938 * @param {Object} params.context.inputRecord - JSON:API formatted input record
939
- 939 * @param {Object} params.context.inputRecord.data - The resource data
940
- 940 * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes
941
- 941 * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
942
- 942 * @param {boolean} params.context.isCreate - Whether this is a create operation (true) or update (false)
943
- 943 * @returns {Promise<void>} Resolves when the operation is complete
944
- 944 * @throws {RestApiResourceError} When updating and the resource is not found
945
- 945 */
946
- 946 helpers.dataPut = async ({ scopeName, context }) => {
947
- 947 const storageAdapter = getScopeStorageAdapter(scopeName)
948
- 948 if (storageAdapter) {
949
- 949 context.storageAdapter = storageAdapter
950
- 950 }
951
- 951 const scope = api.resources[scopeName]
952
- 952 const id = context.id
953
- 953 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
954
- 954 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
955
- 955 const db = context.db || api.knex.instance
956
- 956 const inputRecord = context.inputRecord
957
- 957 const isCreate = context.isCreate
958
- 958
959
- 959 log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
960
- 960
961
- 961 // Extract attributes and process relationships using helper
962
- 962 const attributes = inputRecord.data.attributes || {}
963
- 963 const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
964
- 964 const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
965
- 965
966
- 966 // Strip non-database fields (computed and virtual) before database operation
967
- 967 const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
968
- 968
969
- 969 // Map 'id' to actual idProperty if needed (for PUT with specific ID)
970
- 970 if (idProperty !== 'id' && inputRecord.data.id) {
971
- 971 finalAttributes[idProperty] = inputRecord.data.id
972
- 972 }
973
- 973
974
- 974 if (isCreate) {
975
- 975 // Create mode - insert new record with specified ID
976
- 976 const recordData = {
977
- 977 ...finalAttributes,
978
- 978 [idProperty]: id
979
- 979 }
980
- 980
981
- 981 await db(tableName).insert(recordData)
982
- 982 } else {
983
- 983 // Update mode - check if record exists first
984
- 984 const exists = await db(tableName)
985
- 985 .where(idProperty, id)
986
- 986 .first()
987
- 987
988
- 988 if (!exists) {
989
- 989 throw new RestApiResourceError(
990
- 990 'Resource not found',
991
- 991 {
992
- 992 subtype: ERROR_SUBTYPES.NOT_FOUND,
993
- 993 resourceType: scopeName,
994
- 994 resourceId: id
995
- 995 }
996
- 996 )
997
- 997 }
998
- 998
999
- 999 // Remove the idProperty from attributes to prevent updating the primary key
1000
- 1000 delete finalAttributes[idProperty]
1001
- 1001
1002
- 1002 // Update the record (replace all fields)
1003
- 1003 if (Object.keys(finalAttributes).length > 0) {
1004
- 1004 await db(tableName)
1005
- 1005 .where(idProperty, id)
1006
- 1006 .update(finalAttributes)
1007
- 1007 }
1008
- 1008 }
1009
- 1009 }
1010
- 1010
1011
- 1011 /**
1012
- 1012 * Partially updates a resource (PATCH operation)
1013
- 1013 *
1014
- 1014 * @param {Object} params - The parameters object
1015
- 1015 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1016
- 1016 * @param {Object} params.context - The context object containing request-specific data
1017
- 1017 * @param {string|number} params.context.id - The ID of the resource to update
1018
- 1018 * @param {Object} params.context.schemaInfo - Schema information for the resource
1019
- 1019 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
1020
- 1020 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
1021
- 1021 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
1022
- 1022 * @param {Object} params.context.db - Database connection (knex instance or transaction)
1023
- 1023 * @param {Object} params.context.inputRecord - JSON:API formatted input record with partial updates
1024
- 1024 * @param {Object} params.context.inputRecord.data - The resource data
1025
- 1025 * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes to update
1026
- 1026 * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
1027
- 1027 * @returns {Promise<void>} Resolves when the update is complete
1028
- 1028 * @throws {RestApiResourceError} When the resource is not found
1029
- 1029 */
1030
- 1030 helpers.dataPatch = async ({ scopeName, context }) => {
1031
- 1031 const storageAdapter = getScopeStorageAdapter(scopeName)
1032
- 1032 if (storageAdapter) {
1033
- 1033 context.storageAdapter = storageAdapter
1034
- 1034 }
1035
- 1035 const scope = api.resources[scopeName]
1036
- 1036 const id = context.id
1037
- 1037 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1038
- 1038 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1039
- 1039 const db = context.db || api.knex.instance
1040
- 1040 const inputRecord = context.inputRecord
1041
- 1041
1042
- 1042 log.debug(`[Knex] PATCH ${tableName}/${id}`)
1043
- 1043
1044
- 1044 // Check if record exists
1045
- 1045 const exists = await db(tableName)
1046
- 1046 .where(idProperty, id)
1047
- 1047 .first()
1048
- 1048
1049
- 1049 if (!exists) {
1050
- 1050 throw new RestApiResourceError(
1051
- 1051 'Resource not found',
1052
- 1052 {
1053
- 1053 subtype: ERROR_SUBTYPES.NOT_FOUND,
1054
- 1054 resourceType: scopeName,
1055
- 1055 resourceId: id
1056
- 1056 }
1057
- 1057 )
1058
- 1058 }
1059
- 1059
1060
- 1060 // Extract attributes and process relationships using helper
1061
- 1061 const attributes = inputRecord.data.attributes || {}
1062
- 1062 const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
1063
- 1063 const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
1064
- 1064
1065
- 1065 // Strip non-database fields (computed and virtual) before database operation
1066
- 1066 const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
1067
- 1067
1068
- 1068 log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
1069
- 1069
1070
- 1070 // Remove the idProperty from attributes to prevent updating the primary key
1071
- 1071 delete finalAttributes[idProperty]
1072
- 1072
1073
- 1073 // Update only if there are changes
1074
- 1074 if (Object.keys(finalAttributes).length > 0) {
1075
- 1075 await db(tableName)
1076
- 1076 .where(idProperty, id)
1077
- 1077 .update(finalAttributes)
1078
- 1078 }
1079
- 1079 }
1080
- 1080
1081
- 1081 /**
1082
- 1082 * Deletes a resource from the database
1083
- 1083 *
1084
- 1084 * @param {Object} params - The parameters object
1085
- 1085 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
1086
- 1086 * @param {Object} params.context - The context object containing request-specific data
1087
- 1087 * @param {string|number} params.context.id - The ID of the resource to delete
1088
- 1088 * @param {Object} params.context.schemaInfo - Schema information for the resource
1089
- 1089 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
1090
- 1090 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
1091
- 1091 * @param {Object} params.context.db - Database connection (knex instance or transaction)
1092
- 1092 * @returns {Promise<Object>} Returns { success: true } when deletion is successful
1093
- 1093 * @throws {RestApiResourceError} When the resource is not found
1094
- 1094 */
1095
- 1095 helpers.dataDelete = async ({ scopeName, context }) => {
1096
- 1096 const storageAdapter = getScopeStorageAdapter(scopeName)
1097
- 1097 if (storageAdapter) {
1098
- 1098 context.storageAdapter = storageAdapter
1099
- 1099 }
1100
- 1100 const scope = api.resources[scopeName]
1101
- 1101 const id = context.id
1102
- 1102
1103
- 1103 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
1104
- 1104 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
1105
- 1105 const db = context.db || api.knex.instance
1106
- 1106
1107
- 1107 log.debug(`[Knex] DELETE ${tableName}/${id}`)
1108
- 1108
1109
- 1109 // Check if record exists
1110
- 1110 const exists = await db(tableName)
1111
- 1111 .where(idProperty, id)
1112
- 1112 .first()
1113
- 1113
1114
- 1114 if (!exists) {
1115
- 1115 throw new RestApiResourceError(
1116
- 1116 'Resource not found',
1117
- 1117 {
1118
- 1118 subtype: ERROR_SUBTYPES.NOT_FOUND,
1119
- 1119 resourceType: scopeName,
1120
- 1120 resourceId: id
1121
- 1121 }
1122
- 1122 )
1123
- 1123 }
1124
- 1124
1125
- 1125 // Delete the record
1126
- 1126 await db(tableName)
1127
- 1127 .where(idProperty, id)
1128
- 1128 .delete()
1129
- 1129
1130
- 1130 return { success: true }
1131
- 1131 }
1132
- 1132
1133
- 1133 log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
1134
- 1134 }
1135
- 1135 }