-
Notifications
You must be signed in to change notification settings - Fork 0
Add multi-tenant protocol with isolation levels and quota management #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| title: Tenant | ||
| description: Tenant Schema Reference | ||
| --- | ||
|
|
||
| ## Properties | ||
|
|
||
| | Property | Type | Required | Description | | ||
| | :--- | :--- | :--- | :--- | | ||
| | **id** | `string` | ✅ | Unique tenant identifier | | ||
| | **name** | `string` | ✅ | Tenant display name | | ||
| | **isolationLevel** | `Enum<'shared_schema' \| 'isolated_schema' \| 'isolated_db'>` | ✅ | Data isolation strategy | | ||
| | **customizations** | `Record<string, any>` | optional | Tenant-specific customizations | | ||
| | **quotas** | `object` | optional | Resource quotas and limits | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| title: TenantIsolationLevel | ||
| description: TenantIsolationLevel Schema Reference | ||
| --- | ||
|
|
||
| ## Allowed Values | ||
|
|
||
| * `shared_schema` | ||
| * `isolated_schema` | ||
| * `isolated_db` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| title: TenantQuota | ||
| description: TenantQuota Schema Reference | ||
| --- | ||
|
|
||
| ## Properties | ||
|
|
||
| | Property | Type | Required | Description | | ||
| | :--- | :--- | :--- | :--- | | ||
|
Comment on lines
+8
to
+9
|
||
| | **maxUsers** | `integer` | optional | Maximum number of users | | ||
| | **maxStorage** | `integer` | optional | Maximum storage in bytes | | ||
| | **apiRateLimit** | `integer` | optional | API requests per minute | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| { | ||
| "$ref": "#/definitions/Tenant", | ||
| "definitions": { | ||
| "Tenant": { | ||
| "type": "object", | ||
| "properties": { | ||
| "id": { | ||
| "type": "string", | ||
| "description": "Unique tenant identifier" | ||
| }, | ||
| "name": { | ||
| "type": "string", | ||
| "description": "Tenant display name" | ||
| }, | ||
| "isolationLevel": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "shared_schema", | ||
| "isolated_schema", | ||
| "isolated_db" | ||
| ], | ||
| "description": "Data isolation strategy" | ||
| }, | ||
| "customizations": { | ||
| "type": "object", | ||
| "additionalProperties": {}, | ||
| "description": "Tenant-specific customizations" | ||
| }, | ||
| "quotas": { | ||
| "type": "object", | ||
| "properties": { | ||
| "maxUsers": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "Maximum number of users" | ||
| }, | ||
| "maxStorage": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "Maximum storage in bytes" | ||
| }, | ||
| "apiRateLimit": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "API requests per minute" | ||
| } | ||
| }, | ||
| "additionalProperties": false, | ||
| "description": "Resource quotas and limits" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "id", | ||
| "name", | ||
| "isolationLevel" | ||
| ], | ||
| "additionalProperties": false | ||
| } | ||
| }, | ||
| "$schema": "http://json-schema.org/draft-07/schema#" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "$ref": "#/definitions/TenantIsolationLevel", | ||
| "definitions": { | ||
| "TenantIsolationLevel": { | ||
| "type": "string", | ||
| "enum": [ | ||
| "shared_schema", | ||
| "isolated_schema", | ||
| "isolated_db" | ||
| ] | ||
| } | ||
| }, | ||
| "$schema": "http://json-schema.org/draft-07/schema#" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| "$ref": "#/definitions/TenantQuota", | ||
| "definitions": { | ||
| "TenantQuota": { | ||
| "type": "object", | ||
| "properties": { | ||
| "maxUsers": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "Maximum number of users" | ||
| }, | ||
| "maxStorage": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "Maximum storage in bytes" | ||
| }, | ||
| "apiRateLimit": { | ||
| "type": "integer", | ||
| "exclusiveMinimum": 0, | ||
| "description": "API requests per minute" | ||
| } | ||
| }, | ||
| "additionalProperties": false | ||
| } | ||
| }, | ||
| "$schema": "http://json-schema.org/draft-07/schema#" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { | ||
| TenantSchema, | ||
| TenantIsolationLevel, | ||
| TenantQuotaSchema, | ||
| type Tenant, | ||
| type TenantQuota, | ||
| } from './tenant.zod'; | ||
|
|
||
| describe('TenantIsolationLevel', () => { | ||
| it('should accept valid isolation levels', () => { | ||
| const levels = ['shared_schema', 'isolated_schema', 'isolated_db']; | ||
|
|
||
| levels.forEach((level) => { | ||
| expect(() => TenantIsolationLevel.parse(level)).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should reject invalid isolation levels', () => { | ||
| expect(() => TenantIsolationLevel.parse('invalid')).toThrow(); | ||
| expect(() => TenantIsolationLevel.parse('sharedSchema')).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('TenantQuotaSchema', () => { | ||
| it('should accept valid quota configuration', () => { | ||
| const validQuota: TenantQuota = { | ||
| maxUsers: 100, | ||
| maxStorage: 10737418240, // 10GB in bytes | ||
| apiRateLimit: 1000, | ||
| }; | ||
|
|
||
| expect(() => TenantQuotaSchema.parse(validQuota)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should accept partial quota configuration', () => { | ||
| const partialQuota = { | ||
| maxUsers: 50, | ||
| }; | ||
|
|
||
| expect(() => TenantQuotaSchema.parse(partialQuota)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should accept empty quota configuration', () => { | ||
| const emptyQuota = {}; | ||
|
|
||
| expect(() => TenantQuotaSchema.parse(emptyQuota)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should reject negative values', () => { | ||
| const invalidQuota = { | ||
| maxUsers: -10, | ||
| }; | ||
|
|
||
| expect(() => TenantQuotaSchema.parse(invalidQuota)).toThrow(); | ||
| }); | ||
|
|
||
| it('should reject non-integer values', () => { | ||
| const invalidQuota = { | ||
| maxUsers: 10.5, | ||
| }; | ||
|
|
||
| expect(() => TenantQuotaSchema.parse(invalidQuota)).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('TenantSchema', () => { | ||
| it('should accept valid tenant configuration', () => { | ||
| const validTenant: Tenant = { | ||
| id: 'tenant_123', | ||
| name: 'Acme Corporation', | ||
| isolationLevel: 'isolated_schema', | ||
| customizations: { | ||
| theme: 'dark', | ||
| logo: 'https://example.com/logo.png', | ||
| }, | ||
| quotas: { | ||
| maxUsers: 100, | ||
| maxStorage: 10737418240, | ||
| apiRateLimit: 1000, | ||
| }, | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(validTenant)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should accept minimal tenant configuration', () => { | ||
| const minimalTenant = { | ||
| id: 'tenant_456', | ||
| name: 'Basic Tenant', | ||
| isolationLevel: 'shared_schema', | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(minimalTenant)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should accept tenant with customizations but no quotas', () => { | ||
| const tenant = { | ||
| id: 'tenant_789', | ||
| name: 'Custom Tenant', | ||
| isolationLevel: 'isolated_db', | ||
| customizations: { | ||
| feature_flags: { | ||
| advanced_analytics: true, | ||
| api_access: true, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(tenant)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should accept tenant with quotas but no customizations', () => { | ||
| const tenant = { | ||
| id: 'tenant_101', | ||
| name: 'Quota Tenant', | ||
| isolationLevel: 'shared_schema', | ||
| quotas: { | ||
| maxUsers: 50, | ||
| apiRateLimit: 500, | ||
| }, | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(tenant)).not.toThrow(); | ||
| }); | ||
|
|
||
| it('should require id field', () => { | ||
| const invalidTenant = { | ||
| name: 'Missing ID Tenant', | ||
| isolationLevel: 'shared_schema', | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(invalidTenant)).toThrow(); | ||
| }); | ||
|
|
||
| it('should require name field', () => { | ||
| const invalidTenant = { | ||
| id: 'tenant_202', | ||
| isolationLevel: 'shared_schema', | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(invalidTenant)).toThrow(); | ||
| }); | ||
|
|
||
| it('should require isolationLevel field', () => { | ||
| const invalidTenant = { | ||
| id: 'tenant_303', | ||
| name: 'Missing Isolation Tenant', | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(invalidTenant)).toThrow(); | ||
| }); | ||
|
|
||
| it('should reject invalid isolationLevel', () => { | ||
| const invalidTenant = { | ||
| id: 'tenant_404', | ||
| name: 'Invalid Isolation Tenant', | ||
| isolationLevel: 'wrong_level', | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(invalidTenant)).toThrow(); | ||
| }); | ||
|
|
||
| it('should allow arbitrary customization values', () => { | ||
| const tenant = { | ||
| id: 'tenant_505', | ||
| name: 'Flexible Customizations', | ||
| isolationLevel: 'shared_schema', | ||
| customizations: { | ||
| string_value: 'text', | ||
| number_value: 42, | ||
| boolean_value: true, | ||
| array_value: [1, 2, 3], | ||
| nested_object: { | ||
| deep: { | ||
| property: 'value', | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| expect(() => TenantSchema.parse(tenant)).not.toThrow(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The properties table starts each row with a double pipe (
|| ...), which introduces an empty column and differs from the standard reference-table format used elsewhere (e.g.,Organization.mdx). Please change these rows to start with a single|so the table renders correctly and consistently.