json-rest-api 1.0.22 → 1.0.24

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 (290) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/README.md +22 -1
  4. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  5. package/docs/API.md +196 -38
  6. package/docs/COMPARISON.md +2 -2
  7. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  9. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  10. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  11. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  12. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  13. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  14. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  15. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  16. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
  17. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  18. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  19. package/docs/GUIDE/index.md +18 -9
  20. package/docs/QUICKSTART.md +5 -1
  21. package/docs/index.md +1 -1
  22. package/index.js +3 -2
  23. package/lib/enhanced-logger.js +1 -1
  24. package/package.json +6 -2
  25. package/plugins/core/bulk-operations-plugin.js +26 -34
  26. package/plugins/core/connectors/express-plugin.js +97 -190
  27. package/plugins/core/connectors/fastify-plugin.js +206 -0
  28. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  29. package/plugins/core/connectors/lib/connector-core.js +195 -0
  30. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  31. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  32. package/plugins/core/lib/anyapi/schema-utils.js +43 -4
  33. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
  34. package/plugins/core/lib/dbIntrospection.js +1078 -0
  35. package/plugins/core/lib/dbTablesOperations.js +1111 -160
  36. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  37. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
  38. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  39. package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
  40. package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
  41. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  42. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  43. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  44. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  45. package/plugins/core/lib/querying/url-helpers.js +8 -2
  46. package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
  47. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
  48. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  49. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  50. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  51. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  52. package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
  53. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  54. package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
  55. package/plugins/core/lib/storage/storage-adapter.js +82 -4
  56. package/plugins/core/lib/storage/storage-mapping.js +274 -0
  57. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
  58. package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
  59. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  60. package/plugins/core/rest-api-knex-plugin.js +271 -226
  61. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  62. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  63. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
  64. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  65. package/plugins/core/rest-api-plugin-methods/common.js +148 -92
  66. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
  67. package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
  68. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  69. package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
  70. package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
  71. package/plugins/core/rest-api-plugin-methods/get.js +29 -14
  72. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
  73. package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
  74. package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
  75. package/plugins/core/rest-api-plugin-methods/post.js +21 -8
  76. package/plugins/core/rest-api-plugin-methods/put.js +73 -21
  77. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  78. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  79. package/plugins/core/rest-api-plugin.js +6 -0
  80. package/plugins/core/rest-api-positioning-plugin.js +2 -5
  81. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  82. package/plugins/core/socketio-plugin.js +1 -4
  83. package/plugins/storage/local-storage.js +1 -1
  84. package/plugins/storage/s3-storage.js +5 -2
  85. package/quickTest.js +1 -0
  86. package/scripts/run-docs.js +80 -84
  87. package/test-debug.js +1 -1
  88. package/tests/README.md +7 -0
  89. package/tests/anyapi-basic.test.js +3 -0
  90. package/tests/autofilter.test.js +463 -0
  91. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  92. package/tests/custom-id-simplified-post.test.js +75 -0
  93. package/tests/custom-idproperty-relationships.test.js +33 -0
  94. package/tests/db-introspection.test.js +521 -0
  95. package/tests/db-table-operations.test.js +786 -0
  96. package/tests/fastify-plugin.test.js +156 -0
  97. package/tests/field-getters.test.js +1 -2
  98. package/tests/field-setters.test.js +0 -4
  99. package/tests/fixtures/api-configs.js +278 -176
  100. package/tests/helpers/fake-fastify.js +99 -0
  101. package/tests/http-connectors-parity.test.js +235 -0
  102. package/tests/id-normalization.test.js +444 -0
  103. package/tests/logical-id-write-validation.test.js +151 -0
  104. package/tests/pagination-cursor-multifield.test.js +11 -11
  105. package/tests/projected-fields.test.js +319 -0
  106. package/tests/relationships.test.js +3 -3
  107. package/tests/searchschema-knex-behavior.test.js +187 -0
  108. package/tests/searchschema-merge.test.js +21 -69
  109. package/tests/storage-mapping.test.js +259 -0
  110. package/audit/00-charter.md +0 -103
  111. package/audit/01-scope-map.md +0 -84
  112. package/audit/02-coverage-matrix.csv +0 -174
  113. package/audit/03-invariants.md +0 -123
  114. package/audit/04-standards-rubric.md +0 -56
  115. package/audit/05-competitor-rubric.md +0 -77
  116. package/audit/06-findings.md +0 -160
  117. package/audit/07-risk-register.csv +0 -10
  118. package/audit/08-remediation-roadmap.md +0 -177
  119. package/audit/09-signoff-check.md +0 -23
  120. package/audit/10-final-synthesis.md +0 -29
  121. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  122. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  123. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  157. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  158. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  159. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  160. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  161. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  162. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  163. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  164. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  165. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  166. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  167. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  168. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  169. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  170. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  171. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  172. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  173. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  174. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  175. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  176. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  177. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  178. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  179. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  180. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  181. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  182. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  183. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  184. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  185. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  186. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  187. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  188. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  189. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  190. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  191. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  192. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  193. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  194. package/audit/evidence/code/error-model-signals.txt +0 -98
  195. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  196. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  197. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  198. package/audit/evidence/code/idproperty-signals.txt +0 -157
  199. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  200. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  201. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  202. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  203. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  204. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  205. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  206. package/audit/evidence/code/permission-signals.txt +0 -193
  207. package/audit/evidence/code/query-include-signals.txt +0 -687
  208. package/audit/evidence/code/transaction-signals.txt +0 -28
  209. package/audit/evidence/code/validation-signals.txt +0 -650
  210. package/audit/evidence/code/write-path-signals.txt +0 -61
  211. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  212. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  213. package/audit/evidence/external/corpus-freeze.md +0 -21
  214. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  215. package/audit/evidence/external/npm-download-signals.txt +0 -23
  216. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  217. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  218. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  219. package/audit/evidence/external/readme-fortune.md +0 -165
  220. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  221. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  222. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  223. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  224. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  225. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  226. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  227. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  228. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  229. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  230. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  231. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  232. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  233. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  234. package/audit/evidence/external/standards-sources.txt +0 -6
  235. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  236. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  237. package/audit/evidence/runtime/environment.txt +0 -4
  238. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  239. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  240. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  241. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  242. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  243. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  244. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  245. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  246. package/audit/evidence/runtime/test-summary.md +0 -107
  247. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  248. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  249. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  250. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  251. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  252. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  253. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  254. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  255. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  256. package/audit/evidence/tests/skipped-tests.txt +0 -10
  257. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  258. package/audit/evidence/tests/test-suite-list.txt +0 -32
  259. package/audit/subsystems/docs-drift.md +0 -49
  260. package/audit/subsystems/pass2-cross-review.md +0 -131
  261. package/audit/subsystems/plugins-access.md +0 -51
  262. package/audit/subsystems/plugins-bulk.md +0 -50
  263. package/audit/subsystems/plugins-cors.md +0 -48
  264. package/audit/subsystems/plugins-file.md +0 -53
  265. package/audit/subsystems/plugins-multihome.md +0 -50
  266. package/audit/subsystems/plugins-positioning.md +0 -54
  267. package/audit/subsystems/plugins-socketio.md +0 -50
  268. package/audit/subsystems/query-engine.md +0 -61
  269. package/audit/subsystems/relationships.md +0 -55
  270. package/audit/subsystems/rest-orchestration.md +0 -66
  271. package/audit/subsystems/schema-core.md +0 -64
  272. package/audit/subsystems/storage-knex.md +0 -57
  273. package/audit/subsystems/tests.md +0 -48
  274. package/audit/subsystems/transport-express.md +0 -55
  275. package/audit/traces/trace-read-get.md +0 -22
  276. package/audit/traces/trace-read-query.md +0 -25
  277. package/audit/traces/trace-relationship-endpoints.md +0 -31
  278. package/audit/traces/trace-write-delete.md +0 -17
  279. package/audit/traces/trace-write-patch.md +0 -26
  280. package/audit/traces/trace-write-post.md +0 -22
  281. package/audit/traces/trace-write-put.md +0 -23
  282. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  283. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  284. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  285. package/plugins/core/rest-api-access.js +0 -459
  286. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  287. package/tests/multihome.test.js +0 -553
  288. package/tests/multihome.test.js.bak +0 -550
  289. package/tests/ownership-optout.test.js +0 -76
  290. 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,
