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
@@ -5,6 +5,7 @@ import { createSchema } from 'json-rest-schema'
5
5
  import {
6
6
  DEFAULT_QUERY_LIMIT,
7
7
  DEFAULT_MAX_QUERY_LIMIT,
8
+ COMPUTED_DEPENDENCIES_KEY,
8
9
  } from './lib/querying-writing/knex-constants.js'
9
10
  import {
10
11
  calculatePaginationMeta,
@@ -29,13 +30,26 @@ import {
29
30
  ensureSearchFieldsAreIndexed,
30
31
  generateSearchSchemaFromSchema,
31
32
  } from './lib/querying-writing/schema-helpers.js'
32
- import { buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
33
+ import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
33
34
  import {
34
35
  polymorphicFiltersHook,
35
36
  crossTableFiltersHook,
36
37
  basicFiltersHook,
37
38
  } from './lib/querying/knex-query-helpers.js'
38
- import { createStorageAdapter } from './lib/storage/storage-adapter.js'
39
+ import {
40
+ createStorageAdapter,
41
+ } from './lib/storage/storage-adapter.js'
42
+ import {
43
+ translateCanonicalAttributesForStorage,
44
+ translateCanonicalRecordFromStorage,
45
+ } from './lib/storage/canonical-storage-mapping.js'
46
+ import {
47
+ buildCursorPredicateChains,
48
+ applyQueryFieldOrder,
49
+ buildEffectiveSortList,
50
+ parseSortEntry
51
+ } from './lib/querying/query-field-sort-helpers.js'
52
+ import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
39
53
 
40
54
  const DEFAULT_TENANT = 'default'
41
55
  const LINKS_TABLE = 'any_links'
@@ -107,6 +121,10 @@ export const RestApiAnyapiKnexPlugin = {
107
121
  return descriptor
108
122
  }
109
123
 
124
+ const getAllowedQueryFieldNames = (scopeName) => (
125
+ Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
126
+ )
127
+
110
128
  const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
111
129
  // Filtering is handled by the query hooks to keep behavior consistent
112
130
  // (operators, null handling, multi-field, joins, polymorphic, etc.).
@@ -148,35 +166,35 @@ export const RestApiAnyapiKnexPlugin = {
148
166
  }
149
167
  }
150
168
 
151
- const applySortingToQuery = ({ query, sort, descriptor, scope }) => {
152
- let sortList = Array.isArray(sort) ? sort : (sort ? [sort] : [])
153
-
154
- if (sortList.length === 0 && scope?.vars?.defaultSort) {
155
- const defaultSort = scope.vars.defaultSort
156
- if (Array.isArray(defaultSort)) {
157
- sortList = defaultSort
158
- } else if (typeof defaultSort === 'string') {
159
- sortList = [defaultSort]
160
- } else if (defaultSort && typeof defaultSort === 'object') {
161
- const field = defaultSort.field || defaultSort.column || 'id'
162
- const direction = (defaultSort.direction || '').toLowerCase() === 'desc' ? '-' : ''
163
- sortList = [`${direction}${field}`]
164
- }
165
- }
166
-
167
- const effectiveSort = sortList.length > 0 ? sortList : ['id']
169
+ const applySortingToQuery = ({ query, sort, descriptor, scope, queryFieldRuntimeByField = new Map() }) => {
170
+ const effectiveSort = buildEffectiveSortList(sort, {
171
+ defaultSort: scope?.vars?.defaultSort,
172
+ idField: 'id'
173
+ })
168
174
  const descriptors = []
169
175
 
170
176
  for (const entry of effectiveSort) {
171
- const desc = typeof entry === 'string' && entry.startsWith('-')
172
- const field = typeof entry === 'string' ? (desc ? entry.slice(1) : entry) : entry
177
+ const { field, direction, sqlDirection } = parseSortEntry(entry)
178
+ const queryFieldRuntime = queryFieldRuntimeByField.get(field)
179
+ if (queryFieldRuntime) {
180
+ applyQueryFieldOrder(query, queryFieldRuntime, sqlDirection)
181
+ descriptors.push({
182
+ field,
183
+ direction,
184
+ definition: queryFieldRuntime.definition,
185
+ isRelationship: false,
186
+ queryFieldRuntime,
187
+ })
188
+ continue
189
+ }
190
+
173
191
  const fieldInfo = resolveFieldInfo(descriptor, field)
174
192
  if (!fieldInfo?.column) continue
175
- query.orderBy(fieldInfo.column, desc ? 'desc' : 'asc')
193
+ query.orderBy(fieldInfo.column, direction)
176
194
  descriptors.push({
177
195
  field,
178
196
  column: fieldInfo.column,
179
- direction: desc ? 'desc' : 'asc',
197
+ direction,
180
198
  definition: fieldInfo.definition || null,
181
199
  isRelationship: fieldInfo.isRelationship || false,
182
200
  })
@@ -282,64 +300,37 @@ export const RestApiAnyapiKnexPlugin = {
282
300
  return typed
283
301
  }
284
302
 
285
- const buildCursorPredicate = (cursorValues, operatorSelector) => {
286
- if (!descriptors || descriptors.length === 0) {
287
- return
288
- }
289
-
290
- if (descriptors.length === 1) {
291
- const descriptor = descriptors[0]
292
- const value = cursorValues[descriptor.field]
293
- if (value === undefined) return
294
- const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
295
- const operator = operatorSelector(descriptor.direction)
296
- if (adapter) {
297
- query.whereRaw(`${columnRef} ${operator} ?`, [value])
298
- } else {
299
- query.where(columnRef, operator, value)
300
- }
301
- return
302
- }
303
-
304
- const translatedConditions = []
305
-
306
- descriptors.forEach((descriptor, index) => {
307
- const value = cursorValues[descriptor.field]
308
- if (value === undefined) return
309
-
310
- const chain = []
311
- for (let i = 0; i < index; i += 1) {
312
- const prev = descriptors[i]
313
- const prevValue = cursorValues[prev.field]
314
- if (prevValue === undefined) return
315
- const prevColumn = adapter ? adapter.translateColumn(prev.column) : prev.column
316
- chain.push({ column: prevColumn, operator: '=', value: prevValue })
317
- }
318
-
319
- const operator = operatorSelector(descriptor.direction)
320
- const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
321
- chain.push({ column: columnRef, operator, value })
322
- translatedConditions.push(chain)
323
- })
303
+ const applyRawCursorPredicate = (cursorValues, operatorSelector) => {
304
+ const predicateChains = buildCursorPredicateChains(
305
+ descriptors,
306
+ cursorValues,
307
+ operatorSelector,
308
+ { onMissingValue: () => {} }
309
+ )
324
310
 
325
- if (translatedConditions.length === 0) {
311
+ if (predicateChains.length === 0) {
326
312
  return
327
313
  }
328
314
 
329
315
  const predicateParts = []
330
316
  const predicateBindings = []
331
- translatedConditions.forEach((chain) => {
332
- const chainParts = chain.map(({ column, operator, value }) => {
317
+
318
+ predicateChains.forEach((chain) => {
319
+ const chainParts = chain.map(({ descriptor, operator, value }) => {
320
+ if (descriptor.queryFieldRuntime) {
321
+ predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []), value)
322
+ return `(${descriptor.queryFieldRuntime.sql}) ${operator} ?`
323
+ }
324
+
325
+ const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
333
326
  predicateBindings.push(value)
334
- return `${column} ${operator} ?`
327
+ return `${columnRef} ${operator} ?`
335
328
  })
329
+
336
330
  predicateParts.push(`(${chainParts.join(' AND ')})`)
337
331
  })
338
332
 
339
- if (predicateParts.length > 0) {
340
- const combined = predicateParts.join(' OR ')
341
- query.whereRaw(`(${combined})`, predicateBindings)
342
- }
333
+ query.whereRaw(`(${predicateParts.join(' OR ')})`, predicateBindings)
343
334
  }
344
335
 
345
336
  const ensurePageSize = () => {
@@ -378,11 +369,11 @@ export const RestApiAnyapiKnexPlugin = {
378
369
  if (pageParams.after) {
379
370
  const cursorMap = parseCursorOrThrow(pageParams.after, 'after')
380
371
  const cursorValues = coerceCursorValues(cursorMap)
381
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
372
+ applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
382
373
  } else if (pageParams.before) {
383
374
  const cursorMap = parseCursorOrThrow(pageParams.before, 'before')
384
375
  const cursorValues = coerceCursorValues(cursorMap)
385
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
376
+ applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
386
377
  }
387
378
 
388
379
  return {
@@ -919,7 +910,9 @@ export const RestApiAnyapiKnexPlugin = {
919
910
  const descriptor = await getDescriptor(scopeName)
920
911
  const { canonical } = descriptor
921
912
  const attributes = context.inputRecord?.data?.attributes || {}
922
- const row = translateAttributesForStorage(attributes, descriptor)
913
+ const row = storageAdapter?.toStorageRow
914
+ ? storageAdapter.toStorageRow(attributes)
915
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
923
916
  row[canonical.tenantColumn] = descriptor.tenant
924
917
  row[canonical.resourceColumn] = descriptor.resource
925
918
 
@@ -943,7 +936,9 @@ export const RestApiAnyapiKnexPlugin = {
943
936
  const { canonical } = descriptor
944
937
  const id = context.id
945
938
  const attributes = context.inputRecord?.data?.attributes || {}
946
- const row = translateAttributesForStorage(attributes, descriptor)
939
+ const row = storageAdapter?.toStorageRow
940
+ ? storageAdapter.toStorageRow(attributes)
941
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
947
942
 
948
943
  row[canonical.resourceColumn] = descriptor.resource
949
944
  row[canonical.tenantColumn] = descriptor.tenant
@@ -974,7 +969,9 @@ export const RestApiAnyapiKnexPlugin = {
974
969
  const { canonical } = descriptor
975
970
  const id = context.id
976
971
  const attributes = context.inputRecord?.data?.attributes || {}
977
- const row = translateAttributesForStorage(attributes, descriptor)
972
+ const row = storageAdapter?.toStorageRow
973
+ ? storageAdapter.toStorageRow(attributes)
974
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
978
975
 
979
976
  const updateRow = Object.fromEntries(
980
977
  Object.entries(row).filter(([, value]) => value !== undefined)
@@ -1011,7 +1008,7 @@ export const RestApiAnyapiKnexPlugin = {
1011
1008
  return result
1012
1009
  }
1013
1010
 
1014
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
1011
+ helpers.dataGetMinimal = async ({ scopeName, context, runHooks, applyQueryFilters }) => {
1015
1012
  const storageAdapter = getScopeStorageAdapter(scopeName)
1016
1013
  if (storageAdapter) {
1017
1014
  context.storageAdapter = storageAdapter
@@ -1021,14 +1018,50 @@ export const RestApiAnyapiKnexPlugin = {
1021
1018
  const id = context.id
1022
1019
  const scope = api.resources?.[scopeName]
1023
1020
 
1024
- const row = await context.db(canonical.tableName)
1021
+ let query = context.db(canonical.tableName)
1025
1022
  .where('id', id)
1026
1023
  .where(canonical.resourceColumn, descriptor.resource)
1027
1024
  .where(canonical.tenantColumn, descriptor.tenant)
1028
- .first()
1025
+
1026
+ if (typeof applyQueryFilters === 'function') {
1027
+ const scopedQueryState = await applyQueryFilters({
1028
+ query,
1029
+ filters: context.queryParams?.filters,
1030
+ tableName: canonical.tableName,
1031
+ db: context.db,
1032
+ scopeName,
1033
+ storageAdapter,
1034
+ isAnyApi: true
1035
+ })
1036
+ query = unwrapQueryBuilderState(scopedQueryState, query)
1037
+ } else if (runHooks) {
1038
+ context.knexQuery = {
1039
+ query,
1040
+ filters: context.queryParams?.filters,
1041
+ schemaInfo: context.schemaInfo,
1042
+ scopeName,
1043
+ tableName: canonical.tableName,
1044
+ db: context.db,
1045
+ isAnyApi: true,
1046
+ adapter: storageAdapter,
1047
+ storageAdapter,
1048
+ }
1049
+
1050
+ await runHooks('knexQueryFiltering')
1051
+
1052
+ if (context.knexQuery) {
1053
+ delete context.knexQuery
1054
+ }
1055
+ }
1056
+
1057
+ const row = await query.first()
1029
1058
 
1030
1059
  if (!row) return null
1031
- const translated = translateRecordFromStorage(row, descriptor)
1060
+ const translated = translateCanonicalRecordFromStorage(
1061
+ row,
1062
+ descriptor,
1063
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1064
+ )
1032
1065
  const minimal = {
1033
1066
  type: descriptor.resource,
1034
1067
  id: String(row.id),
@@ -1069,17 +1102,102 @@ export const RestApiAnyapiKnexPlugin = {
1069
1102
  return tree
1070
1103
  }
1071
1104
 
1105
+ const applyIncludeFieldSelection = async ({ query, resourceName, tableName, context }) => {
1106
+ const targetScope = api.resources?.[resourceName]
1107
+ if (!targetScope) {
1108
+ return { query, fieldSelectionInfo: null }
1109
+ }
1110
+
1111
+ const storageAdapter = getScopeStorageAdapter(resourceName)
1112
+ const fieldSelectionInfo = await buildFieldSelection(targetScope, {
1113
+ context: {
1114
+ ...context,
1115
+ scopeName: resourceName,
1116
+ schemaInfo: targetScope.vars.schemaInfo,
1117
+ storageAdapter,
1118
+ }
1119
+ })
1120
+
1121
+ const selectionState = await applyFieldSelectionToQuery({
1122
+ query,
1123
+ scope: targetScope,
1124
+ fieldSelectionInfo,
1125
+ tableName,
1126
+ useTablePrefix: false,
1127
+ storageAdapter,
1128
+ db: context.db || context.transaction || api.knex.instance,
1129
+ context,
1130
+ scopeName: resourceName
1131
+ })
1132
+
1133
+ return {
1134
+ query: selectionState.query,
1135
+ fieldSelectionInfo
1136
+ }
1137
+ }
1138
+
1139
+ const applyTargetScopeFilters = async ({ query, resourceName, tableName, context, filters }) => {
1140
+ const targetScope = api.resources?.[resourceName]
1141
+ if (!targetScope?.applyQueryFilters) {
1142
+ return { query }
1143
+ }
1144
+
1145
+ const storageAdapter = getScopeStorageAdapter(resourceName)
1146
+ const queryState = await targetScope.applyQueryFilters({
1147
+ query,
1148
+ filters,
1149
+ schemaInfo: targetScope.vars?.schemaInfo,
1150
+ scopeName: resourceName,
1151
+ tableName,
1152
+ db: context.db || context.transaction || api.knex.instance,
1153
+ isAnyApi: true,
1154
+ storageAdapter
1155
+ }, {
1156
+ ...context,
1157
+ scopeName: resourceName,
1158
+ schemaInfo: targetScope.vars?.schemaInfo,
1159
+ storageAdapter
1160
+ })
1161
+
1162
+ return {
1163
+ query: unwrapQueryBuilderState(queryState, query)
1164
+ }
1165
+ }
1166
+
1167
+ const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
1168
+ const translated = translateCanonicalRecordFromStorage(
1169
+ row,
1170
+ descriptor,
1171
+ { allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
1172
+ )
1173
+ const includeResource = {
1174
+ type: descriptor.resource,
1175
+ id: String(row.id),
1176
+ attributes: translated.attributes,
1177
+ }
1178
+
1179
+ if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1180
+ includeResource.relationships = translated.relationships
1181
+ }
1182
+
1183
+ if (fieldSelectionInfo?.computedDependencies?.length) {
1184
+ includeResource[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
1185
+ }
1186
+
1187
+ const targetScope = api.resources?.[descriptor.resource]
1188
+ if (targetScope) {
1189
+ includeResource.links = includeResource.links || {}
1190
+ includeResource.links.self = buildResourceUrl(context, targetScope, descriptor.resource, includeResource.id)
1191
+ }
1192
+
1193
+ return includeResource
1194
+ }
1195
+
1072
1196
  const collectIncludes = async ({ descriptor, resources, includeTree, context, includes, seen }) => {
1073
1197
  const entries = Object.entries(includeTree || {})
1074
1198
  if (entries.length === 0 || !resources || resources.length === 0) return
1075
1199
 
1076
1200
  const db = context.db || context.transaction || api.knex.instance
1077
- const addSelfLink = (resource, resourceType) => {
1078
- const targetScope = api.resources?.[resourceType]
1079
- if (!targetScope) return
1080
- resource.links = resource.links || {}
1081
- resource.links.self = buildResourceUrl(context, targetScope, resourceType, resource.id)
1082
- }
1083
1201
 
1084
1202
  for (const [relName, childTree] of entries) {
1085
1203
  const childKeys = Object.keys(childTree || {})
@@ -1095,26 +1213,30 @@ export const RestApiAnyapiKnexPlugin = {
1095
1213
  const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target)
1096
1214
  if (!targetDescriptor) continue
1097
1215
 
1098
- const rows = await db(targetDescriptor.canonical.tableName)
1216
+ let query = db(targetDescriptor.canonical.tableName)
1099
1217
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1100
1218
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1101
1219
  .whereIn('id', ids)
1220
+ const selectionState = await applyIncludeFieldSelection({
1221
+ query,
1222
+ resourceName: targetDescriptor.resource,
1223
+ tableName: targetDescriptor.canonical.tableName,
1224
+ context
1225
+ })
1226
+ query = selectionState.query
1227
+ const rows = await query
1102
1228
 
1103
1229
  const newResources = []
1104
1230
  for (const row of rows) {
1105
1231
  const includeKey = `${targetDescriptor.resource}:${row.id}`
1106
1232
  if (seen.has(includeKey)) continue
1107
1233
  seen.add(includeKey)
1108
- const translated = translateRecordFromStorage(row, targetDescriptor)
1109
- const includeResource = {
1110
- type: targetDescriptor.resource,
1111
- id: String(row.id),
1112
- attributes: translated.attributes,
1113
- }
1114
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1115
- includeResource.relationships = translated.relationships
1116
- }
1117
- addSelfLink(includeResource, targetDescriptor.resource)
1234
+ const includeResource = buildIncludedResource({
1235
+ row,
1236
+ descriptor: targetDescriptor,
1237
+ context,
1238
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1239
+ })
1118
1240
  includes.push(includeResource)
1119
1241
  newResources.push(includeResource)
1120
1242
  }
@@ -1159,25 +1281,29 @@ export const RestApiAnyapiKnexPlugin = {
1159
1281
  continue
1160
1282
  }
1161
1283
 
1162
- const rows = await db(targetDescriptor.canonical.tableName)
1284
+ let query = db(targetDescriptor.canonical.tableName)
1163
1285
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1164
1286
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1165
1287
  .whereIn('id', [...idSet])
1288
+ const selectionState = await applyIncludeFieldSelection({
1289
+ query,
1290
+ resourceName: targetDescriptor.resource,
1291
+ tableName: targetDescriptor.canonical.tableName,
1292
+ context
1293
+ })
1294
+ query = selectionState.query
1295
+ const rows = await query
1166
1296
 
1167
1297
  for (const row of rows) {
1168
1298
  const includeKey = `${targetDescriptor.resource}:${row.id}`
1169
1299
  if (seen.has(includeKey)) continue
1170
1300
  seen.add(includeKey)
1171
- const translated = translateRecordFromStorage(row, targetDescriptor)
1172
- const includeResource = {
1173
- type: targetDescriptor.resource,
1174
- id: String(row.id),
1175
- attributes: translated.attributes,
1176
- }
1177
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1178
- includeResource.relationships = translated.relationships
1179
- }
1180
- addSelfLink(includeResource, targetDescriptor.resource)
1301
+ const includeResource = buildIncludedResource({
1302
+ row,
1303
+ descriptor: targetDescriptor,
1304
+ context,
1305
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1306
+ })
1181
1307
  includes.push(includeResource)
1182
1308
  newResources.push(includeResource)
1183
1309
  }
@@ -1243,6 +1369,7 @@ export const RestApiAnyapiKnexPlugin = {
1243
1369
 
1244
1370
  let rows = []
1245
1371
  let groupingColumn = null
1372
+ let fieldSelectionInfo = null
1246
1373
 
1247
1374
  if (hasManyInfo.foreignKey) {
1248
1375
  const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey]
@@ -1250,21 +1377,53 @@ export const RestApiAnyapiKnexPlugin = {
1250
1377
  continue
1251
1378
  }
1252
1379
  groupingColumn = foreignField.slot
1253
- rows = await db(targetDescriptor.canonical.tableName)
1380
+ let query = db(targetDescriptor.canonical.tableName)
1254
1381
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1255
1382
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1256
1383
  .whereIn(groupingColumn, parentIds)
1384
+ const selectionState = await applyIncludeFieldSelection({
1385
+ query,
1386
+ resourceName: targetDescriptor.resource,
1387
+ tableName: targetDescriptor.canonical.tableName,
1388
+ context
1389
+ })
1390
+ query = selectionState.query
1391
+ const scopedQueryState = await applyTargetScopeFilters({
1392
+ query,
1393
+ resourceName: targetDescriptor.resource,
1394
+ tableName: targetDescriptor.canonical.tableName,
1395
+ context
1396
+ })
1397
+ query = scopedQueryState.query
1398
+ fieldSelectionInfo = selectionState.fieldSelectionInfo
1399
+ rows = await query
1257
1400
  } else if (hasManyInfo.via) {
1258
1401
  const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via]
1259
1402
  if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
1260
1403
  continue
1261
1404
  }
1262
1405
  groupingColumn = polyInfo.idColumn
1263
- rows = await db(targetDescriptor.canonical.tableName)
1406
+ let query = db(targetDescriptor.canonical.tableName)
1264
1407
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1265
1408
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1266
1409
  .where(polyInfo.typeColumn, descriptor.resource)
1267
1410
  .whereIn(groupingColumn, queryIds)
1411
+ const selectionState = await applyIncludeFieldSelection({
1412
+ query,
1413
+ resourceName: targetDescriptor.resource,
1414
+ tableName: targetDescriptor.canonical.tableName,
1415
+ context
1416
+ })
1417
+ query = selectionState.query
1418
+ const scopedQueryState = await applyTargetScopeFilters({
1419
+ query,
1420
+ resourceName: targetDescriptor.resource,
1421
+ tableName: targetDescriptor.canonical.tableName,
1422
+ context
1423
+ })
1424
+ query = scopedQueryState.query
1425
+ fieldSelectionInfo = selectionState.fieldSelectionInfo
1426
+ rows = await query
1268
1427
  }
1269
1428
 
1270
1429
  if (!groupingColumn || rows.length === 0) {
@@ -1276,16 +1435,12 @@ export const RestApiAnyapiKnexPlugin = {
1276
1435
  const includeKey = `${targetDescriptor.resource}:${row.id}`
1277
1436
  if (seen.has(includeKey)) continue
1278
1437
  seen.add(includeKey)
1279
- const translated = translateRecordFromStorage(row, targetDescriptor)
1280
- const includeResource = {
1281
- type: targetDescriptor.resource,
1282
- id: String(row.id),
1283
- attributes: translated.attributes,
1284
- }
1285
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1286
- includeResource.relationships = translated.relationships
1287
- }
1288
- addSelfLink(includeResource, targetDescriptor.resource)
1438
+ const includeResource = buildIncludedResource({
1439
+ row,
1440
+ descriptor: targetDescriptor,
1441
+ context,
1442
+ fieldSelectionInfo
1443
+ })
1289
1444
  includes.push(includeResource)
1290
1445
  newResources.push(includeResource)
1291
1446
  }
@@ -1323,26 +1478,30 @@ export const RestApiAnyapiKnexPlugin = {
1323
1478
  const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target)
1324
1479
  if (!targetDescriptor) continue
1325
1480
 
1326
- const rows = await db(targetDescriptor.canonical.tableName)
1481
+ let query = db(targetDescriptor.canonical.tableName)
1327
1482
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1328
1483
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1329
1484
  .whereIn('id', childIds)
1485
+ const selectionState = await applyIncludeFieldSelection({
1486
+ query,
1487
+ resourceName: targetDescriptor.resource,
1488
+ tableName: targetDescriptor.canonical.tableName,
1489
+ context
1490
+ })
1491
+ query = selectionState.query
1492
+ const rows = await query
1330
1493
 
1331
1494
  const newResources = []
1332
1495
  for (const row of rows) {
1333
1496
  const includeKey = `${targetDescriptor.resource}:${row.id}`
1334
1497
  if (seen.has(includeKey)) continue
1335
1498
  seen.add(includeKey)
1336
- const translated = translateRecordFromStorage(row, targetDescriptor)
1337
- const includeResource = {
1338
- type: targetDescriptor.resource,
1339
- id: String(row.id),
1340
- attributes: translated.attributes,
1341
- }
1342
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1343
- includeResource.relationships = translated.relationships
1344
- }
1345
- addSelfLink(includeResource, targetDescriptor.resource)
1499
+ const includeResource = buildIncludedResource({
1500
+ row,
1501
+ descriptor: targetDescriptor,
1502
+ context,
1503
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1504
+ })
1346
1505
  includes.push(includeResource)
1347
1506
  newResources.push(includeResource)
1348
1507
  }
@@ -1410,10 +1569,18 @@ export const RestApiAnyapiKnexPlugin = {
1410
1569
  if (!foreignField?.slot) continue
1411
1570
  const column = foreignField.slot
1412
1571
 
1413
- const rows = await db(targetDescriptor.canonical.tableName)
1572
+ let query = db(targetDescriptor.canonical.tableName)
1414
1573
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1415
1574
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1416
1575
  .whereIn(column, normalizedParentIds)
1576
+ const scopedQueryState = await applyTargetScopeFilters({
1577
+ query,
1578
+ resourceName: targetDescriptor.resource,
1579
+ tableName: targetDescriptor.canonical.tableName,
1580
+ context
1581
+ })
1582
+ query = scopedQueryState.query
1583
+ const rows = await query
1417
1584
 
1418
1585
  const grouped = rows.reduce((acc, row) => {
1419
1586
  const parentId = row[column]
@@ -1447,11 +1614,19 @@ export const RestApiAnyapiKnexPlugin = {
1447
1614
  .filter((value) => value !== null),
1448
1615
  ]))
1449
1616
 
1450
- const rows = await db(targetDescriptor.canonical.tableName)
1617
+ let query = db(targetDescriptor.canonical.tableName)
1451
1618
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1452
1619
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1453
1620
  .where(polyInfo.typeColumn, descriptor.resource)
1454
1621
  .whereIn(polyInfo.idColumn, queryIds)
1622
+ const scopedQueryState = await applyTargetScopeFilters({
1623
+ query,
1624
+ resourceName: targetDescriptor.resource,
1625
+ tableName: targetDescriptor.canonical.tableName,
1626
+ context
1627
+ })
1628
+ query = scopedQueryState.query
1629
+ const rows = await query
1455
1630
 
1456
1631
  const grouped = rows.reduce((acc, row) => {
1457
1632
  const parentId = row[polyInfo.idColumn]
@@ -1522,15 +1697,35 @@ export const RestApiAnyapiKnexPlugin = {
1522
1697
  context.computedDependencies = []
1523
1698
  }
1524
1699
 
1525
- const row = await context.db(canonical.tableName)
1700
+ let query = context.db(canonical.tableName)
1526
1701
  .where('id', id)
1527
1702
  .where(canonical.resourceColumn, descriptor.resource)
1528
1703
  .where(canonical.tenantColumn, descriptor.tenant)
1529
- .first()
1704
+
1705
+ if (scope && fieldSelectionInfo) {
1706
+ const selectionState = await applyFieldSelectionToQuery({
1707
+ query,
1708
+ scope,
1709
+ fieldSelectionInfo,
1710
+ tableName: canonical.tableName,
1711
+ useTablePrefix: false,
1712
+ storageAdapter,
1713
+ db: context.db,
1714
+ context,
1715
+ scopeName
1716
+ })
1717
+ query = selectionState.query
1718
+ }
1719
+
1720
+ const row = await query.first()
1530
1721
 
1531
1722
  if (!row) return null
1532
1723
 
1533
- const record = translateRecordFromStorage(row, descriptor)
1724
+ const record = translateCanonicalRecordFromStorage(
1725
+ row,
1726
+ descriptor,
1727
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1728
+ )
1534
1729
  const data = {
1535
1730
  type: descriptor.resource,
1536
1731
  id: String(row.id),
@@ -1592,10 +1787,11 @@ export const RestApiAnyapiKnexPlugin = {
1592
1787
  const rehydrateSchemaInfo = async (scopeName) => {
1593
1788
  const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {}
1594
1789
  const storedOptions = scopeOptionsRegistry.get(scopeName) || {}
1790
+ const rawSchema = scope.scopeOptions?.schema || storedOptions.schema || {}
1595
1791
 
1596
1792
  if (!scope.scopeOptions) {
1597
1793
  scope.scopeOptions = {
1598
- schema: storedOptions.schema || {},
1794
+ schema: rawSchema,
1599
1795
  relationships: storedOptions.relationships || {},
1600
1796
  searchSchema: storedOptions.searchSchema || null,
1601
1797
  tableName: storedOptions.tableName || scopeName,
@@ -1611,7 +1807,6 @@ export const RestApiAnyapiKnexPlugin = {
1611
1807
 
1612
1808
  const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) }
1613
1809
  const computed = { ...(existingInfo.computed || {}) }
1614
-
1615
1810
  for (const [fieldName, definition] of Object.entries(descriptor.schema || {})) {
1616
1811
  if (definition?.computed) {
1617
1812
  if (!computed[fieldName]) {
@@ -1699,26 +1894,6 @@ export const RestApiAnyapiKnexPlugin = {
1699
1894
  return { resourceToTableName, tableNameToResource }
1700
1895
  }
1701
1896
 
1702
- const translateSelectFieldsForAdapter = (fields, adapter) => {
1703
- if (!adapter || !fields) return fields
1704
-
1705
- return fields.map((field) => {
1706
- if (typeof field !== 'string') return field
1707
-
1708
- if (field === '*') return '*'
1709
-
1710
- const aliasMatch = field.match(/\s+as\s+/i)
1711
- if (aliasMatch) {
1712
- const [source, alias] = field.split(/\s+as\s+/i)
1713
- const translatedSource = adapter.translateColumn(source.trim()) || source.trim()
1714
- return `${translatedSource} as ${alias.trim()}`
1715
- }
1716
-
1717
- const translated = adapter.translateColumn(field)
1718
- return translated || field
1719
- })
1720
- }
1721
-
1722
1897
  const scopesProxy = new Proxy({}, {
1723
1898
  get (_target, prop) {
1724
1899
  if (typeof prop !== 'string') {
@@ -1809,11 +1984,26 @@ export const RestApiAnyapiKnexPlugin = {
1809
1984
 
1810
1985
  const countQuery = queryBuilder.clone()
1811
1986
 
1987
+ const selectionState = scope && fieldSelectionInfo
1988
+ ? await applyFieldSelectionToQuery({
1989
+ query: queryBuilder,
1990
+ scope,
1991
+ fieldSelectionInfo,
1992
+ tableName: adapter.tableAlias,
1993
+ useTablePrefix: false,
1994
+ storageAdapter,
1995
+ db,
1996
+ context,
1997
+ scopeName
1998
+ })
1999
+ : { query: queryBuilder, queryFieldRuntimeByField: new Map() }
2000
+
1812
2001
  const sortDescriptors = applySortingToQuery({
1813
- query: queryBuilder,
2002
+ query: selectionState.query,
1814
2003
  sort: queryParams.sort,
1815
2004
  descriptor,
1816
2005
  scope,
2006
+ queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
1817
2007
  })
1818
2008
 
1819
2009
  if (queryParams?.page?.after) {
@@ -1827,14 +2017,6 @@ export const RestApiAnyapiKnexPlugin = {
1827
2017
  adapter,
1828
2018
  })
1829
2019
 
1830
- if (queryParams?.page?.after) {
1831
- }
1832
-
1833
- if (fieldSelectionInfo && fieldSelectionInfo.fieldsToSelect && fieldSelectionInfo.fieldsToSelect.length > 0) {
1834
- const translatedSelect = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, adapter)
1835
- queryBuilder.select(translatedSelect)
1836
- }
1837
-
1838
2020
  let rows = await queryBuilder
1839
2021
  let cursorRecords = null
1840
2022
  let hasMore = false
@@ -1846,6 +2028,8 @@ export const RestApiAnyapiKnexPlugin = {
1846
2028
  for (const descriptorEntry of cursorDescriptors) {
1847
2029
  if (descriptorEntry.field === 'id') {
1848
2030
  record.id = row.id
2031
+ } else if (descriptorEntry.queryFieldRuntime) {
2032
+ record[descriptorEntry.field] = row[descriptorEntry.field]
1849
2033
  } else if (descriptorEntry.column) {
1850
2034
  record[descriptorEntry.field] = row[descriptorEntry.column]
1851
2035
  }
@@ -1861,7 +2045,11 @@ export const RestApiAnyapiKnexPlugin = {
1861
2045
  }
1862
2046
 
1863
2047
  const data = rows.map((row) => {
1864
- const translated = translateRecordFromStorage(row, descriptor)
2048
+ const translated = translateCanonicalRecordFromStorage(
2049
+ row,
2050
+ descriptor,
2051
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
2052
+ )
1865
2053
  const resource = {
1866
2054
  type: descriptor.resource,
1867
2055
  id: String(row.id),
@@ -2194,64 +2382,3 @@ export const RestApiAnyapiKnexPlugin = {
2194
2382
  })
2195
2383
  },
2196
2384
  }
2197
-
2198
- const translateAttributesForStorage = (attributes, descriptor) => {
2199
- const row = {}
2200
- for (const [fieldName, value] of Object.entries(attributes)) {
2201
- const slot = descriptor.fields[fieldName]
2202
- if (!slot) continue
2203
-
2204
- if (slot.slotType === 'belongsTo') {
2205
- row[slot.slot] = value == null ? null : String(value)
2206
- } else {
2207
- row[slot.slot] = value
2208
- }
2209
-
2210
- if (slot.slotType === 'belongsTo') {
2211
- const alias = slot.alias || descriptor.fields[fieldName]?.alias
2212
- const belongsToInfo = alias ? descriptor.belongsTo?.[alias] : null
2213
- if (belongsToInfo) {
2214
- row[belongsToInfo.typeColumn] = value == null ? null : belongsToInfo.target
2215
- }
2216
- }
2217
- }
2218
- return row
2219
- }
2220
-
2221
- const translateRecordFromStorage = (row, descriptor) => {
2222
- const attributes = {}
2223
- for (const [slot, logical] of Object.entries(descriptor.reverseAttributes)) {
2224
- if (slot in row) {
2225
- attributes[logical] = row[slot]
2226
- }
2227
- }
2228
-
2229
- const relationships = {}
2230
- for (const [alias, info] of Object.entries(descriptor.belongsTo || {})) {
2231
- const idValue = row[info.idColumn]
2232
- const relationshipData = idValue == null
2233
- ? null
2234
- : { type: info.target, id: String(idValue) }
2235
-
2236
- relationships[alias] = { data: relationshipData }
2237
- }
2238
-
2239
- for (const [alias, info] of Object.entries(descriptor.polymorphicBelongsTo || {})) {
2240
- const typeValue = info.typeColumn ? row[info.typeColumn] : null
2241
- const idValue = info.idColumn ? row[info.idColumn] : null
2242
-
2243
- let relationshipData = null
2244
- if (typeValue != null && idValue != null) {
2245
- relationshipData = {
2246
- type: String(typeValue),
2247
- id: String(idValue),
2248
- }
2249
- } else if (typeValue == null && idValue == null) {
2250
- relationshipData = null
2251
- }
2252
-
2253
- relationships[alias] = { data: relationshipData }
2254
- }
2255
-
2256
- return { attributes, relationships }
2257
- }