Skip to content

Commit 0df063e

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(platform-objects): make sys_business_unit/sys_team organization_id optional (single-tenant create) (#2178)
Single-tenant has no sys_organization row and no auto-stamp (org-scoping is multi-tenant-only), so a required organization_id made these objects uncreatable (VALIDATION_FAILED). Optional now: single-tenant = null; multi-tenant still auto-stamps via OrgScopingPlugin and tenant-isolation RLS hides null-org rows (fail-closed). +dogfood regression test. Verified live on os dev (BU + Team create 201 single-tenant). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6812d92 commit 0df063e

4 files changed

Lines changed: 65 additions & 4 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
---
4+
5+
Fix: `sys_business_unit` / `sys_team` could not be created in single-tenant deployments.
6+
7+
`organization_id` was `required`, but single-tenant has no `sys_organization` row and
8+
nothing auto-stamps one (OrgScopingPlugin is multi-tenant-only), so every create failed
9+
with `VALIDATION_FAILED: organization_id (required)`. Make `organization_id` optional on
10+
both objects: single-tenant leaves it null; multi-tenant still auto-stamps it via
11+
OrgScopingPlugin and tenant-isolation RLS hides any null-org row (fail-closed), so there is
12+
no cross-tenant exposure. (sys_member / sys_invitation carry the same `required` flag but are
13+
created only through better-auth org flows, which always supply an org — left unchanged.)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* ADR-0057 — org-scoped identity objects must be creatable in SINGLE-TENANT.
5+
*
6+
* Single-tenant deployments have no `sys_organization` row and no auto-stamp
7+
* (OrgScopingPlugin is multi-tenant-only), so a `required` `organization_id`
8+
* made sys_business_unit / sys_team uncreatable (VALIDATION_FAILED). The field
9+
* is now optional; this proves the create path works with no org.
10+
*/
11+
12+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
13+
import showcaseStack from '@objectstack/example-showcase';
14+
import { bootStack, type VerifyStack } from '@objectstack/verify';
15+
16+
describe('ADR-0057: org-scoped identity creatable single-tenant', () => {
17+
let stack: VerifyStack;
18+
let token: string;
19+
20+
beforeAll(async () => {
21+
stack = await bootStack(showcaseStack, {}); // single-tenant: no org-scoping, no org row
22+
token = await stack.signIn();
23+
}, 120_000);
24+
25+
afterAll(async () => { await stack?.stop?.(); });
26+
27+
it('creates a sys_business_unit with no organization_id', async () => {
28+
const res = await stack.apiAs(token, 'POST', '/data/sys_business_unit', { name: 'Engineering', kind: 'department' });
29+
expect(res.status).toBe(201);
30+
const body: any = await res.json();
31+
expect(body.record?.organization_id ?? null).toBeNull();
32+
});
33+
34+
it('creates a sys_team with no organization_id', async () => {
35+
const res = await stack.apiAs(token, 'POST', '/data/sys_team', { name: 'Tiger Team' });
36+
expect(res.status).toBe(201);
37+
});
38+
});

packages/platform-objects/src/identity/sys-business-unit.object.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ export const SysBusinessUnit = ObjectSchema.create({
114114

115115
organization_id: Field.lookup('sys_organization', {
116116
label: 'Organization',
117-
required: true,
118-
description: 'Tenant scope.',
117+
// Optional: single-tenant deployments have no organization row (org-scoping
118+
// is multi-tenant-only, nothing auto-stamps one) — requiring it would make
119+
// the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin
120+
// auto-stamps this from the active tenant and tenant-isolation RLS hides any
121+
// null-org row (fail-closed). ADR-0057 addendum.
122+
required: false,
123+
description: 'Tenant scope. Null in single-tenant; auto-stamped in multi-tenant.',
119124
group: 'Hierarchy',
120125
}),
121126

packages/platform-objects/src/identity/sys-team.object.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,13 @@ export const SysTeam = ObjectSchema.create({
122122

123123
organization_id: Field.lookup('sys_organization', {
124124
label: 'Organization',
125-
required: true,
126-
description: 'Parent organization for this team',
125+
// Optional: single-tenant deployments have no organization row (org-scoping
126+
// is multi-tenant-only, nothing auto-stamps one) — requiring it would make
127+
// the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin
128+
// auto-stamps this from the active tenant and tenant-isolation RLS hides any
129+
// null-org row (fail-closed). ADR-0057 addendum.
130+
required: false,
131+
description: 'Parent organization for this team. Null in single-tenant; auto-stamped in multi-tenant.',
127132
group: 'Identity',
128133
}),
129134

0 commit comments

Comments
 (0)