json-rest-api 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -0,0 +1,463 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import {
5
+ validateJsonApiStructure,
6
+ cleanTables,
7
+ createJsonApiDocument,
8
+ assertResourceAttributes,
9
+ createRelationship,
10
+ resourceIdentifier
11
+ } from './helpers/test-utils.js'
12
+ import { storageMode } from './helpers/storage-mode.js'
13
+ import { createAutoFilterApi } from './fixtures/api-configs.js'
14
+
15
+ const knex = knexLib({
16
+ client: 'better-sqlite3',
17
+ connection: {
18
+ filename: ':memory:'
19
+ },
20
+ useNullAsDefault: true
21
+ })
22
+
23
+ let api
24
+
25
+ const scopedContext = (workspaceId, userId, projectId) => ({
26
+ scopeValues: {
27
+ workspaceId,
28
+ userId,
29
+ ...(projectId !== undefined ? { projectId } : {})
30
+ }
31
+ })
32
+
33
+ const moveTaskOutOfScope = async ({ taskId, workspaceId, userId }) => {
34
+ if (storageMode.isAnyApi()) {
35
+ const descriptor = await api.anyapi.registry.getDescriptor(api.anyapi.tenantId, 'tasks')
36
+ await knex('any_records')
37
+ .where({
38
+ id: Number(taskId),
39
+ tenant_id: api.anyapi.tenantId,
40
+ resource: 'tasks'
41
+ })
42
+ .update({
43
+ [descriptor.fields.workspace_id.slot]: workspaceId,
44
+ [descriptor.fields.user_id.slot]: userId
45
+ })
46
+ return
47
+ }
48
+
49
+ await knex('autofilter_tasks')
50
+ .where({ id: Number(taskId) })
51
+ .update({
52
+ workspace_id: workspaceId,
53
+ user_id: userId
54
+ })
55
+ }
56
+
57
+ describe('AutoFilter Plugin', () => {
58
+ before(async () => {
59
+ api = await createAutoFilterApi(knex)
60
+ })
61
+
62
+ after(async () => {
63
+ await knex.destroy()
64
+ })
65
+
66
+ beforeEach(async () => {
67
+ await cleanTables(knex, [
68
+ 'autofilter_workspace_reports',
69
+ 'autofilter_user_notes',
70
+ 'autofilter_projects',
71
+ 'autofilter_tasks',
72
+ 'autofilter_optional_tasks',
73
+ 'autofilter_system_settings'
74
+ ])
75
+ })
76
+
77
+ it('stamps workspace-scoped records and filters collections by workspace', async () => {
78
+ const reportA = await api.resources.workspace_reports.post({
79
+ inputRecord: createJsonApiDocument('workspace_reports', {
80
+ title: 'Workspace A report'
81
+ }),
82
+ simplified: false
83
+ }, scopedContext('workspace-a', 101))
84
+
85
+ await api.resources.workspace_reports.post({
86
+ inputRecord: createJsonApiDocument('workspace_reports', {
87
+ title: 'Workspace B report'
88
+ }),
89
+ simplified: false
90
+ }, scopedContext('workspace-b', 202))
91
+
92
+ validateJsonApiStructure(reportA)
93
+ assertResourceAttributes(reportA.data, {
94
+ title: 'Workspace A report',
95
+ workspace_id: 'workspace-a'
96
+ })
97
+
98
+ const queryA = await api.resources.workspace_reports.query({
99
+ simplified: false
100
+ }, scopedContext('workspace-a', 999))
101
+
102
+ validateJsonApiStructure(queryA, true)
103
+ assert.equal(queryA.data.length, 1)
104
+ assert.equal(queryA.data[0].attributes.title, 'Workspace A report')
105
+ assert.equal(queryA.data[0].attributes.workspace_id, 'workspace-a')
106
+ })
107
+
108
+ it('stamps user-scoped records and filters collections by user', async () => {
109
+ const noteA = await api.resources.user_notes.post({
110
+ inputRecord: createJsonApiDocument('user_notes', {
111
+ body: 'User 101 note'
112
+ }),
113
+ simplified: false
114
+ }, scopedContext('workspace-a', 101))
115
+
116
+ await api.resources.user_notes.post({
117
+ inputRecord: createJsonApiDocument('user_notes', {
118
+ body: 'User 202 note'
119
+ }),
120
+ simplified: false
121
+ }, scopedContext('workspace-a', 202))
122
+
123
+ validateJsonApiStructure(noteA)
124
+ assertResourceAttributes(noteA.data, {
125
+ body: 'User 101 note',
126
+ user_id: 101
127
+ })
128
+
129
+ const queryA = await api.resources.user_notes.query({
130
+ simplified: false
131
+ }, scopedContext('workspace-b', 101))
132
+
133
+ validateJsonApiStructure(queryA, true)
134
+ assert.equal(queryA.data.length, 1)
135
+ assert.equal(queryA.data[0].attributes.body, 'User 101 note')
136
+ assert.equal(queryA.data[0].attributes.user_id, 101)
137
+ })
138
+
139
+ it('applies composite workspace+user scoping', async () => {
140
+ const ownProject = await api.resources.projects.post({
141
+ inputRecord: createJsonApiDocument('projects', {
142
+ name: 'Workspace A / User 101'
143
+ }),
144
+ simplified: false
145
+ }, scopedContext('workspace-a', 101))
146
+
147
+ await api.resources.projects.post({
148
+ inputRecord: createJsonApiDocument('projects', {
149
+ name: 'Workspace A / User 202'
150
+ }),
151
+ simplified: false
152
+ }, scopedContext('workspace-a', 202))
153
+
154
+ await api.resources.projects.post({
155
+ inputRecord: createJsonApiDocument('projects', {
156
+ name: 'Workspace B / User 101'
157
+ }),
158
+ simplified: false
159
+ }, scopedContext('workspace-b', 101))
160
+
161
+ validateJsonApiStructure(ownProject)
162
+ assertResourceAttributes(ownProject.data, {
163
+ name: 'Workspace A / User 101',
164
+ workspace_id: 'workspace-a',
165
+ user_id: 101
166
+ })
167
+
168
+ const scopedQuery = await api.resources.projects.query({
169
+ simplified: false
170
+ }, scopedContext('workspace-a', 101))
171
+
172
+ validateJsonApiStructure(scopedQuery, true)
173
+ assert.equal(scopedQuery.data.length, 1)
174
+ assert.equal(scopedQuery.data[0].attributes.name, 'Workspace A / User 101')
175
+ })
176
+
177
+ it('treats public resources as unscoped', async () => {
178
+ const created = await api.resources.system_settings.post({
179
+ inputRecord: createJsonApiDocument('system_settings', {
180
+ key: 'app.version',
181
+ value: '1.0.0'
182
+ }),
183
+ simplified: false
184
+ })
185
+
186
+ validateJsonApiStructure(created)
187
+
188
+ const anonymousQuery = await api.resources.system_settings.query({
189
+ simplified: false
190
+ })
191
+ validateJsonApiStructure(anonymousQuery, true)
192
+ assert.equal(anonymousQuery.data.length, 1)
193
+
194
+ const scopedQuery = await api.resources.system_settings.query({
195
+ simplified: false
196
+ }, scopedContext('workspace-a', 101))
197
+ validateJsonApiStructure(scopedQuery, true)
198
+ assert.equal(scopedQuery.data.length, 1)
199
+ })
200
+
201
+ it('scopes single-record lookups so out-of-scope records are not found', async () => {
202
+ const project = await api.resources.projects.post({
203
+ inputRecord: createJsonApiDocument('projects', {
204
+ name: 'Scoped Project'
205
+ }),
206
+ simplified: false
207
+ }, scopedContext('workspace-a', 101))
208
+
209
+ await assert.rejects(
210
+ async () => {
211
+ await api.resources.projects.get({
212
+ id: project.data.id,
213
+ simplified: false
214
+ }, scopedContext('workspace-a', 202))
215
+ },
216
+ (error) => error.code === 'REST_API_RESOURCE',
217
+ 'Out-of-scope records should behave as not found'
218
+ )
219
+ })
220
+
221
+ it('stamps scoped fields on PUT and rejects inconsistent scoped updates', async () => {
222
+ const project = await api.resources.projects.post({
223
+ inputRecord: createJsonApiDocument('projects', {
224
+ name: 'Original Project',
225
+ description: 'Original Description'
226
+ }),
227
+ simplified: false
228
+ }, scopedContext('workspace-a', 101))
229
+
230
+ await api.resources.projects.put({
231
+ inputRecord: {
232
+ data: {
233
+ type: 'projects',
234
+ id: project.data.id,
235
+ attributes: {
236
+ name: 'Updated Project',
237
+ description: 'Updated Description',
238
+ status: 'active'
239
+ }
240
+ }
241
+ },
242
+ simplified: false
243
+ }, scopedContext('workspace-a', 101))
244
+
245
+ const afterPut = await api.resources.projects.get({
246
+ id: project.data.id,
247
+ simplified: false
248
+ }, scopedContext('workspace-a', 101))
249
+
250
+ validateJsonApiStructure(afterPut)
251
+ assertResourceAttributes(afterPut.data, {
252
+ name: 'Updated Project',
253
+ description: 'Updated Description',
254
+ workspace_id: 'workspace-a',
255
+ user_id: 101
256
+ })
257
+
258
+ await assert.rejects(
259
+ async () => {
260
+ await api.resources.projects.patch({
261
+ id: project.data.id,
262
+ inputRecord: {
263
+ data: {
264
+ type: 'projects',
265
+ id: project.data.id,
266
+ attributes: {
267
+ workspace_id: 'workspace-b'
268
+ }
269
+ }
270
+ },
271
+ simplified: false
272
+ }, scopedContext('workspace-a', 101))
273
+ },
274
+ (error) => error.code === 'REST_API_VALIDATION',
275
+ 'PATCH should reject inconsistent scope-field updates'
276
+ )
277
+ })
278
+
279
+ it('uses scoped lookups when validating relationships', async () => {
280
+ const projectA = await api.resources.projects.post({
281
+ inputRecord: createJsonApiDocument('projects', {
282
+ name: 'Project A'
283
+ }),
284
+ simplified: false
285
+ }, scopedContext('workspace-a', 101))
286
+
287
+ const projectB = await api.resources.projects.post({
288
+ inputRecord: createJsonApiDocument('projects', {
289
+ name: 'Project B'
290
+ }),
291
+ simplified: false
292
+ }, scopedContext('workspace-b', 101))
293
+
294
+ const task = await api.resources.tasks.post({
295
+ inputRecord: createJsonApiDocument('tasks',
296
+ {
297
+ title: 'Task A'
298
+ },
299
+ {
300
+ project: createRelationship(resourceIdentifier('projects', projectA.data.id))
301
+ }
302
+ ),
303
+ simplified: false
304
+ }, scopedContext('workspace-a', 101))
305
+
306
+ validateJsonApiStructure(task)
307
+ assertResourceAttributes(task.data, {
308
+ title: 'Task A',
309
+ workspace_id: 'workspace-a',
310
+ user_id: 101
311
+ })
312
+
313
+ await assert.rejects(
314
+ async () => {
315
+ await api.resources.tasks.post({
316
+ inputRecord: createJsonApiDocument('tasks',
317
+ {
318
+ title: 'Cross-scope Task'
319
+ },
320
+ {
321
+ project: createRelationship(resourceIdentifier('projects', projectB.data.id))
322
+ }
323
+ ),
324
+ simplified: false
325
+ }, scopedContext('workspace-a', 101))
326
+ },
327
+ (error) => error.code === 'REST_API_RESOURCE' || error.code === 'REST_API_VALIDATION',
328
+ 'Relationship validation should respect scoped datasets'
329
+ )
330
+ })
331
+
332
+ it('scopes included resources through the same autofilter path as top-level queries', async () => {
333
+ const project = await api.resources.projects.post({
334
+ inputRecord: createJsonApiDocument('projects', {
335
+ name: 'Scoped include project'
336
+ }),
337
+ simplified: false
338
+ }, scopedContext('workspace-a', 101))
339
+
340
+ const visibleTask = await api.resources.tasks.post({
341
+ inputRecord: createJsonApiDocument('tasks',
342
+ {
343
+ title: 'Visible task'
344
+ },
345
+ {
346
+ project: createRelationship(resourceIdentifier('projects', project.data.id))
347
+ }
348
+ ),
349
+ simplified: false
350
+ }, scopedContext('workspace-a', 101))
351
+
352
+ const hiddenTask = await api.resources.tasks.post({
353
+ inputRecord: createJsonApiDocument('tasks',
354
+ {
355
+ title: 'Out-of-scope task'
356
+ },
357
+ {
358
+ project: createRelationship(resourceIdentifier('projects', project.data.id))
359
+ }
360
+ ),
361
+ simplified: false
362
+ }, scopedContext('workspace-a', 101))
363
+
364
+ await moveTaskOutOfScope({
365
+ taskId: hiddenTask.data.id,
366
+ workspaceId: 'workspace-b',
367
+ userId: 202
368
+ })
369
+
370
+ const result = await api.resources.projects.get({
371
+ id: project.data.id,
372
+ queryParams: {
373
+ include: ['tasks']
374
+ },
375
+ simplified: false
376
+ }, scopedContext('workspace-a', 101))
377
+
378
+ validateJsonApiStructure(result)
379
+ assert.deepEqual(result.data.relationships.tasks.data, [
380
+ resourceIdentifier('tasks', visibleTask.data.id)
381
+ ])
382
+ assert.equal(result.included?.length, 1)
383
+ assert.equal(result.included?.[0]?.id, visibleTask.data.id)
384
+ assert.equal(result.included?.[0]?.attributes.title, 'Visible task')
385
+ })
386
+
387
+ it('treats null belongsTo autofilter values as valid null linkage on writes', async () => {
388
+ const implicitNull = await api.resources.optional_tasks.post({
389
+ inputRecord: createJsonApiDocument('optional_tasks', {
390
+ title: 'Implicit null project'
391
+ }),
392
+ simplified: false
393
+ }, scopedContext('workspace-a', 101))
394
+
395
+ const explicitNull = await api.resources.optional_tasks.post({
396
+ inputRecord: createJsonApiDocument(
397
+ 'optional_tasks',
398
+ {
399
+ title: 'Explicit null project'
400
+ },
401
+ {
402
+ project: { data: null }
403
+ }
404
+ ),
405
+ simplified: false
406
+ }, scopedContext('workspace-a', 101))
407
+
408
+ const implicitFetched = await api.resources.optional_tasks.get({
409
+ id: implicitNull.data.id,
410
+ simplified: false
411
+ }, scopedContext('workspace-a', 101))
412
+
413
+ const explicitFetched = await api.resources.optional_tasks.get({
414
+ id: explicitNull.data.id,
415
+ simplified: false
416
+ }, scopedContext('workspace-a', 101))
417
+
418
+ validateJsonApiStructure(implicitFetched)
419
+ validateJsonApiStructure(explicitFetched)
420
+ assert.equal(implicitFetched.data.relationships.project.data, null)
421
+ assert.equal(explicitFetched.data.relationships.project.data, null)
422
+ })
423
+
424
+ it('fails cleanly when a required autofilter value is missing', async () => {
425
+ await assert.rejects(
426
+ async () => {
427
+ await api.resources.projects.query({
428
+ simplified: false
429
+ })
430
+ },
431
+ (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
432
+ 'Scoped resources should require configured scope values'
433
+ )
434
+
435
+ await assert.rejects(
436
+ async () => {
437
+ await api.resources.projects.post({
438
+ inputRecord: createJsonApiDocument('projects', {
439
+ name: 'No Scope'
440
+ }),
441
+ simplified: false
442
+ })
443
+ },
444
+ (error) => error.code === 'REST_API_AUTOFILTER_CONTEXT',
445
+ 'Scoped writes should require configured scope values'
446
+ )
447
+ })
448
+
449
+ it('exposes compiled preset configuration without auth semantics', () => {
450
+ assert.deepEqual(api.autofilter.getConfig(), {
451
+ presets: ['public', 'workspace', 'user', 'workspace_user'],
452
+ resolvers: ['workspace', 'user', 'currentProject']
453
+ })
454
+
455
+ assert.deepEqual(api.autofilter.getScopeConfig('projects'), {
456
+ preset: 'workspace_user',
457
+ filters: [
458
+ { field: 'workspace_id', resolver: 'workspace', required: true },
459
+ { field: 'user_id', resolver: 'user', required: true }
460
+ ]
461
+ })
462
+ })
463
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import { Api } from 'hooked-api'
5
+ import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
6
+ import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
7
+ import { cleanTables } from './helpers/test-utils.js'
8
+
9
+ const knex = knexLib({
10
+ client: 'better-sqlite3',
11
+ connection: {
12
+ filename: ':memory:'
13
+ },
14
+ useNullAsDefault: true
15
+ })
16
+
17
+ let api
18
+
19
+ describe('Simplified POST with explicit resource ids', () => {
20
+ before(async () => {
21
+ api = new Api({
22
+ name: 'custom-id-simplified-post-test'
23
+ })
24
+
25
+ await api.use(RestApiPlugin, {
26
+ simplifiedApi: true,
27
+ simplifiedTransport: false,
28
+ returnRecordApi: {
29
+ post: 'full',
30
+ put: 'full',
31
+ patch: 'full'
32
+ }
33
+ })
34
+ await api.use(RestApiKnexPlugin, { knex })
35
+
36
+ await api.addResource('user_settings', {
37
+ tableName: 'test_user_settings',
38
+ idProperty: 'user_id',
39
+ searchSchema: {
40
+ id: { type: 'id', actualField: 'id' }
41
+ },
42
+ schema: {
43
+ theme: { type: 'string', required: true, max: 32 }
44
+ }
45
+ })
46
+ await api.resources.user_settings.createKnexTable()
47
+ })
48
+
49
+ after(async () => {
50
+ await knex.destroy()
51
+ })
52
+
53
+ beforeEach(async () => {
54
+ await cleanTables(knex, ['test_user_settings'])
55
+ })
56
+
57
+ it('persists the provided resource id when posting simplified records to a custom-id resource', async () => {
58
+ const created = await api.resources.user_settings.post({
59
+ id: '7',
60
+ theme: 'dark'
61
+ })
62
+
63
+ assert.equal(created.id, '7')
64
+ assert.equal(created.theme, 'dark')
65
+
66
+ const dbRow = await knex('test_user_settings').where('user_id', 7).first()
67
+ assert.ok(dbRow)
68
+ assert.equal(String(dbRow.user_id), '7')
69
+ assert.equal(dbRow.theme, 'dark')
70
+
71
+ const fetched = await api.resources.user_settings.get({ id: '7' })
72
+ assert.equal(fetched.id, '7')
73
+ assert.equal(fetched.theme, 'dark')
74
+ })
75
+ })
@@ -161,6 +161,14 @@ describe('Custom idProperty Relationship Operations', () => {
161
161
  assert.equal(getResult.included.length, 1)
162
162
  assert.equal(getResult.included[0].type, 'countries')
163
163
  assert.equal(getResult.included[0].attributes.name, 'United States')
164
+ assert.equal(
165
+ getResult.data.relationships.country.links.self,
166
+ `/publishers/${publisherResult.data.id}/relationships/country`
167
+ )
168
+ assert.equal(
169
+ getResult.data.relationships.country.links.related,
170
+ `/publishers/${publisherResult.data.id}/country`
171
+ )
164
172
  })
165
173
 
166
174
  it('should update belongsTo relationship via PATCH', async () => {
@@ -895,5 +903,30 @@ describe('Custom idProperty Relationship Operations', () => {
895
903
  assertResourceRelationship(putResult.data, 'country',
896
904
  resourceIdentifier('countries', country.data.id))
897
905
  })
906
+
907
+ it('should reject PUT when an existing persisted attribute is omitted', async () => {
908
+ const country = await api.resources.countries.post({
909
+ inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
910
+ })
911
+
912
+ await assert.rejects(
913
+ async () => api.resources.countries.put({
914
+ inputRecord: {
915
+ data: {
916
+ type: 'countries',
917
+ id: country.data.id,
918
+ attributes: {
919
+ name: 'Renamed Country'
920
+ }
921
+ }
922
+ }
923
+ }),
924
+ (error) => {
925
+ assert.equal(error.message, 'PUT request omits persisted fields that already have stored values')
926
+ assert(error.details.fields.includes('data.attributes.code'))
927
+ return true
928
+ }
929
+ )
930
+ })
898
931
  })
899
932
  })