@@ -13,7 +14,7 @@ import {
13
14
  buildCursorMeta,
14
15
  parseCursor,
15
16
  } from './lib/querying/knex-pagination-helpers.js'
16
- import { getUrlPrefix, buildResourceUrl } from './lib/querying/url-helpers.js'
17
+ import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './lib/querying/url-helpers.js'
17
18
  import {
18
19
  normalizeId,
19
20
  resolveFieldInfo,
@@ -29,13 +30,30 @@ 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,
38
+ resolveSearchOperator,
39
+ applyWhereForOperator,
37
40
  } from './lib/querying/knex-query-helpers.js'
38
- import { createStorageAdapter } from './lib/storage/storage-adapter.js'
41
+ import {
42
+ createStorageAdapter,
43
+ } from './lib/storage/storage-adapter.js'
44
+ import {
45
+ translateCanonicalAttributesForStorage,
46
+ getCanonicalResourceId,
47
+ getCanonicalResourceIdColumn,
48
+ translateCanonicalRecordFromStorage,
49
+ } from './lib/storage/canonical-storage-mapping.js'
50
+ import {
51
+ buildCursorPredicateChains,
52
+ applyQueryFieldOrder,
53
+ buildEffectiveSortList,
54
+ parseSortEntry
55
+ } from './lib/querying/query-field-sort-helpers.js'
56
+ import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
39
57
 
40
58
  const DEFAULT_TENANT = 'default'
41
59
  const LINKS_TABLE = 'any_links'
@@ -107,76 +125,138 @@ export const RestApiAnyapiKnexPlugin = {
107
125
  return descriptor
108
126
  }
109
127
 
110
- const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
111
- // Filtering is handled by the query hooks to keep behavior consistent
112
- // (operators, null handling, multi-field, joins, polymorphic, etc.).
113
- // Avoid applying filters here to prevent duplicate conditions.
114
- if (searchSchema) {
128
+ const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
129
+ if (!resource || !scope || !scopeName || !context) return
130
+
131
+ resource.links = resource.links || {}
132
+ resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
133
+
134
+ if (!resource.relationships || typeof resource.relationships !== 'object') {
115
135
  return
116
136
  }
117
137
 
118
- // TODO: Probably delete the rest of the function. searchSchema is always there...!!!
138
+ for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
139
+ if (!relationshipObject || typeof relationshipObject !== 'object') {
140
+ continue
141
+ }
142
+
143
+ relationshipObject.links = {
144
+ self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
145
+ related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
146
+ }
147
+ }
148
+ }
149
+
150
+ const getAllowedQueryFieldNames = (scopeName) => (
151
+ Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
152
+ )
153
+
154
+ const getLogicalResourceIdColumn = (descriptor) => getCanonicalResourceIdColumn(descriptor)
155
+
156
+ const getLogicalResourceId = (row, descriptor) => getCanonicalResourceId(row, descriptor)
157
+
158
+ const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
119
159
  if (!filters || Object.keys(filters).length === 0) return
120
160
 
121
- // Fallback: apply minimal safe filtering when no search schema is defined
122
- for (const [field, rawValue] of Object.entries(filters)) {
123
- const fieldInfo = ensureFilterableField(descriptor, field)
161
+ const normalizeValue = (field, value) => {
162
+ if (!adapter?.translateFilterValue) {
163
+ return value
164
+ }
165
+ if (Array.isArray(value)) {
166
+ return value.map((entry) => adapter.translateFilterValue(field, entry))
167
+ }
168
+ return adapter.translateFilterValue(field, value)
169
+ }
170
+
171
+ for (const [filterKey, rawValue] of Object.entries(filters)) {
172
+ const fieldDef = searchSchema?.[filterKey]
173
+
174
+ if (fieldDef?.polymorphicField) {
175
+ continue
176
+ }
177
+
178
+ if (fieldDef?.actualField?.includes('.')) {
179
+ continue
180
+ }
181
+
182
+ if (fieldDef?.oneOf?.some((field) => field.includes('.'))) {
183
+ continue
184
+ }
185
+
186
+ if (fieldDef?.oneOf && Array.isArray(fieldDef.oneOf)) {
187
+ continue
188
+ }
189
+
190
+ if (fieldDef?.applyFilter && typeof fieldDef.applyFilter === 'function') {
191
+ continue
192
+ }
193
+
194
+ const actualField = fieldDef?.actualField || filterKey
195
+ const fieldInfo = ensureFilterableField(descriptor, filterKey)
196
+ const columnRef = adapter ? adapter.translateColumn(actualField) : fieldInfo.column
197
+ const operator = resolveSearchOperator(fieldDef || fieldInfo.definition)
124
198
  const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
125
199
  isRelationship: fieldInfo.isRelationship,
126
200
  })
