diff --git a/content/docs/references/system/Tenant.mdx b/content/docs/references/system/Tenant.mdx new file mode 100644 index 0000000..cbef4b7 --- /dev/null +++ b/content/docs/references/system/Tenant.mdx @@ -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` | optional | Tenant-specific customizations | +| **quotas** | `object` | optional | Resource quotas and limits | diff --git a/content/docs/references/system/TenantIsolationLevel.mdx b/content/docs/references/system/TenantIsolationLevel.mdx new file mode 100644 index 0000000..74af0d0 --- /dev/null +++ b/content/docs/references/system/TenantIsolationLevel.mdx @@ -0,0 +1,10 @@ +--- +title: TenantIsolationLevel +description: TenantIsolationLevel Schema Reference +--- + +## Allowed Values + +* `shared_schema` +* `isolated_schema` +* `isolated_db` \ No newline at end of file diff --git a/content/docs/references/system/TenantQuota.mdx b/content/docs/references/system/TenantQuota.mdx new file mode 100644 index 0000000..f1eb79a --- /dev/null +++ b/content/docs/references/system/TenantQuota.mdx @@ -0,0 +1,12 @@ +--- +title: TenantQuota +description: TenantQuota Schema Reference +--- + +## Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxUsers** | `integer` | optional | Maximum number of users | +| **maxStorage** | `integer` | optional | Maximum storage in bytes | +| **apiRateLimit** | `integer` | optional | API requests per minute | diff --git a/packages/spec/json-schema/Tenant.json b/packages/spec/json-schema/Tenant.json new file mode 100644 index 0000000..62e32c1 --- /dev/null +++ b/packages/spec/json-schema/Tenant.json @@ -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#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TenantIsolationLevel.json b/packages/spec/json-schema/TenantIsolationLevel.json new file mode 100644 index 0000000..efc5c25 --- /dev/null +++ b/packages/spec/json-schema/TenantIsolationLevel.json @@ -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#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/TenantQuota.json b/packages/spec/json-schema/TenantQuota.json new file mode 100644 index 0000000..d554d27 --- /dev/null +++ b/packages/spec/json-schema/TenantQuota.json @@ -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#" +} \ No newline at end of file diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 38922a7..0293511 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -18,6 +18,7 @@ export * from './organization.zod'; export * from './policy.zod'; export * from './role.zod'; export * from './territory.zod'; +export * from './tenant.zod'; export * from './license.zod'; export * from './webhook.zod'; export * from './translation.zod'; diff --git a/packages/spec/src/system/tenant.test.ts b/packages/spec/src/system/tenant.test.ts new file mode 100644 index 0000000..5053e14 --- /dev/null +++ b/packages/spec/src/system/tenant.test.ts @@ -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(); + }); +}); diff --git a/packages/spec/src/system/tenant.zod.ts b/packages/spec/src/system/tenant.zod.ts new file mode 100644 index 0000000..9d2c665 --- /dev/null +++ b/packages/spec/src/system/tenant.zod.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; + +/** + * Tenant Schema (Multi-Tenant Architecture) + * + * Defines the tenant/tenancy model for ObjectStack SaaS deployments. + * Supports different levels of data isolation to meet varying security, + * performance, and compliance requirements. + * + * Isolation Levels: + * - shared_schema: All tenants share the same database and schema (row-level isolation) + * - isolated_schema: Tenants have separate schemas within a shared database + * - isolated_db: Each tenant has a completely separate database + */ + +/** + * Tenant Isolation Level Enum + * Defines how tenant data is separated in the system + */ +export const TenantIsolationLevel = z.enum([ + 'shared_schema', // Shared DB, shared schema, row-level isolation (most economical) + 'isolated_schema', // Shared DB, separate schema per tenant (balanced) + 'isolated_db', // Separate database per tenant (maximum isolation) +]); + +export type TenantIsolationLevel = z.infer; + +/** + * Tenant Quota Schema + * Defines resource limits and usage quotas for a tenant + */ +export const TenantQuotaSchema = z.object({ + /** + * Maximum number of users allowed for this tenant + */ + maxUsers: z.number().int().positive().optional().describe('Maximum number of users'), + + /** + * Maximum storage space in bytes + */ + maxStorage: z.number().int().positive().optional().describe('Maximum storage in bytes'), + + /** + * API rate limit (requests per minute) + */ + apiRateLimit: z.number().int().positive().optional().describe('API requests per minute'), +}); + +export type TenantQuota = z.infer; + +/** + * Tenant Schema + * Represents a tenant in a multi-tenant SaaS deployment + */ +export const TenantSchema = z.object({ + /** + * Unique tenant identifier + */ + id: z.string().describe('Unique tenant identifier'), + + /** + * Tenant name (display name) + */ + name: z.string().describe('Tenant display name'), + + /** + * Data isolation level for this tenant + * Determines how tenant data is segregated from other tenants + */ + isolationLevel: TenantIsolationLevel.describe('Data isolation strategy'), + + /** + * Custom configurations and metadata specific to this tenant + * Can store tenant-specific settings, branding, features, etc. + */ + customizations: z.record(z.any()).optional().describe('Tenant-specific customizations'), + + /** + * Resource quotas and limits for this tenant + */ + quotas: TenantQuotaSchema.optional().describe('Resource quotas and limits'), +}); + +export type Tenant = z.infer;