json-rest-api 1.0.21 → 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 (88) 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 +4 -4
  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/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  81. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  82. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  83. package/plugins/core/rest-api-access.js +0 -459
  84. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  85. package/tests/multihome.test.js +0 -553
  86. package/tests/multihome.test.js.bak +0 -550
  87. package/tests/ownership-optout.test.js +0 -76
  88. 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.