diff --git a/.changeset/fix-org-id-optional-single-tenant.md b/.changeset/fix-org-id-optional-single-tenant.md new file mode 100644 index 0000000000..661e172565 --- /dev/null +++ b/.changeset/fix-org-id-optional-single-tenant.md @@ -0,0 +1,13 @@ +--- +"@objectstack/platform-objects": patch +--- + +Fix: `sys_business_unit` / `sys_team` could not be created in single-tenant deployments. + +`organization_id` was `required`, but single-tenant has no `sys_organization` row and +nothing auto-stamps one (OrgScopingPlugin is multi-tenant-only), so every create failed +with `VALIDATION_FAILED: organization_id (required)`. Make `organization_id` optional on +both objects: single-tenant leaves it null; multi-tenant still auto-stamps it via +OrgScopingPlugin and tenant-isolation RLS hides any null-org row (fail-closed), so there is +no cross-tenant exposure. (sys_member / sys_invitation carry the same `required` flag but are +created only through better-auth org flows, which always supply an org — left unchanged.) diff --git a/packages/dogfood/test/single-tenant-identity-create.dogfood.test.ts b/packages/dogfood/test/single-tenant-identity-create.dogfood.test.ts new file mode 100644 index 0000000000..f62cb35eba --- /dev/null +++ b/packages/dogfood/test/single-tenant-identity-create.dogfood.test.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0057 — org-scoped identity objects must be creatable in SINGLE-TENANT. + * + * Single-tenant deployments have no `sys_organization` row and no auto-stamp + * (OrgScopingPlugin is multi-tenant-only), so a `required` `organization_id` + * made sys_business_unit / sys_team uncreatable (VALIDATION_FAILED). The field + * is now optional; this proves the create path works with no org. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import showcaseStack from '@objectstack/example-showcase'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; + +describe('ADR-0057: org-scoped identity creatable single-tenant', () => { + let stack: VerifyStack; + let token: string; + + beforeAll(async () => { + stack = await bootStack(showcaseStack, {}); // single-tenant: no org-scoping, no org row + token = await stack.signIn(); + }, 120_000); + + afterAll(async () => { await stack?.stop?.(); }); + + it('creates a sys_business_unit with no organization_id', async () => { + const res = await stack.apiAs(token, 'POST', '/data/sys_business_unit', { name: 'Engineering', kind: 'department' }); + expect(res.status).toBe(201); + const body: any = await res.json(); + expect(body.record?.organization_id ?? null).toBeNull(); + }); + + it('creates a sys_team with no organization_id', async () => { + const res = await stack.apiAs(token, 'POST', '/data/sys_team', { name: 'Tiger Team' }); + expect(res.status).toBe(201); + }); +}); diff --git a/packages/platform-objects/src/identity/sys-business-unit.object.ts b/packages/platform-objects/src/identity/sys-business-unit.object.ts index 6a9bc26244..68b5be7ede 100644 --- a/packages/platform-objects/src/identity/sys-business-unit.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit.object.ts @@ -114,8 +114,13 @@ export const SysBusinessUnit = ObjectSchema.create({ organization_id: Field.lookup('sys_organization', { label: 'Organization', - required: true, - description: 'Tenant scope.', + // Optional: single-tenant deployments have no organization row (org-scoping + // is multi-tenant-only, nothing auto-stamps one) — requiring it would make + // the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin + // auto-stamps this from the active tenant and tenant-isolation RLS hides any + // null-org row (fail-closed). ADR-0057 addendum. + required: false, + description: 'Tenant scope. Null in single-tenant; auto-stamped in multi-tenant.', group: 'Hierarchy', }), diff --git a/packages/platform-objects/src/identity/sys-team.object.ts b/packages/platform-objects/src/identity/sys-team.object.ts index 0e9a873359..25dfb8c50e 100644 --- a/packages/platform-objects/src/identity/sys-team.object.ts +++ b/packages/platform-objects/src/identity/sys-team.object.ts @@ -122,8 +122,13 @@ export const SysTeam = ObjectSchema.create({ organization_id: Field.lookup('sys_organization', { label: 'Organization', - required: true, - description: 'Parent organization for this team', + // Optional: single-tenant deployments have no organization row (org-scoping + // is multi-tenant-only, nothing auto-stamps one) — requiring it would make + // the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin + // auto-stamps this from the active tenant and tenant-isolation RLS hides any + // null-org row (fail-closed). ADR-0057 addendum. + required: false, + description: 'Parent organization for this team. Null in single-tenant; auto-stamped in multi-tenant.', group: 'Identity', }),