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
@@ -1,10 +1,15 @@
1
1
  import { requirePackage } from 'hooked-api'
2
2
  import { createSchema } from 'json-rest-schema'
3
- import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
4
- import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
5
- import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
6
- import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
7
- import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
3
+ import {
4
+ createKnexTable,
5
+ addKnexFields,
6
+ alterKnexFields,
7
+ generateKnexMigration,
8
+ generateKnexMigrationDiff
9
+ } from './lib/dbTablesOperations.js'
10
+ import { introspectKnexTableSnapshot } from './lib/dbIntrospection.js'
11
+ import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
12
+ import { buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
8
13
  import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
9
14
  import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
10
15
  import { processIncludes } from './lib/querying/knex-process-includes.js'
@@ -26,30 +31,13 @@ import {
26
31
  } from './lib/querying/knex-pagination-helpers.js'
27
32
  import { getUrlPrefix } from './lib/querying/url-helpers.js'
28
33
  import { createStorageAdapter } from './lib/storage/storage-adapter.js'
29
-
30
- /**
31
- * Strips non-database fields (computed and virtual) from attributes before database operations
32
- * @param {Object} attributes - The attributes object
33
- * @param {Object} schemaInfo - The schema info containing computed field definitions and schema structure
34
- * @returns {Object} Attributes with computed and virtual fields removed
35
- */
36
- const stripNonDatabaseFields = (attributes, schemaInfo) => {
37
- if (!attributes || !schemaInfo) return attributes || {}
38
-
39
- const { computed = {}, schemaStructure = {} } = schemaInfo
40
- return Object.entries(attributes)
41
- .filter(([key]) => {
42
- // Remove computed fields
43
- if (key in computed) return false
44
- // Remove fields marked as virtual in the schema
45
- const fieldDef = schemaStructure[key]
46
- if (fieldDef && fieldDef.virtual === true) {
47
- return false
48
- }
49
- return true
50
- })
51
- .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
52
- }
34
+ import {
35
+ applyCursorPredicate,
36
+ applyQueryFieldOrder,
37
+ buildEffectiveSortList,
38
+ parseSortEntry
39
+ } from './lib/querying/query-field-sort-helpers.js'
40
+ import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
53
41
 
54
42
  export const RestApiKnexPlugin = {
55
43
  name: 'rest-api-knex',
@@ -57,9 +45,8 @@ export const RestApiKnexPlugin = {
57
45
 
58
46
  async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
59
47
  // Try to import knex dynamically
60
- let knexLib
61
48
  try {
62
- knexLib = await import('knex')
49
+ await import('knex')
63
50
  } catch (e) {
64
51
  requirePackage('knex', 'rest-api-knex',
65
52
  'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
@@ -99,22 +86,90 @@ export const RestApiKnexPlugin = {
99
86
  api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
100
87
  helpers.getStorageAdapter = getScopeStorageAdapter
101
88
 
102
- const createSelectTranslator = (adapter) => {
103
- if (!adapter) return null
104
- return (field, alias) => {
105
- if (field === '*') {
106
- return alias ? `${alias}.*` : '*'
89
+ const buildScopeTableSchema = (vars = {}) => {
90
+ const schemaStructure = vars.schemaInfo?.schemaStructure || {}
91
+ const filteredSchema = {}
92
+
93
+ Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
94
+ if (!fieldDef.virtual) {
95
+ filteredSchema[fieldName] = fieldDef
96
+ }
97
+ })
98
+
99
+ return {
100
+ structure: filteredSchema,
101
+ storage: vars.schemaInfo?.storage,
102
+ indexes: vars.schemaInfo?.indexes || [],
103
+ foreignKeys: vars.schemaInfo?.foreignKeys || [],
104
+ checkConstraints: vars.schemaInfo?.checkConstraints || []
105
+ }
106
+ }
107
+
108
+ const buildLegacySortDescriptors = ({
109
+ query,
110
+ sort,
111
+ schemaInfo,
112
+ sortableFields,
113
+ storageAdapter,
114
+ defaultSort,
115
+ queryFieldRuntimeByField = new Map()
116
+ }) => {
117
+ const effectiveSort = buildEffectiveSortList(sort, { defaultSort, idField: 'id' })
118
+ const descriptors = []
119
+
120
+ for (const sortEntry of effectiveSort) {
121
+ const { field, direction, sqlDirection } = parseSortEntry(sortEntry)
122
+
123
+ if (field !== 'id' && sortableFields?.length > 0 && !sortableFields.includes(field)) {
124
+ log.warn(`Ignoring non-sortable field: ${field}`)
125
+ continue
107
126
  }
108
- const translated = adapter.translateColumn(field)
109
- const result = translated || field
110
- if (!alias) {
111
- return result
127
+
128
+ const queryFieldRuntime = queryFieldRuntimeByField.get(field)
129
+ if (queryFieldRuntime) {
130
+ applyQueryFieldOrder(query, queryFieldRuntime, sqlDirection)
131
+ descriptors.push({
132
+ field,
133
+ direction: sqlDirection,
134
+ queryFieldRuntime,
135
+ definition: queryFieldRuntime.definition,
136
+ isRelationship: false
137
+ })
138
+ continue
112
139
  }
113
- if (result.includes('.')) {
114
- return result
140
+
141
+ let dbField = field
142
+ const searchField = schemaInfo.searchSchemaStructure?.[field]
143
+ if (searchField?.actualField) {
144
+ dbField = searchField.actualField
115
145
  }
116
- return `${alias}.${result}`
146
+
147
+ const translatedField = storageAdapter?.translateColumn
148
+ ? storageAdapter.translateColumn(dbField)
149
+ : dbField
150
+
151
+ query.orderBy(translatedField, direction)
152
+ descriptors.push({
153
+ field,
154
+ direction: sqlDirection,
155
+ column: translatedField,
156
+ definition: schemaInfo.schemaStructure?.[field] || { type: field === 'id' ? 'id' : undefined },
157
+ isRelationship: Boolean(searchField?.isRelationship)
158
+ })
117
159
  }
160
+
161
+ if (descriptors.length === 0) {
162
+ query.orderBy('id', 'asc')
163
+ descriptors.push({
164
+ field: 'id',
165
+ direction: 'ASC',
166
+ column: 'id',
167
+ definition: { type: 'id' },
168
+ isRelationship: false
169
+ })
170
+ }
171
+
172
+ return descriptors
118
173
  }
119
174
 
120
175
  // Check database capabilities
@@ -170,21 +225,45 @@ export const RestApiKnexPlugin = {
170
225
 
171
226
  // Helper scope method to get all schema-related information
172
227
  addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
173
- // Create a filtered schema that excludes virtual fields
174
- const schemaStructure = vars.schemaInfo.schemaStructure || {}
175
- const filteredSchema = {}
228
+ const tableSchemaInstance = buildScopeTableSchema(vars)
229
+ await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
230
+ })
176
231
 
177
- // Copy only non-virtual fields to the filtered schema
178
- Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
179
- if (!fieldDef.virtual) {
180
- filteredSchema[fieldName] = fieldDef
181
- }
232
+ addScopeMethod('introspectKnexTableSnapshot', async ({ vars }) => {
233
+ return introspectKnexTableSnapshot(api.knex.instance, {
234
+ tableName: vars.schemaInfo.tableName,
235
+ idColumn: vars.schemaInfo.idProperty
182
236
  })
237
+ })
183
238
 
184
- // Create schema object from filtered fields
185
- const tableSchemaInstance = createSchema(filteredSchema)
239
+ addScopeMethod('generateKnexMigration', async ({ vars, scopeOptions }) => {
240
+ return generateKnexMigration(
241
+ vars.schemaInfo.tableName,
242
+ buildScopeTableSchema(vars),
243
+ {
244
+ ...scopeOptions,
245
+ idProperty: vars.schemaInfo.idProperty,
246
+ dialect: api.knex.instance?.client?.config?.client
247
+ }
248
+ )
249
+ })
186
250
 
187
- await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
251
+ addScopeMethod('generateKnexMigrationDiff', async ({ vars, params }) => {
252
+ const snapshot = await introspectKnexTableSnapshot(api.knex.instance, {
253
+ tableName: vars.schemaInfo.tableName,
254
+ idColumn: vars.schemaInfo.idProperty
255
+ })
256
+
257
+ return generateKnexMigrationDiff(
258
+ vars.schemaInfo.tableName,
259
+ snapshot,
260
+ buildScopeTableSchema(vars),
261
+ {
262
+ ...(params?.options || {}),
263
+ idProperty: vars.schemaInfo.idProperty,
264
+ dialect: snapshot.dialect || api.knex.instance?.client?.config?.client
265
+ }
266
+ )
188
267
  })
189
268
 
190
269
  // Helper scope method to alter existing fields in a table
@@ -198,7 +277,7 @@ export const RestApiKnexPlugin = {
198
277
  api.knex.instance,
199
278
  vars.schemaInfo.tableName,
200
279
  params.fields,
201
- params.options // Pass through any additional options
280
+ { ...params.options, idProperty: vars.schemaInfo.idProperty }
202
281
  )
203
282
  })
204
283
 
@@ -207,7 +286,12 @@ export const RestApiKnexPlugin = {
207
286
  // Create schema object from filtered fields
208
287
  const partialTableSchema = createSchema(params.fields)
209
288
 
210
- await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
289
+ await addKnexFields(
290
+ api.knex.instance,
291
+ vars.schemaInfo.tableName,
292
+ partialTableSchema,
293
+ { idProperty: vars.schemaInfo.idProperty }
294
+ )
211
295
  })
212
296
 
213
297
  helpers.newTransaction = async () => {
@@ -317,15 +401,18 @@ export const RestApiKnexPlugin = {
317
401
  // Permission checks will handle access control
318
402
  let query = db(tableName).where(idProperty, id)
319
403
 
320
- // Apply field selection
321
- const selectTranslator = createSelectTranslator(storageAdapter)
322
- query = buildQuerySelection(
404
+ const selectionState = await applyFieldSelectionToQuery({
323
405
  query,
406
+ scope,
407
+ fieldSelectionInfo,
324
408
  tableName,
325
- fieldSelectionInfo.fieldsToSelect,
326
- false,
327
- selectTranslator ? { translateColumn: selectTranslator } : undefined
328
- )
409
+ useTablePrefix: false,
410
+ storageAdapter,
411
+ db,
412
+ context,
413
+ scopeName
414
+ })
415
+ query = selectionState.query
329
416
 
330
417
  const record = await query.first()
331
418
 
@@ -342,7 +429,7 @@ export const RestApiKnexPlugin = {
342
429
 
343
430
  // Load relationship identifiers for all hasMany relationships
344
431
  const records = [record] // Wrap in array for processing
345
- await loadRelationshipIdentifiers(records, scopeName, scopes, db)
432
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
346
433
 
347
434
  // Process includes
348
435
  const included = await processIncludes(scope, records, {
@@ -369,7 +456,7 @@ export const RestApiKnexPlugin = {
369
456
  * @param {Object} params.context.db - Database connection (knex instance or transaction)
370
457
  * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
371
458
  */
372
- helpers.dataGetMinimal = async ({ scopeName, context }) => {
459
+ helpers.dataGetMinimal = async ({ scopeName, context, runHooks, applyQueryFilters }) => {
373
460
  const storageAdapter = getScopeStorageAdapter(scopeName)
374
461
  if (storageAdapter) {
375
462
  context.storageAdapter = storageAdapter
@@ -382,8 +469,9 @@ export const RestApiKnexPlugin = {
382
469
 
383
470
  log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
384
471
 
385
- // Build query - no filtering hooks for single records
386
- // Permission checks will handle access control
472
+ // Build query.
473
+ // Single-record lookups still go through knexQueryFiltering when hooks are available,
474
+ // so resource scoping plugins can treat GET/PUT/PATCH/DELETE the same way as collections.
387
475
  let query = db(tableName).where(idProperty, id)
388
476
 
389
477
  // Add alias if idProperty is not 'id'
@@ -391,6 +479,35 @@ export const RestApiKnexPlugin = {
391
479
  query = query.select('*', `${idProperty} as id`)
392
480
  }
393
481
 
482
+ if (typeof applyQueryFilters === 'function') {
483
+ const scopedQueryState = await applyQueryFilters({
484
+ query,
485
+ filters: context.queryParams?.filters,
486
+ tableName,
487
+ db,
488
+ scopeName,
489
+ storageAdapter
490
+ })
491
+ query = unwrapQueryBuilderState(scopedQueryState, query)
492
+ } else if (runHooks) {
493
+ context.knexQuery = {
494
+ query,
495
+ filters: context.queryParams?.filters,
496
+ schemaInfo: context.schemaInfo,
497
+ scopeName,
498
+ tableName,
499
+ db,
500
+ adapter: storageAdapter,
501
+ storageAdapter,
502
+ }
503
+
504
+ await runHooks('knexQueryFiltering')
505
+
506
+ if (context.knexQuery) {
507
+ delete context.knexQuery
508
+ }
509
+ }
510
+
394
511
  // Execute query
395
512
  const record = await query.first()
396
513
 
@@ -461,14 +578,19 @@ export const RestApiKnexPlugin = {
461
578
  // Start building query with table prefix (for JOIN support)
462
579
  let query = db(tableName)
463
580
 
464
- const selectTranslator = createSelectTranslator(storageAdapter)
465
- query = buildQuerySelection(
581
+ const selectionState = await applyFieldSelectionToQuery({
466
582
  query,
583
+ scope,
584
+ fieldSelectionInfo,
467
585
  tableName,
468
- fieldSelectionInfo.fieldsToSelect,
469
- true,
470
- selectTranslator ? { translateColumn: selectTranslator } : undefined
471
- )
586
+ useTablePrefix: true,
587
+ storageAdapter,
588
+ db,
589
+ context,
590
+ scopeName
591
+ })
592
+ query = selectionState.query
593
+ const queryFieldRuntimeByField = selectionState.queryFieldRuntimeByField
472
594
 
473
595
  /* ═══════════════════════════════════════════════════════════════════
474
596
  * FILTERING HOOKS
@@ -513,33 +635,15 @@ export const RestApiKnexPlugin = {
513
635
 
514
636
  log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
515
637
 
516
- // Apply sorting directly (no hooks)
517
- if (queryParams.sort && queryParams.sort.length > 0) {
518
- queryParams.sort.forEach(sortField => {
519
- const desc = sortField.startsWith('-')
520
- const field = desc ? sortField.substring(1) : sortField
521
-
522
- // Check if field is sortable
523
- if (sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
524
- log.warn(`Ignoring non-sortable field: ${field}`)
525
- return // Skip non-sortable fields
526
- }
527
-
528
- // Map relationship names to actual database columns for sorting
529
- // Check if this is a relationship field that needs to be mapped
530
- let dbField = field
531
- const searchField = schemaInfo.searchSchemaStructure?.[field]
532
- if (searchField?.actualField) {
533
- // This is a relationship field, use the actual database column
534
- dbField = searchField.actualField
535
- }
536
- const translatedField = storageAdapter?.translateColumn
537
- ? storageAdapter.translateColumn(dbField)
538
- : dbField
539
-
540
- query.orderBy(translatedField, desc ? 'desc' : 'asc')
541
- })
542
- }
638
+ const sortDescriptors = buildLegacySortDescriptors({
639
+ query,
640
+ sort: queryParams.sort,
641
+ schemaInfo,
642
+ sortableFields,
643
+ defaultSort: scope.vars.defaultSort,
644
+ storageAdapter,
645
+ queryFieldRuntimeByField
646
+ })
543
647
 
544
648
  // Apply pagination
545
649
  // Check if page object has any actual pagination parameters
@@ -577,15 +681,10 @@ export const RestApiKnexPlugin = {
577
681
  query
578
682
  .limit(pageSize)
579
683
  .offset((pageNumber - 1) * pageSize)
580
- }
581
- // Cursor-based pagination
582
- else if (queryParams.page.after || queryParams.page.before) {
684
+ } else if (queryParams.page.after || queryParams.page.before) {
583
685
  // Fetch one extra record to determine if there are more
584
686
  query.limit(pageSize + 1)
585
687
 
586
- // Get the configured ID property
587
- const idProperty = context.schemaInfo.idProperty || 'id'
588
-
589
688
  if (queryParams.page.after) {
590
689
  let cursorData
591
690
  try {
@@ -604,56 +703,16 @@ export const RestApiKnexPlugin = {
604
703
  )
605
704
  }
606
705
 
607
- // Build sort fields array with directions
608
- const sortFields = []
609
- if (queryParams.sort && queryParams.sort.length > 0) {
610
- queryParams.sort.forEach(sortField => {
611
- const desc = sortField.startsWith('-')
612
- const field = desc ? sortField.substring(1) : sortField
613
- sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
614
- })
615
- } else if (scope.vars.defaultSort) {
616
- sortFields.push({
617
- field: scope.vars.defaultSort.field || idProperty,
618
- direction: scope.vars.defaultSort.direction || 'ASC'
619
- })
620
- } else {
621
- sortFields.push({ field: idProperty, direction: 'ASC' })
622
- }
623
-
624
- // Build compound WHERE clause for multi-field cursor
625
- query.where(function () {
626
- sortFields.forEach((sortInfo, index) => {
627
- const { field, direction } = sortInfo
628
- const qualifiedField = `${tableName}.${field}`
629
- const cursorValue = cursorData[field]
630
-
631
- if (cursorValue === undefined) {
632
- log.warn(`Cursor missing value for sort field: ${field}`)
633
- return
634
- }
635
-
636
- // Build condition for this level and all previous levels
637
- this.orWhere(function () {
638
- // All previous fields must be equal
639
- for (let i = 0; i < index; i++) {
640
- const prevField = sortFields[i].field
641
- const prevQualifiedField = `${tableName}.${prevField}`
642
- const prevValue = cursorData[prevField]
643
- if (prevValue !== undefined) {
644
- this.where(prevQualifiedField, '=', prevValue)
645
- }
646
- }
647
-
648
- // Current field must be greater/less than cursor value
649
- if (direction === 'DESC') {
650
- this.where(qualifiedField, '<', cursorValue)
651
- } else {
652
- this.where(qualifiedField, '>', cursorValue)
653
- }
654
- })
655
- })
656
- })
706
+ applyCursorPredicate(
707
+ query,
708
+ sortDescriptors,
709
+ cursorData,
710
+ (direction) => (direction === 'DESC' ? '<' : '>'),
711
+ (builder, descriptor, operator, value) => builder.where(descriptor.column, operator, value),
712
+ {
713
+ onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
714
+ }
715
+ )
657
716
  } else if (queryParams.page.before) {
658
717
  let cursorData
659
718
  try {
@@ -672,60 +731,18 @@ export const RestApiKnexPlugin = {
672
731
  )
673
732
  }
674
733
 
675
- // Build sort fields array with directions
676
- const sortFields = []
677
- if (queryParams.sort && queryParams.sort.length > 0) {
678
- queryParams.sort.forEach(sortField => {
679
- const desc = sortField.startsWith('-')
680
- const field = desc ? sortField.substring(1) : sortField
681
- sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
682
- })
683
- } else if (scope.vars.defaultSort) {
684
- sortFields.push({
685
- field: scope.vars.defaultSort.field || idProperty,
686
- direction: scope.vars.defaultSort.direction || 'ASC'
687
- })
688
- } else {
689
- sortFields.push({ field: idProperty, direction: 'ASC' })
690
- }
691
-
692
- // Build compound WHERE clause for multi-field cursor (reversed for before)
693
- query.where(function () {
694
- sortFields.forEach((sortInfo, index) => {
695
- const { field, direction } = sortInfo
696
- const qualifiedField = `${tableName}.${field}`
697
- const cursorValue = cursorData[field]
698
-
699
- if (cursorValue === undefined) {
700
- log.warn(`Cursor missing value for sort field: ${field}`)
701
- return
702
- }
703
-
704
- // Build condition for this level and all previous levels
705
- this.orWhere(function () {
706
- // All previous fields must be equal
707
- for (let i = 0; i < index; i++) {
708
- const prevField = sortFields[i].field
709
- const prevQualifiedField = `${tableName}.${prevField}`
710
- const prevValue = cursorData[prevField]
711
- if (prevValue !== undefined) {
712
- this.where(prevQualifiedField, '=', prevValue)
713
- }
714
- }
715
-
716
- // Current field must be less/greater than cursor value (reversed)
717
- if (direction === 'DESC') {
718
- this.where(qualifiedField, '>', cursorValue)
719
- } else {
720
- this.where(qualifiedField, '<', cursorValue)
721
- }
722
- })
723
- })
724
- })
734
+ applyCursorPredicate(
735
+ query,
736
+ sortDescriptors,
737
+ cursorData,
738
+ (direction) => (direction === 'DESC' ? '>' : '<'),
739
+ (builder, descriptor, operator, value) => builder.where(descriptor.column, operator, value),
740
+ {
741
+ onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
742
+ }
743
+ )
725
744
  }
726
- }
727
- // Default pagination if only size is specified - treat as cursor-based for "load more"
728
- else {
745
+ } else {
729
746
  // Fetch one extra to detect hasMore
730
747
  query.limit(pageSize + 1)
731
748
  }
@@ -844,12 +861,7 @@ export const RestApiKnexPlugin = {
844
861
  records.pop()
845
862
  }
846
863
 
847
- // Determine sort fields for cursor
848
- const idProperty = context.schemaInfo.idProperty || 'id'
849
- let sortFields = [idProperty]
850
- if (queryParams.sort && queryParams.sort.length > 0) {
851
- sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
852
- }
864
+ const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
853
865
 
854
866
  context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
855
867
  const urlPrefix = getUrlPrefix(context, scope)
@@ -865,7 +877,7 @@ export const RestApiKnexPlugin = {
865
877
  }
866
878
 
867
879
  // Load relationship identifiers for all hasMany relationships
868
- await loadRelationshipIdentifiers(records, scopeName, scopes, db)
880
+ await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
869
881
 
870
882
  // Process includes
871
883
  const included = await processIncludes(scope, records, {
@@ -901,7 +913,6 @@ export const RestApiKnexPlugin = {
901
913
  if (storageAdapter) {
902
914
  context.storageAdapter = storageAdapter
903
915
  }
904
- const scope = api.resources[scopeName]
905
916
  const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
906
917
  const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
907
918
  const db = context.db || api.knex.instance
@@ -909,18 +920,43 @@ export const RestApiKnexPlugin = {
909
920
 
910
921
  log.debug(`[Knex] POST ${tableName}`, inputRecord)
911
922
 
912
- // Extract attributes from JSON:API format
913
- const attributes = inputRecord.data.attributes
923
+ // Extract attributes from JSON:API format.
924
+ // POST may legitimately provide a resource id when the table uses a custom primary key
925
+ // (for example, one-to-one rows keyed by user_id/workspace_id). Keep that id in the
926
+ // attribute bag so storage translation can write it to the configured id column.
927
+ const attributes = {
928
+ ...(inputRecord.data.attributes || {}),
929
+ ...(inputRecord.data.id !== undefined ? { id: inputRecord.data.id } : {})
930
+ }
914
931
 
915
932
  // Strip non-database fields (computed and virtual) before insert
916
- const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
933
+ const dbAttributes = storageAdapter?.toStorageRow
934
+ ? storageAdapter.toStorageRow(attributes, {
935
+ context,
936
+ operation: 'post'
937
+ })
938
+ : attributes
939
+
940
+ const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
917
941
 
918
942
  // Insert and get the new ID
919
943
  const result = await db(tableName).insert(dbAttributes).returning(idProperty)
920
944
 
921
- // Extract the ID value (SQLite returns array of objects)
922
- const id = result[0]?.[idProperty] || result[0]
923
- return id
945
+ // Extract the ID value. Some dialects ignore .returning() for inserts and can
946
+ // yield 0 even when the caller explicitly supplied the primary key.
947
+ const returnedId = Array.isArray(result)
948
+ ? (result[0]?.[idProperty] ?? result[0])
949
+ : (result?.[idProperty] ?? result)
950
+
951
+ if (
952
+ explicitId !== undefined &&
953
+ explicitId !== null &&
954
+ (returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
955
+ ) {
956
+ return explicitId
957
+ }
958
+
959
+ return returnedId
924
960
  }
925
961
 
926
962
  /**
@@ -964,7 +1000,12 @@ export const RestApiKnexPlugin = {
964
1000
  const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
965
1001
 
966
1002
  // Strip non-database fields (computed and virtual) before database operation
967
- const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
1003
+ const finalAttributes = storageAdapter?.toStorageRow
1004
+ ? storageAdapter.toStorageRow(mergedAttributes, {
1005
+ context,
1006
+ operation: 'put'
1007
+ })
1008
+ : mergedAttributes
968
1009
 
969
1010
  // Map 'id' to actual idProperty if needed (for PUT with specific ID)
970
1011
  if (idProperty !== 'id' && inputRecord.data.id) {
@@ -1063,7 +1104,12 @@ export const RestApiKnexPlugin = {
1063
1104
  const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
1064
1105
 
1065
1106
  // Strip non-database fields (computed and virtual) before database operation
1066
- const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
1107
+ const finalAttributes = storageAdapter?.toStorageRow
1108
+ ? storageAdapter.toStorageRow(mergedAttributes, {
1109
+ context,
1110
+ operation: 'patch'
1111
+ })
1112
+ : mergedAttributes
1067
1113
 
1068
1114
  log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
1069
1115
 
@@ -1097,7 +1143,6 @@ export const RestApiKnexPlugin = {
1097
1143
  if (storageAdapter) {
1098
1144
  context.storageAdapter = storageAdapter
1099
1145
  }
1100
- const scope = api.resources[scopeName]
1101
1146
  const id = context.id
1102
1147
 
1103
1148
  const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName