Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions content/docs/references/system/Tenant.mdx
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 |
| :--- | :--- | :--- | :--- |
Comment on lines +8 to +9
Copy link

Copilot AI Jan 23, 2026

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.

Copilot uses AI. Check for mistakes.
| **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 |
10 changes: 10 additions & 0 deletions content/docs/references/system/TenantIsolationLevel.mdx
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`
12 changes: 12 additions & 0 deletions content/docs/references/system/TenantQuota.mdx
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
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table header has an extra leading pipe (|| Property / || :---), which will render an empty first column and is inconsistent with other reference docs (e.g., Organization.mdx). Drop the extra pipe so the header rows start with a single | to match the established table format.

Copilot uses AI. Check for mistakes.
| **maxUsers** | `integer` | optional | Maximum number of users |
| **maxStorage** | `integer` | optional | Maximum storage in bytes |
| **apiRateLimit** | `integer` | optional | API requests per minute |
61 changes: 61 additions & 0 deletions packages/spec/json-schema/Tenant.json
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#"
}
14 changes: 14 additions & 0 deletions packages/spec/json-schema/TenantIsolationLevel.json
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#"
}
27 changes: 27 additions & 0 deletions packages/spec/json-schema/TenantQuota.json
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#"
}
1 change: 1 addition & 0 deletions packages/spec/src/system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
184 changes: 184 additions & 0 deletions packages/spec/src/system/tenant.test.ts
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();
});
});
Loading
Loading