127
-
128
- const nonNullValues = values.filter((value) => value !== null)
129
- const hasNull = values.length !== nonNullValues.length
201
+ const normalizedValues = values.map((value) => normalizeValue(actualField, value))
202
+ const nonNullValues = normalizedValues.filter((value) => value !== null)
203
+ const hasNull = normalizedValues.length !== nonNullValues.length
130
204
 
131
205
  if (nonNullValues.length === 0 && hasNull) {
132
- query.whereNull(fieldInfo.column)
206
+ query.whereNull(columnRef)
133
207
  continue
134
208
  }
135
209
  if (nonNullValues.length === 1 && !hasNull) {
136
- query.where(fieldInfo.column, nonNullValues[0])
210
+ applyWhereForOperator({
211
+ builder: query,
212
+ columnRef,
213
+ operator,
214
+ value: nonNullValues[0],
215
+ knex,
216
+ })
137
217
  continue
138
218
  }
139
219
  if (nonNullValues.length > 1 && !hasNull) {
140
- query.whereIn(fieldInfo.column, nonNullValues)
220
+ query.whereIn(columnRef, nonNullValues)
141
221
  continue
142
222
  }
143
223
  if (nonNullValues.length > 0 && hasNull) {
144
- query.where(function filterGroup () {
145
- this.whereIn(fieldInfo.column, nonNullValues).orWhereNull(fieldInfo.column)
224
+ query.where(function anyApiNullFilterGroup () {
225
+ this.whereIn(columnRef, nonNullValues).orWhereNull(columnRef)
146
226
  })
147
227
  }
148
228
  }
149
229
  }
