Skip to content

Commit 1320d12

Browse files
authored
Merge pull request #82 from objectstack-ai/copilot/add-tenant-schema-file
2 parents 7184fbb + 972bcde commit 1320d12

File tree

9 files changed

+407
-0
lines changed

9 files changed

+407
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Tenant
3+
description: Tenant Schema Reference
4+
---
5+
6+
## Properties
7+
8+
| Property | Type | Required | Description |
9+
| :--- | :--- | :--- | :--- |
10+
| **id** | `string` || Unique tenant identifier |
11+
| **name** | `string` || Tenant display name |
12+
| **isolationLevel** | `Enum<'shared_schema' \| 'isolated_schema' \| 'isolated_db'>` || Data isolation strategy |
13+
| **customizations** | `Record<string, any>` | optional | Tenant-specific customizations |
14+
| **quotas** | `object` | optional | Resource quotas and limits |
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: TenantIsolationLevel
3+
description: TenantIsolationLevel Schema Reference
4+
---
5+
6+
## Allowed Values
7+
8+
* `shared_schema`
9+
* `isolated_schema`
10+
* `isolated_db`
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: TenantQuota
3+
description: TenantQuota Schema Reference
4+
---
5+
6+
## Properties
7+
8+
| Property | Type | Required | Description |
9+
| :--- | :--- | :--- | :--- |
10+
| **maxUsers** | `integer` | optional | Maximum number of users |
11+
| **maxStorage** | `integer` | optional | Maximum storage in bytes |
12+
| **apiRateLimit** | `integer` | optional | API requests per minute |
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"$ref": "#/definitions/Tenant",
3+
"definitions": {
4+
"Tenant": {
5+
"type": "object",
6+
"properties": {
7+
"id": {
8+
"type": "string",
9+
"description": "Unique tenant identifier"
10+
},
11+
"name": {
12+
"type": "string",
13+
"description": "Tenant display name"
14+
},
15+
"isolationLevel": {
16+
"type": "string",
17+
"enum": [
18+
"shared_schema",
19+
"isolated_schema",
20+
"isolated_db"
21+
],
22+
"description": "Data isolation strategy"
23+
},
24+
"customizations": {
25+
"type": "object",
26+
"additionalProperties": {},
27+
"description": "Tenant-specific customizations"
28+
},
29+
"quotas": {
30+
"type": "object",
31+
"properties": {
32+
"maxUsers": {
33+
"type": "integer",
34+
"exclusiveMinimum": 0,
35+
"description": "Maximum number of users"
36+
},
37+
"maxStorage": {
38+
"type": "integer",
39+
"exclusiveMinimum": 0,
40+
"description": "Maximum storage in bytes"
41+
},
42+
"apiRateLimit": {
43+
"type": "integer",
44+
"exclusiveMinimum": 0,
45+
"description": "API requests per minute"
46+
}
47+
},
48+
"additionalProperties": false,
49+
"description": "Resource quotas and limits"
50+
}
51+
},
52+
"required": [
53+
"id",
54+
"name",
55+
"isolationLevel"
56+
],
57+
"additionalProperties": false
58+
}
59+
},
60+
"$schema": "http://json-schema.org/draft-07/schema#"
61+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$ref": "#/definitions/TenantIsolationLevel",
3+
"definitions": {
4+
"TenantIsolationLevel": {
5+
"type": "string",
6+
"enum": [
7+
"shared_schema",
8+
"isolated_schema",
9+
"isolated_db"
10+
]
11+
}
12+
},
13+
"$schema": "http://json-schema.org/draft-07/schema#"
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"$ref": "#/definitions/TenantQuota",
3+
"definitions": {
4+
"TenantQuota": {
5+
"type": "object",
6+
"properties": {
7+
"maxUsers": {
8+
"type": "integer",
9+
"exclusiveMinimum": 0,
10+
"description": "Maximum number of users"
11+
},
12+
"maxStorage": {
13+
"type": "integer",
14+
"exclusiveMinimum": 0,
15+
"description": "Maximum storage in bytes"
16+
},
17+
"apiRateLimit": {
18+
"type": "integer",
19+
"exclusiveMinimum": 0,
20+
"description": "API requests per minute"
21+
}
22+
},
23+
"additionalProperties": false
24+
}
25+
},
26+
"$schema": "http://json-schema.org/draft-07/schema#"
27+
}

