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.
- package/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/README.md +22 -1
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +196 -38
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +97 -190
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1111 -160
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
- package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
- package/plugins/core/lib/storage/storage-adapter.js +82 -4
- package/plugins/core/lib/storage/storage-mapping.js +274 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +271 -226
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +148 -92
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +29 -14
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +21 -8
- package/plugins/core/rest-api-plugin-methods/put.js +73 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +6 -0
- package/plugins/core/rest-api-positioning-plugin.js +2 -5
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -4
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +521 -0
- package/tests/db-table-operations.test.js +786 -0
- package/tests/fastify-plugin.test.js +156 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +278 -176
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +259 -0
- package/audit/00-charter.md +0 -103
- package/audit/01-scope-map.md +0 -84
- package/audit/02-coverage-matrix.csv +0 -174
- package/audit/03-invariants.md +0 -123
- package/audit/04-standards-rubric.md +0 -56
- package/audit/05-competitor-rubric.md +0 -77
- package/audit/06-findings.md +0 -160
- package/audit/07-risk-register.csv +0 -10
- package/audit/08-remediation-roadmap.md +0 -177
- package/audit/09-signoff-check.md +0 -23
- package/audit/10-final-synthesis.md +0 -29
- package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
- package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
- package/audit/evidence/code/boundary-leak-checks.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
- package/audit/evidence/code/critical-pass1-summary.csv +0 -67
- package/audit/evidence/code/critical-pass2-summary.csv +0 -67
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
- package/audit/evidence/code/error-model-signals.txt +0 -98
- package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
- package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
- package/audit/evidence/code/hook-flow-signals.txt +0 -282
- package/audit/evidence/code/idproperty-signals.txt +0 -157
- package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
- package/audit/evidence/code/imports-json-rest-api.txt +0 -354
- package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
- package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
- package/audit/evidence/code/permission-signals.txt +0 -193
- package/audit/evidence/code/query-include-signals.txt +0 -687
- package/audit/evidence/code/transaction-signals.txt +0 -28
- package/audit/evidence/code/validation-signals.txt +0 -650
- package/audit/evidence/code/write-path-signals.txt +0 -61
- package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
- package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
- package/audit/evidence/external/corpus-freeze.md +0 -21
- package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
- package/audit/evidence/external/npm-download-signals.txt +0 -23
- package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
- package/audit/evidence/external/readme-fortune-signals.txt +0 -26
- package/audit/evidence/external/readme-fortune.md +0 -165
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
- package/audit/evidence/external/readme-json-api-serializer.md +0 -690
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
- package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
- package/audit/evidence/external/readme-jsonapi-server.md +0 -104
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
- package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
- package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
- package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
- package/audit/evidence/external/standards-sources.txt +0 -6
- package/audit/evidence/runtime/baseline-commits.txt +0 -2
- package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
- package/audit/evidence/runtime/environment.txt +0 -4
- package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
- package/audit/evidence/runtime/test-exitcodes.txt +0 -4
- package/audit/evidence/runtime/test-summary.md +0 -107
- package/audit/evidence/runtime/trace-anchors.txt +0 -61
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
- package/audit/evidence/tests/failing-tests-summary.txt +0 -25
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
- package/audit/evidence/tests/query-include-test-signals.txt +0 -948
- package/audit/evidence/tests/relationship-test-signals.txt +0 -223
- package/audit/evidence/tests/skipped-tests.txt +0 -10
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
- package/audit/evidence/tests/test-suite-list.txt +0 -32
- package/audit/subsystems/docs-drift.md +0 -49
- package/audit/subsystems/pass2-cross-review.md +0 -131
- package/audit/subsystems/plugins-access.md +0 -51
- package/audit/subsystems/plugins-bulk.md +0 -50
- package/audit/subsystems/plugins-cors.md +0 -48
- package/audit/subsystems/plugins-file.md +0 -53
- package/audit/subsystems/plugins-multihome.md +0 -50
- package/audit/subsystems/plugins-positioning.md +0 -54
- package/audit/subsystems/plugins-socketio.md +0 -50
- package/audit/subsystems/query-engine.md +0 -61
- package/audit/subsystems/relationships.md +0 -55
- package/audit/subsystems/rest-orchestration.md +0 -66
- package/audit/subsystems/schema-core.md +0 -64
- package/audit/subsystems/storage-knex.md +0 -57
- package/audit/subsystems/tests.md +0 -48
- package/audit/subsystems/transport-express.md +0 -55
- package/audit/traces/trace-read-get.md +0 -22
- package/audit/traces/trace-read-query.md +0 -25
- package/audit/traces/trace-relationship-endpoints.md +0 -31
- package/audit/traces/trace-write-delete.md +0 -17
- package/audit/traces/trace-write-patch.md +0 -26
- package/audit/traces/trace-write-post.md +0 -22
- package/audit/traces/trace-write-put.md +0 -23
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
# Multi-Tenancy with the MultiHome Plugin
|
|
2
|
-
|
|
3
|
-
The MultiHome plugin provides automatic data isolation for multi-tenant applications. It ensures that users can only access data belonging to their tenant, making it impossible to accidentally or maliciously access data from other tenants.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Overview](#overview)
|
|
8
|
-
- [How It Works](#how-it-works)
|
|
9
|
-
- [Installation](#installation)
|
|
10
|
-
- [Configuration](#configuration)
|
|
11
|
-
- [Usage Examples](#usage-examples)
|
|
12
|
-
- [Security Features](#security-features)
|
|
13
|
-
- [Integration with Other Plugins](#integration-with-other-plugins)
|
|
14
|
-
- [API Reference](#api-reference)
|
|
15
|
-
- [Troubleshooting](#troubleshooting)
|
|
16
|
-
- [Best Practices](#best-practices)
|
|
17
|
-
|
|
18
|
-
## Overview
|
|
19
|
-
|
|
20
|
-
Multi-tenancy is a critical requirement for SaaS applications where multiple customers (tenants) share the same application instance but must have completely isolated data. The MultiHome plugin makes this easy by:
|
|
21
|
-
|
|
22
|
-
- **Automatic Query Filtering**: Every database query automatically includes a WHERE clause for the tenant ID
|
|
23
|
-
- **Automatic Record Assignment**: New records are automatically assigned to the current tenant
|
|
24
|
-
- **Request-Based Tenant Detection**: Extracts tenant ID from subdomain, header, path, or custom logic
|
|
25
|
-
- **Zero Data Leakage**: Makes it impossible to access data from the wrong tenant
|
|
26
|
-
- **Flexible Configuration**: Supports various multi-tenancy strategies
|
|
27
|
-
|
|
28
|
-
## How It Works
|
|
29
|
-
|
|
30
|
-
The plugin operates at multiple levels to ensure complete data isolation:
|
|
31
|
-
|
|
32
|
-
### 1. Tenant Extraction (Transport Layer)
|
|
33
|
-
|
|
34
|
-
When a request arrives, the plugin extracts the tenant ID using a configurable extractor function:
|
|
35
|
-
|
|
36
|
-
```javascript
|
|
37
|
-
// Default: Extract from subdomain
|
|
38
|
-
// mobily.app.com → tenant_id = 'mobily'
|
|
39
|
-
// acme.app.com → tenant_id = 'acme'
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### 2. Query Filtering (Database Layer)
|
|
43
|
-
|
|
44
|
-
Every database query is automatically modified to include the tenant filter:
|
|
45
|
-
|
|
46
|
-
```sql
|
|
47
|
-
-- Original query
|
|
48
|
-
SELECT * FROM posts WHERE status = 'published'
|
|
49
|
-
|
|
50
|
-
-- Modified query (automatic)
|
|
51
|
-
SELECT * FROM posts WHERE status = 'published' AND multihome_id = 'mobily'
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### 3. Record Creation (API Layer)
|
|
55
|
-
|
|
56
|
-
When creating new records, the tenant ID is automatically set:
|
|
57
|
-
|
|
58
|
-
```javascript
|
|
59
|
-
// User sends:
|
|
60
|
-
POST /api/posts
|
|
61
|
-
{ "title": "My Post", "content": "..." }
|
|
62
|
-
|
|
63
|
-
// Plugin automatically adds:
|
|
64
|
-
{ "title": "My Post", "content": "...", "multihome_id": "mobily" }
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Installation
|
|
68
|
-
|
|
69
|
-
1. First, ensure you have the required dependencies:
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
import { Api } from 'hooked-api';
|
|
73
|
-
import { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
|
|
74
|
-
import { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
|
|
75
|
-
import { ExpressPlugin } from './plugins/core/connectors/express-plugin.js';
|
|
76
|
-
import { MultiHomePlugin } from './plugins/core/multihome-plugin.js';
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
2. Create your API and install the plugins:
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
const api = new Api({
|
|
83
|
-
name: 'my-multi-tenant-api',
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Install required plugins first
|
|
87
|
-
await api.use(RestApiPlugin);
|
|
88
|
-
await api.use(RestApiKnexPlugin, { knex: knexInstance });
|
|
89
|
-
await api.use(ExpressPlugin, { app: expressApp });
|
|
90
|
-
|
|
91
|
-
// Install MultiHome plugin
|
|
92
|
-
await api.use(MultiHomePlugin, {
|
|
93
|
-
field: 'tenant_id', // The database field name
|
|
94
|
-
excludeResources: ['migrations'], // Resources to exclude
|
|
95
|
-
requireAuth: true, // Require tenant context
|
|
96
|
-
allowMissing: false, // Require field in schema
|
|
97
|
-
extractor: (request) => { // Custom extraction logic
|
|
98
|
-
// Extract from subdomain
|
|
99
|
-
const host = request.headers.host;
|
|
100
|
-
const subdomain = host.split('.')[0];
|
|
101
|
-
return subdomain;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
3. Add the tenant field to your resource schemas:
|
|
107
|
-
|
|
108
|
-
```javascript
|
|
109
|
-
api.addResource('posts', {
|
|
110
|
-
schema: {
|
|
111
|
-
title: { type: 'string', required: true },
|
|
112
|
-
content: { type: 'string' },
|
|
113
|
-
tenant_id: { type: 'string', required: true } // Required field
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Configuration
|
|
119
|
-
|
|
120
|
-
### Plugin Options
|
|
121
|
-
|
|
122
|
-
| Option | Type | Default | Description |
|
|
123
|
-
|--------|------|---------|-------------|
|
|
124
|
-
| `field` | string | `'multihome_id'` | The database field name for tenant ID |
|
|
125
|
-
| `excludeResources` | array | `['system_migrations', 'system_logs']` | Resources that don't need tenant isolation |
|
|
126
|
-
| `requireAuth` | boolean | `true` | Whether to require tenant context for all operations |
|
|
127
|
-
| `allowMissing` | boolean | `false` | Whether to allow resources without the tenant field |
|
|
128
|
-
| `extractor` | function | Subdomain extractor | Function to extract tenant ID from request |
|
|
129
|
-
|
|
130
|
-
### Default Extractor
|
|
131
|
-
|
|
132
|
-
The default extractor tries multiple sources:
|
|
133
|
-
|
|
134
|
-
1. **Subdomain**: `tenant1.app.com` → `tenant1`
|
|
135
|
-
2. **Header**: `X-Multihome-ID: tenant1`
|
|
136
|
-
3. Returns `null` if no tenant found
|
|
137
|
-
|
|
138
|
-
Common subdomains like 'www', 'api', 'app' are ignored.
|
|
139
|
-
|
|
140
|
-
### Custom Extractors
|
|
141
|
-
|
|
142
|
-
You can provide your own extractor function:
|
|
143
|
-
|
|
144
|
-
```javascript
|
|
145
|
-
// Extract from JWT token
|
|
146
|
-
extractor: (request) => {
|
|
147
|
-
const token = request.headers.authorization?.split(' ')[1];
|
|
148
|
-
if (token) {
|
|
149
|
-
const decoded = jwt.verify(token, secret);
|
|
150
|
-
return decoded.tenant_id;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Extract from URL path
|
|
156
|
-
extractor: (request) => {
|
|
157
|
-
// /api/tenants/acme/posts → 'acme'
|
|
158
|
-
const match = request.path.match(/\/tenants\/([^\/]+)/);
|
|
159
|
-
return match ? match[1] : null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Extract from custom header
|
|
163
|
-
extractor: (request) => {
|
|
164
|
-
return request.headers['x-tenant-id'];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Complex logic with fallbacks
|
|
168
|
-
extractor: (request) => {
|
|
169
|
-
// Try JWT first
|
|
170
|
-
if (request.auth?.claims?.tenant_id) {
|
|
171
|
-
return request.auth.claims.tenant_id;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Then try subdomain
|
|
175
|
-
const subdomain = request.headers.host?.split('.')[0];
|
|
176
|
-
if (subdomain && !['www', 'api'].includes(subdomain)) {
|
|
177
|
-
return subdomain;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Finally try header
|
|
181
|
-
return request.headers['x-customer-id'];
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Usage Examples
|
|
186
|
-
|
|
187
|
-
### Basic Setup with Subdomain-Based Tenancy
|
|
188
|
-
|
|
189
|
-
```javascript
|
|
190
|
-
// Configuration
|
|
191
|
-
await api.use(MultiHomePlugin, {
|
|
192
|
-
field: 'tenant_id',
|
|
193
|
-
extractor: (request) => {
|
|
194
|
-
const host = request.headers.host;
|
|
195
|
-
return host.split('.')[0]; // acme.myapp.com → 'acme'
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Define resources with tenant field
|
|
200
|
-
api.addResource('projects', {
|
|
201
|
-
schema: {
|
|
202
|
-
name: { type: 'string', required: true },
|
|
203
|
-
description: { type: 'string' },
|
|
204
|
-
tenant_id: { type: 'string', required: true }
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
api.addResource('users', {
|
|
209
|
-
schema: {
|
|
210
|
-
email: { type: 'string', required: true },
|
|
211
|
-
name: { type: 'string', required: true },
|
|
212
|
-
tenant_id: { type: 'string', required: true }
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Header-Based Tenancy for APIs
|
|
218
|
-
|
|
219
|
-
```javascript
|
|
220
|
-
// Configuration for API clients that send tenant ID in header
|
|
221
|
-
await api.use(MultiHomePlugin, {
|
|
222
|
-
field: 'organization_id',
|
|
223
|
-
extractor: (request) => {
|
|
224
|
-
const tenantId = request.headers['x-organization-id'];
|
|
225
|
-
if (!tenantId) {
|
|
226
|
-
throw new Error('X-Organization-ID header is required');
|
|
227
|
-
}
|
|
228
|
-
return tenantId;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### JWT-Based Tenancy
|
|
234
|
-
|
|
235
|
-
```javascript
|
|
236
|
-
// Works with the JWT Auth plugin
|
|
237
|
-
await api.use(JwtAuthPlugin, { secret: process.env.JWT_SECRET });
|
|
238
|
-
await api.use(MultiHomePlugin, {
|
|
239
|
-
field: 'company_id',
|
|
240
|
-
extractor: (request) => {
|
|
241
|
-
// JWT plugin sets request.auth
|
|
242
|
-
if (!request.auth?.claims?.company_id) {
|
|
243
|
-
throw new Error('No company context in JWT token');
|
|
244
|
-
}
|
|
245
|
-
return request.auth.claims.company_id;
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Mixed Mode with System Resources
|
|
251
|
-
|
|
252
|
-
```javascript
|
|
253
|
-
// Some resources are tenant-specific, others are global
|
|
254
|
-
await api.use(MultiHomePlugin, {
|
|
255
|
-
field: 'tenant_id',
|
|
256
|
-
excludeResources: [
|
|
257
|
-
'system_settings', // Global settings
|
|
258
|
-
'countries', // Shared reference data
|
|
259
|
-
'currencies', // Shared reference data
|
|
260
|
-
'audit_logs' // System-wide audit trail
|
|
261
|
-
],
|
|
262
|
-
allowMissing: true // Allow resources without tenant_id field
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Tenant-specific resource
|
|
266
|
-
api.addResource('invoices', {
|
|
267
|
-
schema: {
|
|
268
|
-
number: { type: 'string', required: true },
|
|
269
|
-
amount: { type: 'number', required: true },
|
|
270
|
-
tenant_id: { type: 'string', required: true }
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Global resource (no tenant_id)
|
|
275
|
-
api.addResource('countries', {
|
|
276
|
-
schema: {
|
|
277
|
-
code: { type: 'string', required: true },
|
|
278
|
-
name: { type: 'string', required: true }
|
|
279
|
-
// No tenant_id field
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## Security Features
|
|
285
|
-
|
|
286
|
-
### 1. Automatic Query Filtering
|
|
287
|
-
|
|
288
|
-
Every query is automatically filtered at the database level:
|
|
289
|
-
|
|
290
|
-
```javascript
|
|
291
|
-
// User tries to access another tenant's data
|
|
292
|
-
GET /api/posts/123
|
|
293
|
-
|
|
294
|
-
// Even if post 123 belongs to another tenant, the query becomes:
|
|
295
|
-
SELECT * FROM posts WHERE id = 123 AND tenant_id = 'current-tenant'
|
|
296
|
-
// Result: 404 Not Found (not a security error message)
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### 2. Validation on Write Operations
|
|
300
|
-
|
|
301
|
-
The plugin validates tenant context on all write operations:
|
|
302
|
-
|
|
303
|
-
```javascript
|
|
304
|
-
// User tries to create a record with wrong tenant_id
|
|
305
|
-
POST /api/posts
|
|
306
|
-
{
|
|
307
|
-
"title": "Hacking attempt",
|
|
308
|
-
"tenant_id": "other-tenant" // This will be rejected
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Error: Cannot set tenant_id to 'other-tenant' - must match current context
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### 3. Security Logging
|
|
315
|
-
|
|
316
|
-
Security violations are logged for monitoring:
|
|
317
|
-
|
|
318
|
-
```javascript
|
|
319
|
-
// When someone tries to access wrong tenant data
|
|
320
|
-
log.error('Multihome security violation attempt', {
|
|
321
|
-
scopeName: 'posts',
|
|
322
|
-
recordId: 123,
|
|
323
|
-
recordMultihomeId: 'tenant-a',
|
|
324
|
-
contextMultihomeId: 'tenant-b'
|
|
325
|
-
});
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### 4. Fail-Safe Design
|
|
329
|
-
|
|
330
|
-
If tenant context is missing and `requireAuth: true`:
|
|
331
|
-
|
|
332
|
-
```javascript
|
|
333
|
-
// No tenant context available
|
|
334
|
-
GET /api/posts
|
|
335
|
-
// Error: No multihome context available - cannot execute query
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
## Integration with Other Plugins
|
|
339
|
-
|
|
340
|
-
### With JWT Auth Plugin
|
|
341
|
-
|
|
342
|
-
The MultiHome plugin works seamlessly with JWT authentication:
|
|
343
|
-
|
|
344
|
-
```javascript
|
|
345
|
-
// JWT token contains tenant information
|
|
346
|
-
{
|
|
347
|
-
"sub": "user123",
|
|
348
|
-
"email": "user@example.com",
|
|
349
|
-
"tenant_id": "acme-corp",
|
|
350
|
-
"exp": 1234567890
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// MultiHome extractor uses the JWT claim
|
|
354
|
-
await api.use(MultiHomePlugin, {
|
|
355
|
-
extractor: (request) => request.auth?.claims?.tenant_id
|
|
356
|
-
});
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### With Express Plugin
|
|
360
|
-
|
|
361
|
-
The Express plugin provides the request object that MultiHome uses:
|
|
362
|
-
|
|
363
|
-
```javascript
|
|
364
|
-
// Express middleware sets up request
|
|
365
|
-
app.use('/api', (req, res) => {
|
|
366
|
-
// MultiHome extractor receives the Express request object
|
|
367
|
-
// with headers, path, auth, etc.
|
|
368
|
-
});
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### With REST API Plugin
|
|
372
|
-
|
|
373
|
-
MultiHome integrates at multiple points in the REST API lifecycle:
|
|
374
|
-
|
|
375
|
-
1. **Before Schema Validation**: Sets tenant_id on new records
|
|
376
|
-
2. **Before Data Operations**: Validates tenant access
|
|
377
|
-
3. **During Query Building**: Adds WHERE clauses
|
|
378
|
-
|
|
379
|
-
## API Reference
|
|
380
|
-
|
|
381
|
-
### Configuration API
|
|
382
|
-
|
|
383
|
-
```javascript
|
|
384
|
-
// Access current configuration
|
|
385
|
-
const config = api.multihome.getConfig();
|
|
386
|
-
console.log(config);
|
|
387
|
-
// {
|
|
388
|
-
// field: 'tenant_id',
|
|
389
|
-
// excludeResources: ['migrations'],
|
|
390
|
-
// requireAuth: true,
|
|
391
|
-
// allowMissing: false,
|
|
392
|
-
// hasCustomExtractor: true
|
|
393
|
-
// }
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Variables
|
|
397
|
-
|
|
398
|
-
The plugin sets these variables accessible via `api.vars.multihome`:
|
|
399
|
-
|
|
400
|
-
- `field`: The tenant ID field name
|
|
401
|
-
- `excludeResources`: Array of excluded resource names
|
|
402
|
-
- `requireAuth`: Whether tenant context is required
|
|
403
|
-
- `allowMissing`: Whether resources can omit the tenant field
|
|
404
|
-
|
|
405
|
-
### Helpers
|
|
406
|
-
|
|
407
|
-
- `helpers.extractMultihomeId(request)`: The configured extractor function
|
|
408
|
-
|
|
409
|
-
### Hooks
|
|
410
|
-
|
|
411
|
-
The plugin adds these hooks:
|
|
412
|
-
|
|
413
|
-
| Hook | When | Purpose |
|
|
414
|
-
|------|------|---------|
|
|
415
|
-
| `transport:request` | Every request | Extract tenant ID from request |
|
|
416
|
-
| `scope:added` | Resource creation | Validate tenant field exists |
|
|
417
|
-
| `knexQueryFiltering` | Database queries | Add WHERE clause for tenant |
|
|
418
|
-
| `beforeSchemaValidate` | Before validation | Set tenant_id on new records |
|
|
419
|
-
| `beforeDataGet/Put/Patch/Delete` | Before operations | Additional security validation |
|
|
420
|
-
|
|
421
|
-
## Troubleshooting
|
|
422
|
-
|
|
423
|
-
### Common Issues
|
|
424
|
-
|
|
425
|
-
#### 1. "No multihome context available"
|
|
426
|
-
|
|
427
|
-
**Cause**: The extractor couldn't find a tenant ID in the request.
|
|
428
|
-
|
|
429
|
-
**Solutions**:
|
|
430
|
-
- Check your extractor function is returning a value
|
|
431
|
-
- Verify the subdomain/header/token contains tenant information
|
|
432
|
-
- Set `requireAuth: false` if some operations don't need tenant context
|
|
433
|
-
|
|
434
|
-
#### 2. "Resource must have 'tenant_id' field in schema"
|
|
435
|
-
|
|
436
|
-
**Cause**: A resource is missing the tenant field in its schema.
|
|
437
|
-
|
|
438
|
-
**Solutions**:
|
|
439
|
-
- Add the field to the schema
|
|
440
|
-
- Add the resource to `excludeResources` if it's global
|
|
441
|
-
- Set `allowMissing: true` if you have mixed resources
|
|
442
|
-
|
|
443
|
-
#### 3. "Cannot set tenant_id to X - must match current context Y"
|
|
444
|
-
|
|
445
|
-
**Cause**: Trying to set a different tenant_id than the current context.
|
|
446
|
-
|
|
447
|
-
**Solution**: Don't include tenant_id in your requests - it's set automatically.
|
|
448
|
-
|
|
449
|
-
#### 4. Queries returning no results
|
|
450
|
-
|
|
451
|
-
**Cause**: Data exists but with different tenant_id.
|
|
452
|
-
|
|
453
|
-
**Debugging**:
|
|
454
|
-
```javascript
|
|
455
|
-
// Check current tenant context
|
|
456
|
-
api.on('transport:request', (context) => {
|
|
457
|
-
console.log('Current tenant:', context.auth?.multihome_id);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Check query modifications
|
|
461
|
-
api.on('knexQueryFiltering', (context) => {
|
|
462
|
-
console.log('Query SQL:', context.knexQuery.query.toString());
|
|
463
|
-
});
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### Debug Mode
|
|
467
|
-
|
|
468
|
-
Enable detailed logging to troubleshoot:
|
|
469
|
-
|
|
470
|
-
```javascript
|
|
471
|
-
const api = new Api({
|
|
472
|
-
name: 'my-api',
|
|
473
|
-
logging: { level: 'trace' }
|
|
474
|
-
});
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
## Best Practices
|
|
478
|
-
|
|
479
|
-
### 1. Schema Design
|
|
480
|
-
|
|
481
|
-
Always include the tenant field in your schemas:
|
|
482
|
-
|
|
483
|
-
```javascript
|
|
484
|
-
// Good
|
|
485
|
-
api.addResource('orders', {
|
|
486
|
-
schema: {
|
|
487
|
-
order_number: { type: 'string', required: true },
|
|
488
|
-
total: { type: 'number', required: true },
|
|
489
|
-
tenant_id: { type: 'string', required: true } // Always include
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### 2. Consistent Field Naming
|
|
495
|
-
|
|
496
|
-
Use the same tenant field name across all resources:
|
|
497
|
-
|
|
498
|
-
```javascript
|
|
499
|
-
// Configure once
|
|
500
|
-
await api.use(MultiHomePlugin, { field: 'tenant_id' });
|
|
501
|
-
|
|
502
|
-
// Use everywhere
|
|
503
|
-
// ✓ Good: All resources use 'tenant_id'
|
|
504
|
-
// ✗ Bad: Some use 'tenant_id', others use 'company_id'
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
### 3. Validation in Extractors
|
|
508
|
-
|
|
509
|
-
Add validation to your extractor functions:
|
|
510
|
-
|
|
511
|
-
```javascript
|
|
512
|
-
extractor: (request) => {
|
|
513
|
-
const tenantId = request.headers['x-tenant-id'];
|
|
514
|
-
|
|
515
|
-
if (!tenantId) {
|
|
516
|
-
throw new Error('X-Tenant-ID header is required');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!/^[a-z0-9-]+$/.test(tenantId)) {
|
|
520
|
-
throw new Error('Invalid tenant ID format');
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return tenantId;
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### 4. Migration Strategy
|
|
528
|
-
|
|
529
|
-
When adding multi-tenancy to an existing application:
|
|
530
|
-
|
|
531
|
-
1. Add the tenant field to all tables
|
|
532
|
-
2. Populate existing data with a default tenant
|
|
533
|
-
3. Enable the plugin with `allowMissing: true` initially
|
|
534
|
-
4. Gradually update all resources
|
|
535
|
-
5. Switch to `allowMissing: false` when complete
|
|
536
|
-
|
|
537
|
-
### 5. Testing
|
|
538
|
-
|
|
539
|
-
Test with multiple tenants:
|
|
540
|
-
|
|
541
|
-
```javascript
|
|
542
|
-
describe('Multi-tenancy', () => {
|
|
543
|
-
it('isolates data between tenants', async () => {
|
|
544
|
-
// Create data for tenant A
|
|
545
|
-
const resA = await fetch('https://tenant-a.app.com/api/posts', {
|
|
546
|
-
method: 'POST',
|
|
547
|
-
body: JSON.stringify({ title: 'Tenant A Post' })
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// Try to access from tenant B
|
|
551
|
-
const resB = await fetch('https://tenant-b.app.com/api/posts/' + resA.id);
|
|
552
|
-
expect(resB.status).toBe(404); // Should not find
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
### 6. Performance Considerations
|
|
558
|
-
|
|
559
|
-
The tenant field should be indexed for performance:
|
|
560
|
-
|
|
561
|
-
```sql
|
|
562
|
-
CREATE INDEX idx_posts_tenant_id ON posts(tenant_id);
|
|
563
|
-
CREATE INDEX idx_posts_tenant_status ON posts(tenant_id, status);
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
## Complete Example
|
|
567
|
-
|
|
568
|
-
Here's a complete multi-tenant API setup:
|
|
569
|
-
|
|
570
|
-
```javascript
|
|
571
|
-
import { Api } from 'hooked-api';
|
|
572
|
-
import { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
|
|
573
|
-
import { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
|
|
574
|
-
import { ExpressPlugin } from './plugins/core/connectors/express-plugin.js';
|
|
575
|
-
import { JwtAuthPlugin } from './plugins/core/jwt-auth-plugin.js';
|
|
576
|
-
import { MultiHomePlugin } from './plugins/core/multihome-plugin.js';
|
|
577
|
-
import knex from 'knex';
|
|
578
|
-
import express from 'express';
|
|
579
|
-
|
|
580
|
-
// Initialize
|
|
581
|
-
const app = express();
|
|
582
|
-
const db = knex({
|
|
583
|
-
client: 'postgresql',
|
|
584
|
-
connection: process.env.DATABASE_URL
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Create API
|
|
588
|
-
const api = new Api({
|
|
589
|
-
name: 'saas-api',
|
|
590
|
-
logging: { level: 'info' }
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// Install plugins
|
|
594
|
-
await api.use(RestApiPlugin);
|
|
595
|
-
await api.use(RestApiKnexPlugin, { knex: db });
|
|
596
|
-
await api.use(ExpressPlugin, { app });
|
|
597
|
-
await api.use(JwtAuthPlugin, {
|
|
598
|
-
secret: process.env.JWT_SECRET
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// Configure multi-tenancy
|
|
602
|
-
await api.use(MultiHomePlugin, {
|
|
603
|
-
field: 'tenant_id',
|
|
604
|
-
excludeResources: ['system_health', 'public_content'],
|
|
605
|
-
requireAuth: true,
|
|
606
|
-
allowMissing: false,
|
|
607
|
-
extractor: (request) => {
|
|
608
|
-
// Try multiple sources
|
|
609
|
-
// 1. JWT token (preferred)
|
|
610
|
-
if (request.auth?.claims?.tenant_id) {
|
|
611
|
-
return request.auth.claims.tenant_id;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// 2. Subdomain (fallback)
|
|
615
|
-
const host = request.headers.host || '';
|
|
616
|
-
const subdomain = host.split('.')[0];
|
|
617
|
-
if (subdomain && !['www', 'api', 'app'].includes(subdomain)) {
|
|
618
|
-
return subdomain;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// 3. Header (API clients)
|
|
622
|
-
if (request.headers['x-tenant-id']) {
|
|
623
|
-
return request.headers['x-tenant-id'];
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// No tenant found
|
|
627
|
-
throw new Error('Unable to determine tenant context');
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
// Define tenant-specific resources
|
|
632
|
-
api.addResource('projects', {
|
|
633
|
-
schema: {
|
|
634
|
-
name: { type: 'string', required: true },
|
|
635
|
-
description: { type: 'string' },
|
|
636
|
-
status: { type: 'string', defaultTo: 'active' },
|
|
637
|
-
tenant_id: { type: 'string', required: true }
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
api.addResource('team_members', {
|
|
642
|
-
schema: {
|
|
643
|
-
email: { type: 'string', required: true },
|
|
644
|
-
name: { type: 'string', required: true },
|
|
645
|
-
role: { type: 'string', defaultTo: 'member' },
|
|
646
|
-
tenant_id: { type: 'string', required: true }
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// Start server
|
|
651
|
-
app.listen(3000, () => {
|
|
652
|
-
console.log('Multi-tenant API running on port 3000');
|
|
653
|
-
}).on('error', (err) => {
|
|
654
|
-
console.error('Failed to start server:', err);
|
|
655
|
-
process.exit(1)
|
|
656
|
-
});
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
Now your API automatically:
|
|
660
|
-
- Extracts tenant ID from JWT tokens, subdomains, or headers
|
|
661
|
-
- Filters all queries by tenant
|
|
662
|
-
- Sets tenant_id on new records
|
|
663
|
-
- Prevents cross-tenant data access
|
|
664
|
-
- Logs security violations
|
|
665
|
-
|
|
666
|
-
The MultiHome plugin makes multi-tenancy transparent to your application logic while ensuring complete data isolation.
|