150
230
 
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']
231
+ const applySortingToQuery = ({ query, sort, descriptor, scope, queryFieldRuntimeByField = new Map() }) => {
232
+ const effectiveSort = buildEffectiveSortList(sort, {
233
+ defaultSort: scope?.vars?.defaultSort,
234
+ idField: 'id'
235
+ })
168
236
  const descriptors = []
169
237
 
170
238
  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
239
+ const { field, direction, sqlDirection } = parseSortEntry(entry)
240
+ const queryFieldRuntime = queryFieldRuntimeByField.get(field)
241
+ if (queryFieldRuntime) {
242
+ applyQueryFieldOrder(query, queryFieldRuntime, sqlDirection)
243
+ descriptors.push({
244
+ field,
245
+ direction,
246
+ definition: queryFieldRuntime.definition,
247
+ isRelationship: false,
248
+ queryFieldRuntime,
249
+ })
250
+ continue
251
+ }
252
+
173
253
  const fieldInfo = resolveFieldInfo(descriptor, field)
174
254
  if (!fieldInfo?.column) continue
175
- query.orderBy(fieldInfo.column, desc ? 'desc' : 'asc')
255
+ query.orderBy(fieldInfo.column, direction)
176
256
  descriptors.push({
177
257
  field,
178
258
  column: fieldInfo.column,
179
- direction: desc ? 'desc' : 'asc',
259
+ direction,
180
260
  definition: fieldInfo.definition || null,
181
261
  isRelationship: fieldInfo.isRelationship || false,
182
262
  })
@@ -282,64 +362,37 @@ export const RestApiAnyapiKnexPlugin = {
282
362
  return typed
283
363
  }
284
364
 
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
- })
365
+ const applyRawCursorPredicate = (cursorValues, operatorSelector) => {
366
+ const predicateChains = buildCursorPredicateChains(
367
+ descriptors,
368
+ cursorValues,
369
+ operatorSelector,
370
+ { onMissingValue: () => {} }
371
+ )
324
372
 
325
- if (translatedConditions.length === 0) {
373
+ if (predicateChains.length === 0) {
326
374
  return
327
375
  }
328
376
 
329
377
  const predicateParts = []
330
378
  const predicateBindings = []
331
- translatedConditions.forEach((chain) => {
332
- const chainParts = chain.map(({ column, operator, value }) => {
379
+
380
+ predicateChains.forEach((chain) => {
381
+ const chainParts = chain.map(({ descriptor, operator, value }) => {
382
+ if (descriptor.queryFieldRuntime) {
383
+ predicateBindings.push(...(descriptor.queryFieldRuntime.bindings || []), value)
384
+ return `(${descriptor.queryFieldRuntime.sql}) ${operator} ?`
385
+ }
386
+
387
+ const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
333
388
  predicateBindings.push(value)
334
- return `${column} ${operator} ?`
389
+ return `${columnRef} ${operator} ?`
335
390
  })
391
+
336
392
  predicateParts.push(`(${chainParts.join(' AND ')})`)
337
393
  })
338
394
 
339
- if (predicateParts.length > 0) {
340
- const combined = predicateParts.join(' OR ')
341
- query.whereRaw(`(${combined})`, predicateBindings)
342
- }
395
+ query.whereRaw(`(${predicateParts.join(' OR ')})`, predicateBindings)
343
396
  }
344
397
 
345
398
  const ensurePageSize = () => {
@@ -378,11 +431,11 @@ export const RestApiAnyapiKnexPlugin = {
378
431
  if (pageParams.after) {
379
432
  const cursorMap = parseCursorOrThrow(pageParams.after, 'after')
380
433
  const cursorValues = coerceCursorValues(cursorMap)
381
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
434
+ applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
382
435
  } else if (pageParams.before) {
383
436
  const cursorMap = parseCursorOrThrow(pageParams.before, 'before')
384
437
  const cursorValues = coerceCursorValues(cursorMap)
385
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
438
+ applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
386
439
  }
387
440
 
388
441
  return {
@@ -900,12 +953,13 @@ export const RestApiAnyapiKnexPlugin = {
900
953
  const descriptor = await getDescriptor(scopeName)
901
954
  const { canonical } = descriptor
902
955
  const id = context.id
956
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
903
957
 
904
958
  const row = await context.db(canonical.tableName)
905
959
  .select('id')
906
960
  .where(canonical.tenantColumn, descriptor.tenant)
907
961
  .where(canonical.resourceColumn, descriptor.resource)
908
- .where('id', id)
962
+ .where(logicalIdColumn, id)
909
963
  .first()
910
964
 
911
965
  return !!row
@@ -918,8 +972,17 @@ export const RestApiAnyapiKnexPlugin = {
918
972
  }
919
973
  const descriptor = await getDescriptor(scopeName)
920
974
  const { canonical } = descriptor
975
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
976
+ const explicitId = context.inputRecord?.data?.id
921
977
  const attributes = context.inputRecord?.data?.attributes || {}
922
- const row = translateAttributesForStorage(attributes, descriptor)
978
+ const row = storageAdapter?.toStorageRow
979
+ ? storageAdapter.toStorageRow(attributes)
980
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
981
+
982
+ if (explicitId !== undefined && explicitId !== null) {
983
+ row[logicalIdColumn] = explicitId
984
+ }
985
+
923
986
  row[canonical.tenantColumn] = descriptor.tenant
924
987
  row[canonical.resourceColumn] = descriptor.resource
925
988
 
@@ -928,10 +991,25 @@ export const RestApiAnyapiKnexPlugin = {
928
991
  .returning('id')
929
992
 
930
993
  const inserted = Array.isArray(result) ? result[0] : result
931
- if (inserted && typeof inserted === 'object' && 'id' in inserted) {
932
- return inserted.id
994
+ const insertedRowId = inserted && typeof inserted === 'object' && 'id' in inserted
995
+ ? inserted.id
996
+ : inserted
997
+
998
+ if (explicitId !== undefined && explicitId !== null) {
999
+ return explicitId
1000
+ }
1001
+
1002
+ if (insertedRowId === undefined || insertedRowId === null) {
1003
+ return insertedRowId
933
1004
  }
934
- return inserted
1005
+
1006
+ const generatedLogicalId = String(insertedRowId)
1007
+
1008
+ await context.db(canonical.tableName)
1009
+ .where({ id: insertedRowId })
1010
+ .update({ [logicalIdColumn]: generatedLogicalId })
1011
+
1012
+ return generatedLogicalId
935
1013
  }
936
1014
 
937
1015
  helpers.dataPut = async ({ scopeName, context }) => {
@@ -942,8 +1020,11 @@ export const RestApiAnyapiKnexPlugin = {
942
1020
  const descriptor = await getDescriptor(scopeName)
943
1021
  const { canonical } = descriptor
944
1022
  const id = context.id
1023
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
945
1024
  const attributes = context.inputRecord?.data?.attributes || {}
946
- const row = translateAttributesForStorage(attributes, descriptor)
1025
+ const row = storageAdapter?.toStorageRow
1026
+ ? storageAdapter.toStorageRow(attributes)
1027
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
947
1028
 
948
1029
  row[canonical.resourceColumn] = descriptor.resource
949
1030
  row[canonical.tenantColumn] = descriptor.tenant
@@ -957,7 +1038,7 @@ export const RestApiAnyapiKnexPlugin = {
957
1038
  }
958
1039
 
959
1040
  const result = await context.db(canonical.tableName)
960
- .where('id', id)
1041
+ .where(logicalIdColumn, id)
961
1042
  .where(canonical.resourceColumn, descriptor.resource)
962
1043
  .where(canonical.tenantColumn, descriptor.tenant)
963
1044
  .update(updateRow)
@@ -973,8 +1054,11 @@ export const RestApiAnyapiKnexPlugin = {
973
1054
  const descriptor = await getDescriptor(scopeName)
974
1055
  const { canonical } = descriptor
975
1056
  const id = context.id
1057
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
976
1058
  const attributes = context.inputRecord?.data?.attributes || {}
977
- const row = translateAttributesForStorage(attributes, descriptor)
1059
+ const row = storageAdapter?.toStorageRow
1060
+ ? storageAdapter.toStorageRow(attributes)
1061
+ : translateCanonicalAttributesForStorage(attributes, descriptor)
978
1062
 
979
1063
  const updateRow = Object.fromEntries(
980
1064
  Object.entries(row).filter(([, value]) => value !== undefined)
@@ -985,7 +1069,7 @@ export const RestApiAnyapiKnexPlugin = {
985
1069
  }
986
1070
 
987
1071
  const result = await context.db(canonical.tableName)
988
- .where('id', id)
1072
+ .where(logicalIdColumn, id)
989
1073
  .where(canonical.resourceColumn, descriptor.resource)
990
1074
  .where(canonical.tenantColumn, descriptor.tenant)
991
1075
  .update(updateRow)
@@ -1001,9 +1085,10 @@ export const RestApiAnyapiKnexPlugin = {
1001
1085
  const descriptor = await getDescriptor(scopeName)
1002
1086
  const { canonical } = descriptor
1003
1087
  const id = context.id
1088
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1004
1089
 
1005
1090
  const result = await context.db(canonical.tableName)
1006
- .where('id', id)
1091
+ .where(logicalIdColumn, id)
1007
1092
  .where(canonical.resourceColumn, descriptor.resource)
1008
1093
  .where(canonical.tenantColumn, descriptor.tenant)
1009
1094
  .delete()
@@ -1011,7 +1096,7 @@ export const RestApiAnyapiKnexPlugin = {
1011
1096
  return result
1012
1097
  }
1013
1098
 
1014
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
1099
+ helpers.dataGetMinimal = async ({ scopeName, context, runHooks, applyQueryFilters }) => {
1015
1100
  const storageAdapter = getScopeStorageAdapter(scopeName)
1016
1101
  if (storageAdapter) {
1017
1102
  context.storageAdapter = storageAdapter
@@ -1019,26 +1104,64 @@ export const RestApiAnyapiKnexPlugin = {
1019
1104
  const descriptor = await getDescriptor(scopeName)
1020
1105
  const { canonical } = descriptor
1021
1106
  const id = context.id
1107
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1022
1108
  const scope = api.resources?.[scopeName]
1023
1109
 
1024
- const row = await context.db(canonical.tableName)
1025
- .where('id', id)
1110
+ let query = context.db(canonical.tableName)
1111
+ .where(logicalIdColumn, id)
1026
1112
  .where(canonical.resourceColumn, descriptor.resource)
1027
1113
  .where(canonical.tenantColumn, descriptor.tenant)
1028
- .first()
1114
+
1115
+ if (typeof applyQueryFilters === 'function') {
1116
+ const scopedQueryState = await applyQueryFilters({
1117
+ query,
1118
+ filters: context.queryParams?.filters,
1119
+ tableName: canonical.tableName,
1120
+ db: context.db,
1121
+ scopeName,
1122
+ storageAdapter,
1123
+ isAnyApi: true
1124
+ })
1125
+ query = unwrapQueryBuilderState(scopedQueryState, query)
1126
+ } else if (runHooks) {
1127
+ context.knexQuery = {
1128
+ query,
1129
+ filters: context.queryParams?.filters,
1130
+ schemaInfo: context.schemaInfo,
1131
+ scopeName,
1132
+ tableName: canonical.tableName,
1133
+ db: context.db,
1134
+ isAnyApi: true,
1135
+ adapter: storageAdapter,
1136
+ storageAdapter,
1137
+ }
1138
+
1139
+ await applyBuiltInAnyApiQueryFilters(context)
1140
+
1141
+ await runHooks('knexQueryFiltering')
1142
+
1143
+ if (context.knexQuery) {
1144
+ delete context.knexQuery
1145
+ }
1146
+ }
1147
+
1148
+ const row = await query.first()
1029
1149
 
1030
1150
  if (!row) return null
1031
- const translated = translateRecordFromStorage(row, descriptor)
1151
+ const translated = translateCanonicalRecordFromStorage(
1152
+ row,
1153
+ descriptor,
1154
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1155
+ )
1156
+ const resourceId = getLogicalResourceId(row, descriptor)
1032
1157
  const minimal = {
1033
1158
  type: descriptor.resource,
1034
- id: String(row.id),
1159
+ id: resourceId,
1035
1160
  attributes: translated.attributes,
1036
1161
  relationships: translated.relationships,
1037
1162
  }
1038
1163
  if (scope) {
1039
- minimal.links = {
1040
- self: buildResourceUrl(context, scope, scopeName, minimal.id),
1041
- }
1164
+ decorateResourceLinks({ resource: minimal, scope, scopeName, context })
1042
1165
  }
1043
1166
  return minimal
1044
1167
  }
@@ -1069,17 +1192,107 @@ export const RestApiAnyapiKnexPlugin = {
1069
1192
  return tree
1070
1193
  }
1071
1194
 
1195
+ const applyIncludeFieldSelection = async ({ query, resourceName, tableName, context }) => {
1196
+ const targetScope = api.resources?.[resourceName]
1197
+ if (!targetScope) {
1198
+ return { query, fieldSelectionInfo: null }
1199
+ }
1200
+
1201
+ const storageAdapter = getScopeStorageAdapter(resourceName)
1202
+ const fieldSelectionInfo = await buildFieldSelection(targetScope, {
1203
+ context: {
1204
+ ...context,
1205
+ scopeName: resourceName,
1206
+ schemaInfo: targetScope.vars.schemaInfo,
1207
+ storageAdapter,
1208
+ }
1209
+ })
1210
+
1211
+ const selectionState = await applyFieldSelectionToQuery({
1212
+ query,
1213
+ scope: targetScope,
1214
+ fieldSelectionInfo,
1215
+ tableName,
1216
+ useTablePrefix: false,
1217
+ storageAdapter,
1218
+ db: context.db || context.transaction || api.knex.instance,
1219
+ context,
1220
+ scopeName: resourceName
1221
+ })
1222
+
1223
+ return {
1224
+ query: selectionState.query,
1225
+ fieldSelectionInfo
1226
+ }
1227
+ }
1228
+
1229
+ const applyTargetScopeFilters = async ({ query, resourceName, tableName, context, filters }) => {
1230
+ const targetScope = api.resources?.[resourceName]
1231
+ if (!targetScope?.applyQueryFilters) {
1232
+ return { query }
1233
+ }
1234
+
1235
+ const storageAdapter = getScopeStorageAdapter(resourceName)
1236
+ const queryState = await targetScope.applyQueryFilters({
1237
+ query,
1238
+ filters,
1239
+ schemaInfo: targetScope.vars?.schemaInfo,
1240
+ scopeName: resourceName,
1241
+ tableName,
1242
+ db: context.db || context.transaction || api.knex.instance,
1243
+ isAnyApi: true,
1244
+ storageAdapter
1245
+ }, {
1246
+ ...context,
1247
+ scopeName: resourceName,
1248
+ schemaInfo: targetScope.vars?.schemaInfo,
1249
+ storageAdapter
1250
+ })
1251
+
1252
+ return {
1253
+ query: unwrapQueryBuilderState(queryState, query)
1254
+ }
1255
+ }
1256
+
1257
+ const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
1258
+ const translated = translateCanonicalRecordFromStorage(
1259
+ row,
1260
+ descriptor,
1261
+ { allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
1262
+ )
1263
+ const resourceId = getLogicalResourceId(row, descriptor)
1264
+ const includeResource = {
1265
+ type: descriptor.resource,
1266
+ id: resourceId,
1267
+ attributes: translated.attributes,
1268
+ }
1269
+
1270
+ if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1271
+ includeResource.relationships = translated.relationships
1272
+ }
1273
+
1274
+ if (fieldSelectionInfo?.computedDependencies?.length) {
1275
+ includeResource[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
1276
+ }
1277
+
1278
+ const targetScope = api.resources?.[descriptor.resource]
1279
+ if (targetScope) {
1280
+ decorateResourceLinks({
1281
+ resource: includeResource,
1282
+ scope: targetScope,
1283
+ scopeName: descriptor.resource,
1284
+ context,
1285
+ })
1286
+ }
1287
+
1288
+ return includeResource
1289
+ }
1290
+
1072
1291
  const collectIncludes = async ({ descriptor, resources, includeTree, context, includes, seen }) => {
1073
1292
  const entries = Object.entries(includeTree || {})
1074
1293
  if (entries.length === 0 || !resources || resources.length === 0) return
1075
1294
 
1076
1295
  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
1296
 
1084
1297
  for (const [relName, childTree] of entries) {
1085
1298
  const childKeys = Object.keys(childTree || {})
@@ -1095,26 +1308,31 @@ export const RestApiAnyapiKnexPlugin = {
1095
1308
  const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target)
1096
1309
  if (!targetDescriptor) continue
1097
1310
 
1098
- const rows = await db(targetDescriptor.canonical.tableName)
1311
+ let query = db(targetDescriptor.canonical.tableName)
1099
1312
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1100
1313
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1101
- .whereIn('id', ids)
1314
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), ids)
1315
+ const selectionState = await applyIncludeFieldSelection({
1316
+ query,
1317
+ resourceName: targetDescriptor.resource,
1318
+ tableName: targetDescriptor.canonical.tableName,
1319
+ context
1320
+ })
1321
+ query = selectionState.query
1322
+ const rows = await query
1102
1323
 
1103
1324
  const newResources = []
1104
1325
  for (const row of rows) {
1105
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1326
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1327
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1106
1328
  if (seen.has(includeKey)) continue
1107
1329
  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)
1330
+ const includeResource = buildIncludedResource({
1331
+ row,
1332
+ descriptor: targetDescriptor,
1333
+ context,
1334
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1335
+ })
1118
1336
  includes.push(includeResource)
1119
1337
  newResources.push(includeResource)
1120
1338
  }
@@ -1159,25 +1377,30 @@ export const RestApiAnyapiKnexPlugin = {
1159
1377
  continue
1160
1378
  }
1161
1379
 
1162
- const rows = await db(targetDescriptor.canonical.tableName)
1380
+ let query = db(targetDescriptor.canonical.tableName)
1163
1381
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1164
1382
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1165
- .whereIn('id', [...idSet])
1383
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), [...idSet])
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 rows = await query
1166
1392
 
1167
1393
  for (const row of rows) {
1168
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1394
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1395
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1169
1396
  if (seen.has(includeKey)) continue
1170
1397
  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)
1398
+ const includeResource = buildIncludedResource({
1399
+ row,
1400
+ descriptor: targetDescriptor,
1401
+ context,
1402
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1403
+ })
1181
1404
  includes.push(includeResource)
1182
1405
  newResources.push(includeResource)
1183
1406
  }
@@ -1243,6 +1466,7 @@ export const RestApiAnyapiKnexPlugin = {
1243
1466
 
1244
1467
  let rows = []
1245
1468
  let groupingColumn = null
1469
+ let fieldSelectionInfo = null
1246
1470
 
1247
1471
  if (hasManyInfo.foreignKey) {
1248
1472
  const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey]
@@ -1250,21 +1474,53 @@ export const RestApiAnyapiKnexPlugin = {
1250
1474
  continue
1251
1475
  }
1252
1476
  groupingColumn = foreignField.slot
1253
- rows = await db(targetDescriptor.canonical.tableName)
1477
+ let query = db(targetDescriptor.canonical.tableName)
1254
1478
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1255
1479
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1256
1480
  .whereIn(groupingColumn, parentIds)
1481
+ const selectionState = await applyIncludeFieldSelection({
1482
+ query,
1483
+ resourceName: targetDescriptor.resource,
1484
+ tableName: targetDescriptor.canonical.tableName,
1485
+ context
1486
+ })
1487
+ query = selectionState.query
1488
+ const scopedQueryState = await applyTargetScopeFilters({
1489
+ query,
1490
+ resourceName: targetDescriptor.resource,
1491
+ tableName: targetDescriptor.canonical.tableName,
1492
+ context
1493
+ })
1494
+ query = scopedQueryState.query
1495
+ fieldSelectionInfo = selectionState.fieldSelectionInfo
1496
+ rows = await query
1257
1497
  } else if (hasManyInfo.via) {
1258
1498
  const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via]
1259
1499
  if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
1260
1500
  continue
1261
1501
  }
1262
1502
  groupingColumn = polyInfo.idColumn
1263
- rows = await db(targetDescriptor.canonical.tableName)
1503
+ let query = db(targetDescriptor.canonical.tableName)
1264
1504
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1265
1505
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1266
1506
  .where(polyInfo.typeColumn, descriptor.resource)
1267
1507
  .whereIn(groupingColumn, queryIds)
1508
+ const selectionState = await applyIncludeFieldSelection({
1509
+ query,
1510
+ resourceName: targetDescriptor.resource,
1511
+ tableName: targetDescriptor.canonical.tableName,
1512
+ context
1513
+ })
1514
+ query = selectionState.query
1515
+ const scopedQueryState = await applyTargetScopeFilters({
1516
+ query,
1517
+ resourceName: targetDescriptor.resource,
1518
+ tableName: targetDescriptor.canonical.tableName,
1519
+ context
1520
+ })
1521
+ query = scopedQueryState.query
1522
+ fieldSelectionInfo = selectionState.fieldSelectionInfo
1523
+ rows = await query
1268
1524
  }
1269
1525
 
1270
1526
  if (!groupingColumn || rows.length === 0) {
@@ -1273,19 +1529,16 @@ export const RestApiAnyapiKnexPlugin = {
1273
1529
 
1274
1530
  const newResources = []
1275
1531
  for (const row of rows) {
1276
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1532
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1533
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1277
1534
  if (seen.has(includeKey)) continue
1278
1535
  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)
1536
+ const includeResource = buildIncludedResource({
1537
+ row,
1538
+ descriptor: targetDescriptor,
1539
+ context,
1540
+ fieldSelectionInfo
1541
+ })
1289
1542
  includes.push(includeResource)
1290
1543
  newResources.push(includeResource)
1291
1544
  }
@@ -1323,26 +1576,31 @@ export const RestApiAnyapiKnexPlugin = {
1323
1576
  const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target)
1324
1577
  if (!targetDescriptor) continue
1325
1578
 
1326
- const rows = await db(targetDescriptor.canonical.tableName)
1579
+ let query = db(targetDescriptor.canonical.tableName)
1327
1580
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1328
1581
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1329
- .whereIn('id', childIds)
1582
+ .whereIn(getLogicalResourceIdColumn(targetDescriptor), childIds)
1583
+ const selectionState = await applyIncludeFieldSelection({
1584
+ query,
1585
+ resourceName: targetDescriptor.resource,
1586
+ tableName: targetDescriptor.canonical.tableName,
1587
+ context
1588
+ })
1589
+ query = selectionState.query
1590
+ const rows = await query
1330
1591
 
1331
1592
  const newResources = []
1332
1593
  for (const row of rows) {
1333
- const includeKey = `${targetDescriptor.resource}:${row.id}`
1594
+ const includeId = getLogicalResourceId(row, targetDescriptor)
1595
+ const includeKey = `${targetDescriptor.resource}:${includeId}`
1334
1596
  if (seen.has(includeKey)) continue
1335
1597
  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)
1598
+ const includeResource = buildIncludedResource({
1599
+ row,
1600
+ descriptor: targetDescriptor,
1601
+ context,
1602
+ fieldSelectionInfo: selectionState.fieldSelectionInfo
1603
+ })
1346
1604
  includes.push(includeResource)
1347
1605
  newResources.push(includeResource)
1348
1606
  }
@@ -1410,17 +1668,25 @@ export const RestApiAnyapiKnexPlugin = {
1410
1668
  if (!foreignField?.slot) continue
1411
1669
  const column = foreignField.slot
1412
1670
 
1413
- const rows = await db(targetDescriptor.canonical.tableName)
1671
+ let query = db(targetDescriptor.canonical.tableName)
1414
1672
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1415
1673
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1416
1674
  .whereIn(column, normalizedParentIds)
1675
+ const scopedQueryState = await applyTargetScopeFilters({
1676
+ query,
1677
+ resourceName: targetDescriptor.resource,
1678
+ tableName: targetDescriptor.canonical.tableName,
1679
+ context
1680
+ })
1681
+ query = scopedQueryState.query
1682
+ const rows = await query
1417
1683
 
1418
1684
  const grouped = rows.reduce((acc, row) => {
1419
1685
  const parentId = row[column]
1420
1686
  if (parentId == null) return acc
1421
1687
  const key = String(parentId)
1422
1688
  acc[key] = acc[key] || []
1423
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1689
+ acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1424
1690
  return acc
1425
1691
  }, {})
1426
1692
 
@@ -1447,18 +1713,26 @@ export const RestApiAnyapiKnexPlugin = {
1447
1713
  .filter((value) => value !== null),
1448
1714
  ]))
1449
1715
 
1450
- const rows = await db(targetDescriptor.canonical.tableName)
1716
+ let query = db(targetDescriptor.canonical.tableName)
1451
1717
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1452
1718
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1453
1719
  .where(polyInfo.typeColumn, descriptor.resource)
1454
1720
  .whereIn(polyInfo.idColumn, queryIds)
1721
+ const scopedQueryState = await applyTargetScopeFilters({
1722
+ query,
1723
+ resourceName: targetDescriptor.resource,
1724
+ tableName: targetDescriptor.canonical.tableName,
1725
+ context
1726
+ })
1727
+ query = scopedQueryState.query
1728
+ const rows = await query
1455
1729
 
1456
1730
  const grouped = rows.reduce((acc, row) => {
1457
1731
  const parentId = row[polyInfo.idColumn]
1458
1732
  if (parentId == null) return acc
1459
1733
  const key = String(parentId)
1460
1734
  acc[key] = acc[key] || []
1461
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1735
+ acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
1462
1736
  return acc
1463
1737
  }, {})
1464
1738
 
@@ -1479,7 +1753,7 @@ export const RestApiAnyapiKnexPlugin = {
1479
1753
  const db = context.db || context.transaction || api.knex.instance
1480
1754
  const parentIds = resources.map((resource) => resource.id)
1481
1755
 
1482
- for (const [relName, relInfo] of manyEntries) {
1756
+ for (const [relName] of manyEntries) {
1483
1757
  const info = await getManyToManyInfo(descriptor.resource, relName)
1484
1758
  if (!info) continue
1485
1759
  const rows = await fetchLinksForParents({
@@ -1512,6 +1786,7 @@ export const RestApiAnyapiKnexPlugin = {
1512
1786
  const descriptor = await getDescriptor(scopeName)
1513
1787
  const { canonical } = descriptor
1514
1788
  const id = context.id
1789
+ const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
1515
1790
  const scope = api.resources?.[scopeName]
1516
1791
 
1517
1792
  let fieldSelectionInfo = null
@@ -1522,18 +1797,39 @@ export const RestApiAnyapiKnexPlugin = {
1522
1797
  context.computedDependencies = []
1523
1798
  }
1524
1799
 
1525
- const row = await context.db(canonical.tableName)
1526
- .where('id', id)
1800
+ let query = context.db(canonical.tableName)
1801
+ .where(logicalIdColumn, id)
1527
1802
  .where(canonical.resourceColumn, descriptor.resource)
1528
1803
  .where(canonical.tenantColumn, descriptor.tenant)
1529
- .first()
1804
+
1805
+ if (scope && fieldSelectionInfo) {
1806
+ const selectionState = await applyFieldSelectionToQuery({
1807
+ query,
1808
+ scope,
1809
+ fieldSelectionInfo,
1810
+ tableName: canonical.tableName,
1811
+ useTablePrefix: false,
1812
+ storageAdapter,
1813
+ db: context.db,
1814
+ context,
1815
+ scopeName
1816
+ })
1817
+ query = selectionState.query
1818
+ }
1819
+
1820
+ const row = await query.first()
1530
1821
 
1531
1822
  if (!row) return null
1532
1823
 
1533
- const record = translateRecordFromStorage(row, descriptor)
1824
+ const record = translateCanonicalRecordFromStorage(
1825
+ row,
1826
+ descriptor,
1827
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
1828
+ )
1829
+ const resourceId = getLogicalResourceId(row, descriptor)
1534
1830
  const data = {
1535
1831
  type: descriptor.resource,
1536
- id: String(row.id),
1832
+ id: resourceId,
1537
1833
  attributes: record.attributes,
1538
1834
  }
1539
1835
 
@@ -1565,24 +1861,20 @@ export const RestApiAnyapiKnexPlugin = {
1565
1861
  }
1566
1862
 
1567
1863
  if (scope) {
1568
- data.links = data.links || {}
1569
- data.links.self = buildResourceUrl(context, scope, scopeName, data.id)
1570
- response.links = {
1571
- self: data.links.self,
1572
- }
1864
+ decorateResourceLinks({ resource: data, scope, scopeName, context })
1865
+ response.links = { self: data.links.self }
1573
1866
  }
1574
1867
 
1575
1868
  if (included.length > 0) {
1576
1869
  for (const includeResource of included) {
1577
1870
  const targetScope = api.resources?.[includeResource.type]
1578
1871
  if (!targetScope) continue
1579
- includeResource.links = includeResource.links || {}
1580
- includeResource.links.self = buildResourceUrl(
1872
+ decorateResourceLinks({
1873
+ resource: includeResource,
1874
+ scope: targetScope,
1875
+ scopeName: includeResource.type,
1581
1876
  context,
1582
- targetScope,
1583
- includeResource.type,
1584
- includeResource.id
1585
- )
1877
+ })
1586
1878
  }
1587
1879
  }
1588
1880
 
@@ -1592,10 +1884,11 @@ export const RestApiAnyapiKnexPlugin = {
1592
1884
  const rehydrateSchemaInfo = async (scopeName) => {
1593
1885
  const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {}
1594
1886
  const storedOptions = scopeOptionsRegistry.get(scopeName) || {}
1887
+ const rawSchema = scope.scopeOptions?.schema || storedOptions.schema || {}
1595
1888
 
1596
1889
  if (!scope.scopeOptions) {
1597
1890
  scope.scopeOptions = {
1598
- schema: storedOptions.schema || {},
1891
+ schema: rawSchema,
1599
1892
  relationships: storedOptions.relationships || {},
1600
1893
  searchSchema: storedOptions.searchSchema || null,
1601
1894
  tableName: storedOptions.tableName || scopeName,
@@ -1611,7 +1904,6 @@ export const RestApiAnyapiKnexPlugin = {
1611
1904
 
1612
1905
  const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) }
1613
1906
  const computed = { ...(existingInfo.computed || {}) }
1614
-
1615
1907
  for (const [fieldName, definition] of Object.entries(descriptor.schema || {})) {
1616
1908
  if (definition?.computed) {
1617
1909
  if (!computed[fieldName]) {
@@ -1699,53 +1991,15 @@ export const RestApiAnyapiKnexPlugin = {
1699
1991
  return { resourceToTableName, tableNameToResource }
1700
1992
  }
1701
1993
 
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 '*'
1994
+ const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
1709
1995
 
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
- })
1996
+ const applyBuiltInAnyApiQueryFilters = async (context) => {
1997
+ const hookParams = { context }
1998
+ await polymorphicFiltersHook(hookParams, queryHookDependencies)
1999
+ await crossTableFiltersHook(hookParams, queryHookDependencies)
2000
+ await basicFiltersHook(hookParams, queryHookDependencies)
1720
2001
  }
1721
2002
 
1722
- const scopesProxy = new Proxy({}, {
1723
- get (_target, prop) {
1724
- if (typeof prop !== 'string') {
1725
- return undefined
1726
- }
1727
- const resource = api.resources?.[prop]
1728
- if (resource?.vars?.schemaInfo) {
1729
- return resource
1730
- }
1731
- return scopes?.[prop]
1732
- },
1733
- })
1734
-
1735
- const queryHookDependencies = { log, scopes: scopesProxy, knex, getStorageAdapter: getScopeStorageAdapter }
1736
-
1737
- addHook('knexQueryFiltering', 'polymorphicFiltersHook', {}, async (hookParams) =>
1738
- polymorphicFiltersHook(hookParams, queryHookDependencies)
1739
- )
1740
-
1741
- addHook('knexQueryFiltering', 'crossTableFiltersHook', {}, async (hookParams) =>
1742
- crossTableFiltersHook(hookParams, queryHookDependencies)
1743
- )
1744
-
1745
- addHook('knexQueryFiltering', 'basicFiltersHook', {}, async (hookParams) =>
1746
- basicFiltersHook(hookParams, queryHookDependencies)
1747
- )
1748
-
1749
2003
  helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
1750
2004
  const storageAdapter = getScopeStorageAdapter(scopeName)
1751
2005
  if (storageAdapter) {
@@ -1801,6 +2055,8 @@ export const RestApiAnyapiKnexPlugin = {
1801
2055
  storageAdapter,
1802
2056
  }
1803
2057
 
2058
+ await applyBuiltInAnyApiQueryFilters(context)
2059
+
1804
2060
  if (runHooks) {
1805
2061
  await runHooks('knexQueryFiltering')
1806
2062
  }
@@ -1809,16 +2065,29 @@ export const RestApiAnyapiKnexPlugin = {
1809
2065
 
1810
2066
  const countQuery = queryBuilder.clone()
1811
2067
 
2068
+ let selectionState = { query: queryBuilder, queryFieldRuntimeByField: new Map() }
2069
+ if (scope && fieldSelectionInfo) {
2070
+ selectionState = await applyFieldSelectionToQuery({
2071
+ query: queryBuilder,
2072
+ scope,
2073
+ fieldSelectionInfo,
2074
+ tableName: adapter.tableAlias,
2075
+ useTablePrefix: false,
2076
+ storageAdapter,
2077
+ db,
2078
+ context,
2079
+ scopeName
2080
+ })
2081
+ }
2082
+
1812
2083
  const sortDescriptors = applySortingToQuery({
1813
- query: queryBuilder,
2084
+ query: selectionState.query,
1814
2085
  sort: queryParams.sort,
1815
2086
  descriptor,
1816
2087
  scope,
2088
+ queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
1817
2089
  })
1818
2090
 
1819
- if (queryParams?.page?.after) {
1820
- }
1821
-
1822
2091
  const paginationInfo = applyPaginationToQuery({
1823
2092
  query: queryBuilder,
1824
2093
  scope,
@@ -1827,14 +2096,6 @@ export const RestApiAnyapiKnexPlugin = {
1827
2096
  adapter,
1828
2097
  })
1829
2098
 
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
2099
  let rows = await queryBuilder
1839
2100
  let cursorRecords = null
1840
2101
  let hasMore = false
@@ -1845,7 +2106,9 @@ export const RestApiAnyapiKnexPlugin = {
1845
2106
  const record = {}
1846
2107
  for (const descriptorEntry of cursorDescriptors) {
1847
2108
  if (descriptorEntry.field === 'id') {
1848
- record.id = row.id
2109
+ record.id = getLogicalResourceId(row, descriptor)
2110
+ } else if (descriptorEntry.queryFieldRuntime) {
2111
+ record[descriptorEntry.field] = row[descriptorEntry.field]
1849
2112
  } else if (descriptorEntry.column) {
1850
2113
  record[descriptorEntry.field] = row[descriptorEntry.column]
1851
2114
  }
@@ -1861,10 +2124,15 @@ export const RestApiAnyapiKnexPlugin = {
1861
2124
  }
1862
2125
 
1863
2126
  const data = rows.map((row) => {
1864
- const translated = translateRecordFromStorage(row, descriptor)
2127
+ const translated = translateCanonicalRecordFromStorage(
2128
+ row,
2129
+ descriptor,
2130
+ { allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
2131
+ )
2132
+ const resourceId = getLogicalResourceId(row, descriptor)
1865
2133
  const resource = {
1866
2134
  type: descriptor.resource,
1867
- id: String(row.id),
2135
+ id: resourceId,
1868
2136
  attributes: translated.attributes,
1869
2137
  }
1870
2138
  if (translated.relationships && Object.keys(translated.relationships).length > 0) {
@@ -1872,8 +2140,7 @@ export const RestApiAnyapiKnexPlugin = {
1872
2140
  }
1873
2141
  const resourceScope = scope
1874
2142
  if (resourceScope) {
1875
- resource.links = resource.links || {}
1876
- resource.links.self = buildResourceUrl(context, resourceScope, scopeName, resource.id)
2143
+ decorateResourceLinks({ resource, scope: resourceScope, scopeName, context })
1877
2144
  }
1878
2145
  if (fieldSelectionInfo?.computedDependencies?.length) {
1879
2146
  resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
@@ -1904,6 +2171,19 @@ export const RestApiAnyapiKnexPlugin = {
1904
2171
  response.included = included
1905
2172
  }
1906
2173
 
2174
+ if (included.length > 0) {
2175
+ for (const includeResource of included) {
2176
+ const targetScope = api.resources?.[includeResource.type]
2177
+ if (!targetScope) continue
2178
+ decorateResourceLinks({
2179
+ resource: includeResource,
2180
+ scope: targetScope,
2181
+ scopeName: includeResource.type,
2182
+ context,
2183
+ })
2184
+ }
2185
+ }
2186
+
1907
2187
  context.returnMeta = context.returnMeta || {}
1908
2188
  context.returnMeta.queryString = buildQueryString(queryParams)
1909
2189
  delete context.returnMeta.paginationMeta
@@ -2194,64 +2474,3 @@ export const RestApiAnyapiKnexPlugin = {
2194
2474
  })
2195
2475
  },
2196
2476
  }
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
- }