json-rest-api 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -1,553 +0,0 @@
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 { createMultiHomeApi } from './fixtures/api-configs.js'
13
-
14
- // Create Knex instance for tests - always use SQLite in-memory
15
- const knex = knexLib({
16
- client: 'better-sqlite3',
17
- connection: {
18
- filename: ':memory:'
19
- },
20
- useNullAsDefault: true
21
- })
22
-
23
- // API instance that persists across ALL tests
24
- let api
25
-
26
- describe('MultiHome Plugin Tests', () => {
27
- // IMPORTANT: before() runs ONCE for the entire test suite
28
- before(async () => {
29
- // Create API instance ONCE - this is reused for all tests
30
- api = await createMultiHomeApi(knex)
31
- })
32
-
33
- // IMPORTANT: after() cleans up resources
34
- after(async () => {
35
- // Always destroy knex connection to allow tests to exit
36
- await knex.destroy()
37
- })
38
-
39
- // IMPORTANT: beforeEach() cleans data but does NOT recreate API
40
- beforeEach(async () => {
41
- // Clean all tables - list ALL tables your tests use
42
- await cleanTables(knex, [
43
- 'multihome_projects',
44
- 'multihome_tasks',
45
- 'multihome_users',
46
- 'multihome_system_settings'
47
- ])
48
- })
49
-
50
- describe('Tenant Isolation', () => {
51
- it('should automatically set tenant_id on new records', async () => {
52
- // Create a project for tenant A
53
- const doc = createJsonApiDocument('projects', {
54
- name: 'Tenant A Project',
55
- description: 'A project for tenant A'
56
- })
57
-
58
- // Create with tenant context
59
- const createResult = await api.resources.projects.post({
60
- inputRecord: doc,
61
- simplified: false
62
- }, {
63
- auth: { multihome_id: 'tenant-a' }
64
- })
65
-
66
- // Verify response
67
- validateJsonApiStructure(createResult)
68
- assert.equal(createResult.data.type, 'projects')
69
- assertResourceAttributes(createResult.data, {
70
- name: 'Tenant A Project',
71
- description: 'A project for tenant A',
72
- tenant_id: 'tenant-a' // Should be automatically set
73
- })
74
- })
75
-
76
- it('should filter queries by tenant_id', async () => {
77
- // Create projects for different tenants
78
- const projectA1 = await api.resources.projects.post({
79
- inputRecord: createJsonApiDocument('projects', {
80
- name: 'Project A1'
81
- }),
82
- simplified: false
83
- }, {
84
- auth: { multihome_id: 'tenant-a' }
85
- })
86
-
87
- const projectA2 = await api.resources.projects.post({
88
- inputRecord: createJsonApiDocument('projects', {
89
- name: 'Project A2'
90
- }),
91
- simplified: false
92
- }, {
93
- auth: { multihome_id: 'tenant-a' }
94
- })
95
-
96
- const projectB1 = await api.resources.projects.post({
97
- inputRecord: createJsonApiDocument('projects', {
98
- name: 'Project B1'
99
- }),
100
- simplified: false
101
- }, {
102
- auth: { multihome_id: 'tenant-b' }
103
- })
104
-
105
- // Query as tenant A - should only see tenant A's projects
106
- const resultA = await api.resources.projects.query({
107
- simplified: false
108
- }, {
109
- auth: { multihome_id: 'tenant-a' }
110
- })
111
-
112
- validateJsonApiStructure(resultA, true) // true for collection
113
- assert.equal(resultA.data.length, 2, 'Tenant A should see 2 projects')
114
- assert(resultA.data.every(p => p.attributes.name.startsWith('Project A')))
115
-
116
- // Query as tenant B - should only see tenant B's projects
117
- const resultB = await api.resources.projects.query({
118
- simplified: false
119
- }, {
120
- auth: { multihome_id: 'tenant-b' }
121
- })
122
-
123
- validateJsonApiStructure(resultB, true) // true for collection
124
- assert.equal(resultB.data.length, 1, 'Tenant B should see 1 project')
125
- assert.equal(resultB.data[0].attributes.name, 'Project B1')
126
- })
127
-
128
- it('should prevent access to other tenant records', async () => {
129
- // Create a project for tenant A
130
- const projectA = await api.resources.projects.post({
131
- inputRecord: createJsonApiDocument('projects', {
132
- name: 'Tenant A Secret Project'
133
- }),
134
- simplified: false
135
- }, {
136
- auth: { multihome_id: 'tenant-a' }
137
- })
138
-
139
- // Try to access as tenant B - should fail
140
- await assert.rejects(
141
- async () => {
142
- await api.resources.projects.get({
143
- id: projectA.data.id,
144
- simplified: false
145
- }, {
146
- auth: { multihome_id: 'tenant-b' }
147
- })
148
- },
149
- (err) => {
150
- return err.code === 'REST_API_RESOURCE'
151
- },
152
- 'Should not be able to access other tenant data'
153
- )
154
- })
155
-
156
- it('should prevent updating tenant_id to different value', async () => {
157
- // Create a project for tenant A
158
- const project = await api.resources.projects.post({
159
- inputRecord: createJsonApiDocument('projects', {
160
- name: 'Original Project'
161
- }),
162
- simplified: false
163
- }, {
164
- auth: { multihome_id: 'tenant-a' }
165
- })
166
-
167
- // Try to update with different tenant_id - should fail
168
- await assert.rejects(
169
- async () => {
170
- await api.resources.projects.patch({
171
- id: project.data.id,
172
- inputRecord: {
173
- data: {
174
- type: 'projects',
175
- id: project.data.id,
176
- attributes: {
177
- name: 'Updated Project',
178
- tenant_id: 'tenant-b' // Trying to change tenant
179
- }
180
- }
181
- },
182
- simplified: false
183
- }, {
184
- auth: { multihome_id: 'tenant-a' }
185
- })
186
- },
187
- (err) => {
188
- console.log('PATCH error:', err.message)
189
- console.log('PATCH error stack:', err.stack)
190
- return err.message.includes('must match current context')
191
- },
192
- 'Should not be able to change tenant_id'
193
- )
194
- })
195
- })
196
-
197
- describe('Relationships Across Tenants', () => {
198
- it('should maintain tenant isolation for related records', async () => {
199
- // Create projects for different tenants
200
- const projectA = await api.resources.projects.post({
201
- inputRecord: createJsonApiDocument('projects', {
202
- name: 'Tenant A Project'
203
- }),
204
- simplified: false
205
- }, {
206
- auth: { multihome_id: 'tenant-a' }
207
- })
208
-
209
- const projectB = await api.resources.projects.post({
210
- inputRecord: createJsonApiDocument('projects', {
211
- name: 'Tenant B Project'
212
- }),
213
- simplified: false
214
- }, {
215
- auth: { multihome_id: 'tenant-b' }
216
- })
217
-
218
- // Create task for tenant A project
219
- const taskA = await api.resources.tasks.post({
220
- inputRecord: createJsonApiDocument('tasks',
221
- {
222
- title: 'Task for Tenant A'
223
- },
224
- {
225
- project: createRelationship(resourceIdentifier('projects', projectA.data.id))
226
- }
227
- ),
228
- simplified: false
229
- }, {
230
- auth: { multihome_id: 'tenant-a' }
231
- })
232
-
233
- // Try to create task for tenant B project as tenant A - should fail
234
- await assert.rejects(
235
- async () => {
236
- await api.resources.tasks.post({
237
- inputRecord: createJsonApiDocument('tasks',
238
- {
239
- title: 'Invalid Task'
240
- },
241
- {
242
- project: createRelationship(resourceIdentifier('projects', projectB.data.id))
243
- }
244
- ),
245
- simplified: false
246
- }, {
247
- auth: { multihome_id: 'tenant-a' }
248
- })
249
- },
250
- (err) => {
251
- // Should fail because project belongs to different tenant
252
- return err.code === 'REST_API_VALIDATION' || err.code === 'REST_API_RESOURCE'
253
- },
254
- 'Should not be able to link to other tenant resources'
255
- )
256
-
257
- // Query with includes - should only see own tenant data
258
- const projectWithTasks = await api.resources.projects.get({
259
- id: projectA.data.id,
260
- queryParams: {
261
- include: ['tasks']
262
- },
263
- simplified: false
264
- }, {
265
- auth: { multihome_id: 'tenant-a' }
266
- })
267
-
268
- validateJsonApiStructure(projectWithTasks)
269
- assert(projectWithTasks.included, 'Should have included tasks')
270
- assert.equal(projectWithTasks.included.length, 1, 'Should include one task')
271
- assert.equal(projectWithTasks.included[0].attributes.tenant_id, 'tenant-a')
272
- })
273
- })
274
-
275
- describe('Excluded Resources', () => {
276
- it('should allow access to excluded resources without tenant context', async () => {
277
- // Create system setting without tenant context
278
- const setting = await api.resources.system_settings.post({
279
- inputRecord: createJsonApiDocument('system_settings', {
280
- key: 'app.version',
281
- value: '1.0.0'
282
- }),
283
- simplified: false
284
- })
285
-
286
- validateJsonApiStructure(setting)
287
- assert.equal(setting.data.type, 'system_settings')
288
- assert(!setting.data.attributes.tenant_id, 'Should not have tenant_id')
289
-
290
- // Query without tenant context should work
291
- const allSettings = await api.resources.system_settings.query({
292
- simplified: false
293
- })
294
-
295
- assert.equal(allSettings.data.length, 1)
296
-
297
- // Should be accessible by any tenant
298
- const settingByTenantA = await api.resources.system_settings.get({
299
- id: setting.data.id,
300
- simplified: false
301
- }, {
302
- auth: { multihome_id: 'tenant-a' }
303
- })
304
-
305
- assert.equal(settingByTenantA.data.id, setting.data.id)
306
- })
307
- })
308
-
309
- describe('Configuration Options', () => {
310
- it('should require tenant context when requireAuth is true', async () => {
311
- // Try to create without tenant context - should fail
312
- await assert.rejects(
313
- async () => {
314
- await api.resources.projects.post({
315
- inputRecord: createJsonApiDocument('projects', {
316
- name: 'No Tenant Project'
317
- }),
318
- simplified: false
319
- })
320
- },
321
- (err) => {
322
- return err.message.includes('multihome context')
323
- },
324
- 'Should require tenant context when requireAuth is true'
325
- )
326
- })
327
-
328
- it('should validate resources have tenant field when allowMissing is false', async () => {
329
- // This test validates that the plugin threw an error during setup
330
- // if a resource was missing the tenant field. Since our test resources
331
- // all have tenant_id (except excluded ones), we can verify the
332
- // configuration is working by checking vars
333
- assert.equal(api.vars.multihome.field, 'tenant_id')
334
- assert.equal(api.vars.multihome.allowMissing, false)
335
- assert(api.vars.multihome.excludeResources.includes('system_settings'))
336
- })
337
- })
338
-
339
- describe('Complex Queries', () => {
340
- it('should apply tenant filter to filtered queries', async () => {
341
- // Create users for different tenants
342
- await api.resources.users.post({
343
- inputRecord: createJsonApiDocument('users', {
344
- email: 'admin@tenant-a.com',
345
- name: 'Admin A',
346
- role: 'admin'
347
- }),
348
- simplified: false
349
- }, {
350
- auth: { multihome_id: 'tenant-a' }
351
- })
352
-
353
- await api.resources.users.post({
354
- inputRecord: createJsonApiDocument('users', {
355
- email: 'user@tenant-a.com',
356
- name: 'User A',
357
- role: 'member'
358
- }),
359
- simplified: false
360
- }, {
361
- auth: { multihome_id: 'tenant-a' }
362
- })
363
-
364
- await api.resources.users.post({
365
- inputRecord: createJsonApiDocument('users', {
366
- email: 'admin@tenant-b.com',
367
- name: 'Admin B',
368
- role: 'admin'
369
- }),
370
- simplified: false
371
- }, {
372
- auth: { multihome_id: 'tenant-b' }
373
- })
374
-
375
- // Query for admins as tenant A - should only see tenant A admin
376
- const adminsA = await api.resources.users.query({
377
- queryParams: {
378
- filters: { role: 'admin' }
379
- },
380
- simplified: false
381
- }, {
382
- auth: { multihome_id: 'tenant-a' }
383
- })
384
-
385
- assert.equal(adminsA.data.length, 1, 'Should only see tenant A admin')
386
- assert.equal(adminsA.data[0].attributes.email, 'admin@tenant-a.com')
387
-
388
- // Query for all users as tenant B - should only see tenant B users
389
- const usersB = await api.resources.users.query({
390
- simplified: false
391
- }, {
392
- auth: { multihome_id: 'tenant-b' }
393
- })
394
-
395
- assert.equal(usersB.data.length, 1, 'Should only see tenant B users')
396
- assert.equal(usersB.data[0].attributes.email, 'admin@tenant-b.com')
397
- })
398
-
399
- it('should apply tenant filter with sorting and pagination', async () => {
400
- // Create multiple projects for tenant A
401
- for (let i = 1; i <= 5; i++) {
402
- await api.resources.projects.post({
403
- inputRecord: createJsonApiDocument('projects', {
404
- name: `Project A${i}`,
405
- status: i % 2 === 0 ? 'active' : 'inactive'
406
- }),
407
- simplified: false
408
- }, {
409
- auth: { multihome_id: 'tenant-a' }
410
- })
411
- }
412
-
413
- // Create projects for tenant B
414
- for (let i = 1; i <= 3; i++) {
415
- await api.resources.projects.post({
416
- inputRecord: createJsonApiDocument('projects', {
417
- name: `Project B${i}`
418
- }),
419
- simplified: false
420
- }, {
421
- auth: { multihome_id: 'tenant-b' }
422
- })
423
- }
424
-
425
- // Query with pagination as tenant A
426
- const page1 = await api.resources.projects.query({
427
- queryParams: {
428
- page: { size: 2, number: 1 },
429
- sort: ['-name'] // Sort by name descending
430
- },
431
- simplified: false
432
- }, {
433
- auth: { multihome_id: 'tenant-a' }
434
- })
435
-
436
- assert.equal(page1.data.length, 2, 'Should return page size')
437
- assert.equal(page1.data[0].attributes.name, 'Project A5', 'Should be sorted descending')
438
- assert.equal(page1.data[1].attributes.name, 'Project A4')
439
-
440
- // Get page 2
441
- const page2 = await api.resources.projects.query({
442
- queryParams: {
443
- page: { size: 2, number: 2 },
444
- sort: ['-name']
445
- },
446
- simplified: false
447
- }, {
448
- auth: { multihome_id: 'tenant-a' }
449
- })
450
-
451
- assert.equal(page2.data.length, 2, 'Should return page size')
452
- assert(page2.data.every(p => p.attributes.tenant_id === 'tenant-a'))
453
- })
454
- })
455
-
456
- describe('Error Handling', () => {
457
- it('should provide clear error when no tenant context available', async () => {
458
- await assert.rejects(
459
- async () => {
460
- await api.resources.projects.query({
461
- simplified: false
462
- })
463
- },
464
- (err) => {
465
- return err.message.includes('No multihome context available')
466
- },
467
- 'Should provide clear error message'
468
- )
469
- })
470
-
471
- it('should handle missing tenant field gracefully for excluded resources', async () => {
472
- // system_settings is excluded and doesn't have tenant_id field
473
- const result = await api.resources.system_settings.post({
474
- inputRecord: createJsonApiDocument('system_settings', {
475
- key: 'test.setting',
476
- value: 'test'
477
- }),
478
- simplified: false
479
- }, {
480
- auth: { multihome_id: 'tenant-a' } // Should be ignored for excluded resources
481
- })
482
-
483
- validateJsonApiStructure(result)
484
- assert(!result.data.attributes.tenant_id, 'Excluded resource should not have tenant_id')
485
- })
486
- })
487
-
488
- describe('Security', () => {
489
- it('should prevent bypassing tenant filter with malicious IDs', async () => {
490
- // Create a project for tenant A
491
- const project = await api.resources.projects.post({
492
- inputRecord: createJsonApiDocument('projects', {
493
- name: 'Secure Project'
494
- }),
495
- simplified: false
496
- }, {
497
- auth: { multihome_id: 'tenant-a' }
498
- })
499
-
500
- // Try various bypass attempts as tenant B
501
- const maliciousIds = [
502
- project.data.id,
503
- `${project.data.id} OR 1=1`,
504
- `${project.data.id}'; DROP TABLE projects; --`
505
- ]
506
-
507
- for (const id of maliciousIds) {
508
- await assert.rejects(
509
- async () => {
510
- await api.resources.projects.get({
511
- id,
512
- simplified: false
513
- }, {
514
- auth: { multihome_id: 'tenant-b' }
515
- })
516
- },
517
- undefined, // Any error is fine
518
- `Should not bypass security with ID: ${id}`
519
- )
520
- }
521
- })
522
-
523
- it('should log security violations', async () => {
524
- // This test verifies that the plugin logs security violations
525
- // In a real implementation, you might capture logs and verify
526
- // For now, we just ensure the security check happens
527
-
528
- // Create data for tenant A
529
- const project = await api.resources.projects.post({
530
- inputRecord: createJsonApiDocument('projects', {
531
- name: 'Tenant A Data'
532
- }),
533
- simplified: false
534
- }, {
535
- auth: { multihome_id: 'tenant-a' }
536
- })
537
-
538
- // Attempt access as tenant B
539
- try {
540
- await api.resources.projects.get({
541
- id: project.data.id,
542
- simplified: false
543
- }, {
544
- auth: { multihome_id: 'tenant-b' }
545
- })
546
- assert.fail('Should have thrown error')
547
- } catch (err) {
548
- // Error is expected - security violation should be logged
549
- assert(err.code === 'REST_API_RESOURCE' || err.message.includes('Access denied'))
550
- }
551
- })
552
- })
553
- })