packages/spec/src/system/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './organization.zod';
1818
export * from './policy.zod';
1919
export * from './role.zod';
2020
export * from './territory.zod';
21+
export * from './tenant.zod';
2122
export * from './license.zod';
2223
export * from './webhook.zod';
2324
export * from './translation.zod';
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
TenantSchema,
4+
TenantIsolationLevel,
5+
TenantQuotaSchema,
6+
type Tenant,
7+
type TenantQuota,
8+
} from './tenant.zod';
9+
10+
describe('TenantIsolationLevel', () => {
11+
it('should accept valid isolation levels', () => {
12+
const levels = ['shared_schema', 'isolated_schema', 'isolated_db'];
13+
14+
levels.forEach((level) => {
15+
expect(() => TenantIsolationLevel.parse(level)).not.toThrow();
16+
});
17+
});
18+
19+
it('should reject invalid isolation levels', () => {
20+
expect(() => TenantIsolationLevel.parse('invalid')).toThrow();
21+
expect(() => TenantIsolationLevel.parse('sharedSchema')).toThrow();
22+
});
23+
});
24+
25+
describe('TenantQuotaSchema', () => {
26+
it('should accept valid quota configuration', () => {
27+
const validQuota: TenantQuota = {
28+
maxUsers: 100,
29+
maxStorage: 10737418240, // 10GB in bytes
30+
apiRateLimit: 1000,
31+
};
32+
33+
expect(() => TenantQuotaSchema.parse(validQuota)).not.toThrow();
34+
});
35+
36+
it('should accept partial quota configuration', () => {
37+
const partialQuota = {
38+
maxUsers: 50,
39+
};
40+
41+
expect(() => TenantQuotaSchema.parse(partialQuota)).not.toThrow();
42+
});
43+
44+
it('should accept empty quota configuration', () => {
45+
const emptyQuota = {};
46+
47+
expect(() => TenantQuotaSchema.parse(emptyQuota)).not.toThrow();
48+
});
49+
50+
it('should reject negative values', () => {
51+
const invalidQuota = {
52+
maxUsers: -10,
53+
};
54+
55+
expect(() => TenantQuotaSchema.parse(invalidQuota)).toThrow();
56+
});
57+
58+
it('should reject non-integer values', () => {
59+
const invalidQuota = {
60+
maxUsers: 10.5,
61+
};
62+
63+
expect(() => TenantQuotaSchema.parse(invalidQuota)).toThrow();
64+
});
65+
});
66+
67+
describe('TenantSchema', () => {
68+
it('should accept valid tenant configuration', () => {
69+
const validTenant: Tenant = {
70+
id: 'tenant_123',
71+
name: 'Acme Corporation',
72+
isolationLevel: 'isolated_schema',
73+
customizations: {
74+
theme: 'dark',
75+
logo: 'https://example.com/logo.png',
76+
},
77+
quotas: {
78+
maxUsers: 100,
79+
maxStorage: 10737418240,
80+
apiRateLimit: 1000,
81+
},
82+
};
83+
84+
expect(() => TenantSchema.parse(validTenant)).not.toThrow();
85+
});
86+
87+
it('should accept minimal tenant configuration', () => {
88+
const minimalTenant = {
89+
id: 'tenant_456',
90+
name: 'Basic Tenant',
91+
isolationLevel: 'shared_schema',
92+
};
93+
94+
expect(() => TenantSchema.parse(minimalTenant)).not.toThrow();
95+
});
96+
97+
it('should accept tenant with customizations but no quotas', () => {
98+
const tenant = {
99+
id: 'tenant_789',
100+
name: 'Custom Tenant',
101+
isolationLevel: 'isolated_db',
102+
customizations: {
103+
feature_flags: {
104+
advanced_analytics: true,
105+
api_access: true,
106+
},
107+
},
108+
};
109+
110+
expect(() => TenantSchema.parse(tenant)).not.toThrow();
111+
});
112+
113+
it('should accept tenant with quotas but no customizations', () => {
114+
const tenant = {
115+
id: 'tenant_101',
116+
name: 'Quota Tenant',
117+
isolationLevel: 'shared_schema',
118+
quotas: {
119+
maxUsers: 50,
120+
apiRateLimit: 500,
121+
},
122+
};
123+
124+
expect(() => TenantSchema.parse(tenant)).not.toThrow();
125+
});
126+
127+
it('should require id field', () => {
128+
const invalidTenant = {
129+
name: 'Missing ID Tenant',
130+
isolationLevel: 'shared_schema',
131+
};
132+
133+
expect(() => TenantSchema.parse(invalidTenant)).toThrow();
134+
});
135+
136+
it('should require name field', () => {
137+
const invalidTenant = {
138+
id: 'tenant_202',
139+
isolationLevel: 'shared_schema',
140+
};
141+
142+
expect(() => TenantSchema.parse(invalidTenant)).toThrow();
143+
});
144+
145+
it('should require isolationLevel field', () => {
146+
const invalidTenant = {
147+
id: 'tenant_303',
148+
name: 'Missing Isolation Tenant',
149+
};
150+
151+
expect(() => TenantSchema.parse(invalidTenant)).toThrow();
152+
});
153+
154+
it('should reject invalid isolationLevel', () => {
155+
const invalidTenant = {
156+
id: 'tenant_404',
157+
name: 'Invalid Isolation Tenant',
158+
isolationLevel: 'wrong_level',
159+
};
160+
161+
expect(() => TenantSchema.parse(invalidTenant)).toThrow();
162+
});
163+
164+
it('should allow arbitrary customization values', () => {
165+
const tenant = {
166+
id: 'tenant_505',
167+
name: 'Flexible Customizations',
168+
isolationLevel: 'shared_schema',
169+
customizations: {
170+
string_value: 'text',
171+
number_value: 42,
172+
boolean_value: true,
173+
array_value: [1, 2, 3],
174+
nested_object: {
175+
deep: {
176+
property: 'value',
177+
},
178+
},
179+
},
180+
};
181+
182+
expect(() => TenantSchema.parse(tenant)).not.toThrow();
183+
});
184+
});

0 commit comments

Comments
 (0)