Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
420fb8c
wip(plugin-webhooks): declared webhooks read from the registry; seede…
claude Oct 19, 2026
e44c642
test(plugin-webhooks): pin the registry read, the ledger switch and t…
claude Oct 19, 2026
8189a84
wip: liveness ledger re-anchored to the registry read; dogfood proof …
claude Oct 19, 2026
e412cb8
docs+ledger: the registry read named across spec comments, liveness, …
claude Oct 19, 2026
bdad70b
spec(liveness): state counts regenerated; isActive named at the dispa…
claude Oct 19, 2026
723af97
fix(plugin-webhooks): a declared webhook is deployment-wide when the …
claude Oct 19, 2026
e0a6dc9
fix(gates): tenant-audit census regenerated for the seeder's two reti…
claude Oct 19, 2026
6b3042a
test(plugin-webhooks): the fake engine refuses a WHERE combinator it …
claude Oct 19, 2026
820121c
fix(lint,showcase): the dead label/description verdict is carried to …
claude Oct 19, 2026
b378ff5
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
c3b1452
test(dogfood): the R2 harness pins one delivery per task change from …
claude Oct 19, 2026
9bce0aa
test(dogfood): the harness case switches the declaration on, so it ca…
claude Oct 19, 2026
7e580dc
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
fa6a837
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
b4ae8e6
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
206d2bc
Merge remote-tracking branch 'origin/claude/issue-24529-8b0g-webhooks…
claude Oct 19, 2026
f89c0f2
wip: birth marker, shapeGeneration, boot guard rule
claude Oct 19, 2026
6e14afa
wip: ceremony steps 5a-5d, inventory, flips, differ ownership
claude Oct 19, 2026
50a2d34
wip: open the ceremony command, ports for webhooks and NOT NULL
claude Oct 19, 2026
9ea4312
wip: existing ceremony tests follow the new step set; NOT NULL routed…
claude Oct 19, 2026
719ab13
wip: the guard's pins follow the birth marker and shapeGeneration
claude Oct 19, 2026
036a2a1
wip: the birth half in every CLI boot, so a CLI-created fresh store b…
claude Oct 19, 2026
0128f07
test(cli): pin the generation-1 ceremony steps over an 18.0-shaped da…
claude Oct 19, 2026
8e6a33a
test: the tenant column's NOT NULL is the ceremony's, not drift; the …
claude Oct 19, 2026
32d1e22
test(dogfood): R2 and R3 restated — the 18.0 and v17 databases take t…
claude Oct 19, 2026
13446cb
fix(cli): a rotation shard's NOT NULL is reported unsupported, never …
claude Oct 19, 2026
06e0556
docs,qa: the open ceremony and the birth marker in the CLI docs and t…
claude Oct 19, 2026
e0c1d2b
chore(spec): regenerate the api-surface, export-origin, declaration-m…
claude Oct 19, 2026
a0fa816
test(platform-objects,service-settings): sys_secret is deployment-lev…
claude Oct 19, 2026
e7a788d
chore: regenerate the platform-object tenancy census (sys_secret and …
claude Oct 19, 2026
f1264d7
chore: the gates the sweep named — the guard double's findOne predica…
claude Oct 19, 2026
a3064ef
test(cli): fixtures standing in for an earlier release's database car…
claude Oct 19, 2026
d6195b9
Merge origin/main into claude/issue-24581-ceremony-open
claude Oct 19, 2026
0232108
docs(qa): the ceremony item's revision 3 cites its PR
claude Oct 19, 2026
dceaa56
fix(driver-turso): the remote face counts what its schema doors creat…
claude Oct 19, 2026
33b7943
test(runtime): the remote-face guard pin reads driver-turso's source
claude Oct 19, 2026
d28f4cf
chore(changeset): the remote face's schema-sync count
claude Oct 19, 2026
edb2db1
test(dogfood): the upgrade ceremony over the 18.0 fixture is a shared…
claude Oct 19, 2026
1699ec5
Merge origin/main after #24667 (step 4) into the ceremony branch
claude Oct 20, 2026
b5010d9
test(dogfood): ceremoniedFixture boots the 18.0 bytes once, refused, …
claude Oct 20, 2026
0f83a8d
fix(text): no shipped text says the ownership ceremony is closed; an …
claude Oct 20, 2026
c6f775c
fix(text): the delta review's text items — fresh installs restated, t…
claude Oct 20, 2026
bec8161
Merge remote-tracking branch 'origin/claude/issue-24581-ceremony-open…
claude Oct 20, 2026
9e368e5
wip(driver-sql,driver-turso,spec): tenant column NOT NULL at CREATE T…
claude Oct 20, 2026
e7be5cd
wip: tenant NOT NULL pins (driver-sql, driver-turso remote)
claude Oct 20, 2026
a801a8e
wip(driver-sql): fixtures that write organization-less rows model a p…
claude Oct 20, 2026
a894313
wip(driver-turso): fixtures that write organization-less rows model a…
claude Oct 20, 2026
30077cb
wip: changeset for the tenant column NOT NULL at CREATE TABLE
claude Oct 20, 2026
08ebe7e
wip(plugin-sharing): fixtures name their organization, or model a pre…
claude Oct 20, 2026
8aabcfa
wip(service-messaging): lean fixture engines hold a Default Organization
claude Oct 20, 2026
0dc0359
wip(plugin-audit): lean fixture engines hold a Default Organization
claude Oct 20, 2026
592372e
wip(service-storage): fixtures hold a Default Organization, or model …
claude Oct 20, 2026
b940bd5
wip(plugin-security): lean fixture engines hold a Default Organization
claude Oct 20, 2026
b0621ec
wip(plugin-security): fixtures model a pre-18 store where they hold o…
claude Oct 20, 2026
3832d75
wip(rest): lean fixture engines hold a Default Organization; testkit …
claude Oct 20, 2026
f1111e1
wip(service-automation): fixtures hold a Default Organization, or mod…
claude Oct 20, 2026
7d7ad20
wip(trigger-record-change): lean fixture engines hold a Default Organ…
claude Oct 20, 2026
0c5b9ee
wip(examples): lean fixture engines hold a Default Organization
claude Oct 20, 2026
a0dbaa9
wip(runtime): lean fixture engines hold a Default Organization
claude Oct 20, 2026
da20f30
wip(runtime): residual fixtures name their organization or model a pr…
claude Oct 20, 2026
6a38035
wip(plugin-approvals): fixtures hold a Default Organization, or model…
claude Oct 20, 2026
0e5e11c
wip(client,http-conformance,service-analytics): lean fixture engines …
claude Oct 20, 2026
e36e855
wip(dogfood): fixtures name their organization, hold a Default Organi…
claude Oct 20, 2026
3bb0ddb
wip(plugin-auth): fixtures model a pre-18 store where they hold organ…
claude Oct 20, 2026
1920c54
wip(cli): served test boots hold a Default Organization ahead of the …
claude Oct 20, 2026
30ec181
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7b…
claude Oct 20, 2026
f99c220
chore(spec): regenerate the reference docs the main merge deferred
claude Oct 20, 2026
582989e
test(plugin-security): main's delegated-write fixture models a pre-18…
claude Oct 20, 2026
382851c
test(cli,plugin-approvals): ceremony and migrate fixtures plant earli…
claude Oct 20, 2026
db78d39
test(cli): the pre-18 store testkit imports the driver lazily, as src…
claude Oct 20, 2026
b5442cd
test(trigger-record-change): the Default Organization testkit takes t…
claude Oct 20, 2026
f56c207
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7b…
claude Oct 20, 2026
5a321e3
test(driver-sql): main's organization-less fixtures (7a-1 read-back, …
claude Oct 20, 2026
97f5296
test(dogfood): the legacy criteria-less rule belongs to the install's…
claude Oct 20, 2026
5bd3515
test(plugin-approvals): main's two new lean fixture engines hold a De…
claude Oct 20, 2026
e1f6bbc
Merge origin/main (7a-2, #24911 fixture-18.0.0, #24906, #24913, #2492…
claude Oct 20, 2026
0b58ee3
Merge origin/main (#25016 organization-delete contract, #24934, #2502…
claude Oct 21, 2026
d663325
Merge origin/main (f46ef4d70: #25048, #25066) into 7b-0
claude Oct 21, 2026
3e1df7f
test: main's new fixtures (#25048, #25066 and the ReadableIds, person…
claude Oct 21, 2026
4f41fbf
test(cli): the os db orphans restrict-dependent row belongs to the se…
claude Oct 21, 2026
cceb73d
Merge origin/main (ae58b26f9) into 7b-0
claude Oct 21, 2026
31189e7
test(plugin-security): main's delegated-membership, update-seam and e…
claude Oct 21, 2026
7fe7c4e
Merge origin/main (edcc8ceae, carrying #25114) into 7b-0
claude Oct 21, 2026
a674501
chore(driver-sql,driver-turso): Metadata-protocol none, a patch with …
claude Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
21 changes: 21 additions & 0 deletions .changeset/24583-tenant-column-not-null-ddl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/driver-sql': patch
'@objectstack/driver-turso': patch
'@objectstack/spec': patch
---

feat(driver-sql,driver-turso)!: a table the driver creates carries its tenant column NOT NULL, so a write that names no organization is refused at the database

Clause-②: yes (narrowing)
Metadata-protocol: none

<!-- adr-0087: not-required (no-migration-prescription) A storage constraint at CREATE TABLE: no key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. No export, type or error code is removed or renamed; `TENANT_SCOPE_FIELD_DEF` keeps its shape and only its description text is corrected. The remedy is a run-time one, stated below. -->

**BREAKING** accept-set narrowing at the database (ADR-0131 D1: "There is no nullable tenant column anywhere"). It changes what a data writer can store, not what a metadata author can write, so it ships as `patch` with this banner.

- **Before.** The SQL driver created a table's tenant column (`organization_id`, or a declared `tenancy.tenantField`) nullable, as the injected field's declaration says (`required: false`, no `storage`). A write that reached the driver without an organization landed a row no organization owns.
- **Now.** Every table the driver CREATES takes its tenant column `NOT NULL`, with no metadata change: the plain managed table (`initObjects`), every newly rotated shard (asked of the base table's declaration), and a table the Turso remote face creates (`syncSchema`, `syncSchemasBatch`, `initObjects`). A write with no organization on such a table is refused by the database (`NOT NULL constraint failed: <table>.organization_id`, `violates not-null constraint`, or `cannot be null`). That includes an `upsert` whose payload omits the organization: the database checks the proposed row before it resolves the conflict.
- **Unchanged.** A tenant column ADDED to a table that already exists stays nullable, and so does an existing shard or an existing remote table: a table that already holds rows is the organization-ownership ceremony's to tighten (`os migrate organization-ownership`), which reports a shard and the Turso remote face as unable to. The schema differ reports no drift over the tenant column's nullability either way. On a database that has not taken the ceremony, a table this release creates (a newly rotated shard, or a cold table a newly configured lifecycle tier creates) is NOT NULL beside nullable siblings: an organization-less row the lifecycle mover would carry into it is refused there, and stays where it is until the ceremony attributes it. An object with no tenant column (`tenancy: { enabled: false }`, `systemFields: { tenant: false }`) is untouched, even when it carries a field named `organization_id`.
- **Who sees it.** A composed `os serve` / `os dev` / `os start` with auth stamps every write with its organization, so nothing changes there. A production `os serve` whose `AuthPlugin` did not compose (no `OS_AUTH_SECRET`) and a lean embedding (`@objectstack/objectql/core` or `createStandaloneStack` over a SQL driver, with no organization object) used to write those rows with a NULL organization; now each such write fails, loudly.

**The remedy.** Carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId })`, or `{ context: { tenantId: orgId } }`. Compose authentication (set `OS_AUTH_SECRET`) so the platform derives it. If the object's rows legitimately belong to no organization, declare it so and it has no tenant column: `systemFields: { tenant: false }` or `tenancy: { enabled: false }`.
45 changes: 45 additions & 0 deletions examples/app-crm/test/default-organization.testkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Give a lean test engine what every real `single` install has: an
* organization object holding its Default Organization.
*
* ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
* NULL. The engine stamps an organization-less system write with the derived
* organization under `single` (D9) — but only where the composition registers
* the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
* the write reaches the database with no organization and is refused there.
* A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
* shapes) calls this once its own objects are registered, so its system
* writes land the way they do on a real install.
*
* The object mirrors the platform's: managed by better-auth, so it carries no
* tenant column of its own.
*/
export const DEFAULT_ORGANIZATION_ID = 'org_default';

export async function installDefaultOrganization(engine: {
registry: { registerObject(def: unknown, owner: string): unknown };
getSchema(object: string): unknown;
syncSchemas(): Promise<unknown>;
getDriverForObject(object: string):
| {
create(object: string, data: Record<string, unknown>): Promise<unknown>;
findOne(object: string, query: unknown): Promise<unknown>;
}
| undefined;
}): Promise<void> {
// Idempotent: a suite that syncs one engine more than once may call it each time.
if (!engine.getSchema('sys_organization')) {
engine.registry.registerObject(
{ name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
'@objectstack/example-crm',
);
}
await engine.syncSchemas();
// Seeded through the driver: the organization is data at rest, not a write
// the suite under test should observe (an audit or activity mirror would).
const driver = engine.getDriverForObject('sys_organization')!;
if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
}
4 changes: 4 additions & 0 deletions examples/app-crm/test/opportunity-stage-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { Account, Opportunity, OpportunityLineItem } from '../src/objects/index.js';
import { allHooks } from '../src/hooks/index.js';
import { OpportunityStageHook } from '../src/hooks/opportunity.hook.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

/** The app id `AppPlugin` derives its `packageId` from. */
const PACKAGE_ID = 'app:com.example.crm';
Expand Down Expand Up @@ -96,6 +97,9 @@ async function bootCrm(hooks: unknown[] = allHooks): Promise<ObjectQL> {
engine.registry.registerObject(def as never, PACKAGE_ID, 'crm');
}
await engine.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(engine);

// Exactly the AppPlugin call, minus the sandbox body runner this app has no
// use for (its single hook is an inline code handler).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
TaskDoneNotifyOwnerFlow,
NotifyOwnerSubflow,
} from '../src/automation/flows/index.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

/** Everything the approvals service treats as an unconditional actor. */
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as any;
Expand Down Expand Up @@ -128,6 +129,9 @@ async function bootShowcaseApprovals(
objectql.registry.registerObject(obj, 'showcase', 'showcase');
}
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);

for (const flow of flows) automation.registerFlow(flow.name, flow);

Expand Down
45 changes: 45 additions & 0 deletions examples/app-showcase/test/default-organization.testkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Give a lean test engine what every real `single` install has: an
* organization object holding its Default Organization.
*
* ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
* NULL. The engine stamps an organization-less system write with the derived
* organization under `single` (D9) — but only where the composition registers
* the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
* the write reaches the database with no organization and is refused there.
* A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
* shapes) calls this once its own objects are registered, so its system
* writes land the way they do on a real install.
*
* The object mirrors the platform's: managed by better-auth, so it carries no
* tenant column of its own.
*/
export const DEFAULT_ORGANIZATION_ID = 'org_default';

export async function installDefaultOrganization(engine: {
registry: { registerObject(def: unknown, owner: string): unknown };
getSchema(object: string): unknown;
syncSchemas(): Promise<unknown>;
getDriverForObject(object: string):
| {
create(object: string, data: Record<string, unknown>): Promise<unknown>;
findOne(object: string, query: unknown): Promise<unknown>;
}
| undefined;
}): Promise<void> {
// Idempotent: a suite that syncs one engine more than once may call it each time.
if (!engine.getSchema('sys_organization')) {
engine.registry.registerObject(
{ name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
'@objectstack/example-showcase',
);
}
await engine.syncSchemas();
// Seeded through the driver: the organization is data at rest, not a write
// the suite under test should observe (an audit or activity mirror would).
const driver = engine.getDriverForObject('sys_organization')!;
if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
}
4 changes: 4 additions & 0 deletions examples/app-showcase/test/hook-body-persisted-writes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { QuickJSScriptRunner, hookBodyRunnerFactory } from '@objectstack/runtime

import { Account, Project, Task, Inquiry } from '../src/data/objects/index.js';
import { allHooks } from '../src/data/hooks/index.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

/** The app id `AppPlugin` derives its `packageId` from. */
const APP_ID = 'com.objectstack.showcase';
Expand Down Expand Up @@ -87,6 +88,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise<ObjectQL> {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(engine);

engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import stack from '../objectstack.config.js';
import { Account, Project } from '../src/data/objects/index.js';
import { NewProjectWizardPage } from '../src/ui/pages/new-project-wizard.page.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

type Rule = {
type?: string;
Expand Down Expand Up @@ -82,6 +83,9 @@ async function bootShowcase(): Promise<ObjectQL> {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(engine);
return engine;
}

Expand Down
4 changes: 4 additions & 0 deletions examples/app-showcase/test/paid-invoice-line-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
PAID_INVOICE_LINE_REMOVE_REFUSAL,
PAID_INVOICE_LINE_MOVE_REFUSAL,
} from '../src/data/hooks/index.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

const APP_ID = 'com.objectstack.showcase';
const PACKAGE_ID = `app:${APP_ID}`;
Expand Down Expand Up @@ -61,6 +62,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise<ObjectQL> {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(engine);
engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
bodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), { ql: engine, appId: APP_ID }),
Expand Down
4 changes: 4 additions & 0 deletions examples/app-showcase/test/task-reopen-hook-refusal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { Account, Project, Task } from '../src/data/objects/index.js';
import { allHooks, TASK_REOPEN_REFUSAL } from '../src/data/hooks/index.js';
import { ReopenTaskAction } from '../src/ui/actions/index.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

const APP_ID = 'com.objectstack.showcase';
const PACKAGE_ID = `app:${APP_ID}`;
Expand Down Expand Up @@ -65,6 +66,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise<ObjectQL> {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(engine);
engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
bodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), { ql: engine, appId: APP_ID }),
Expand Down
45 changes: 45 additions & 0 deletions examples/app-todo/test/default-organization.testkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Give a lean test engine what every real `single` install has: an
* organization object holding its Default Organization.
*
* ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
* NULL. The engine stamps an organization-less system write with the derived
* organization under `single` (D9) — but only where the composition registers
* the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
* the write reaches the database with no organization and is refused there.
* A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
* shapes) calls this once its own objects are registered, so its system
* writes land the way they do on a real install.
*
* The object mirrors the platform's: managed by better-auth, so it carries no
* tenant column of its own.
*/
export const DEFAULT_ORGANIZATION_ID = 'org_default';

export async function installDefaultOrganization(engine: {
registry: { registerObject(def: unknown, owner: string): unknown };
getSchema(object: string): unknown;
syncSchemas(): Promise<unknown>;
getDriverForObject(object: string):
| {
create(object: string, data: Record<string, unknown>): Promise<unknown>;
findOne(object: string, query: unknown): Promise<unknown>;
}
| undefined;
}): Promise<void> {
// Idempotent: a suite that syncs one engine more than once may call it each time.
if (!engine.getSchema('sys_organization')) {
engine.registry.registerObject(
{ name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
'@objectstack/example-todo',
);
}
await engine.syncSchemas();
// Seeded through the driver: the organization is data at rest, not a write
// the suite under test should observe (an audit or activity mirror would).
const driver = engine.getDriverForObject('sys_organization')!;
if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
}
4 changes: 4 additions & 0 deletions examples/app-todo/test/derived-flag-removal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { Task } from '../src/objects/task.object.js';
import taskHook from '../src/objects/task.hook.js';
import TodoApp from '../objectstack.config.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

/** The two fields this card retired. */
const REMOVED_FIELDS = ['is_completed', 'is_overdue'] as const;
Expand Down Expand Up @@ -94,6 +95,9 @@ async function bootEngine(objectDef: unknown = Task) {
openDrivers.push(driver);
objectql.registry.registerObject(objectDef, 'todo', 'todo');
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);
objectql.bindHooks([taskHook], { packageId: 'app:com.example.todo' });
return objectql;
}
Expand Down
4 changes: 4 additions & 0 deletions examples/app-todo/test/loop-body-iteration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/ser

import { OverdueEscalationFlow, TaskReminderFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

/** One captured `messaging.emit()` call — the notice as the service received it. */
interface CapturedEmit {
Expand Down Expand Up @@ -109,6 +110,9 @@ async function bootTodoKernel(): Promise<{
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);

automation.registerFlow(OverdueEscalationFlow.name, OverdueEscalationFlow);
automation.registerFlow(TaskReminderFlow.name, TaskReminderFlow);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';

import { OverdueEscalationFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

const openDrivers: Array<{ disconnect?: () => Promise<void> }> = [];
afterEach(async () => {
Expand Down Expand Up @@ -106,6 +107,9 @@ async function bootTodoData(): Promise<any> {
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);

return kernel.getService('data');
}
Expand Down
7 changes: 7 additions & 0 deletions examples/app-todo/test/task-completion-trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { allFlows, TaskCompletionFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
import taskHook from '../src/objects/task.hook.js';
import TodoApp from '../objectstack.config.js';
import { installDefaultOrganization } from './default-organization.testkit.js';

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

Expand Down Expand Up @@ -83,6 +84,9 @@ async function bootTodoKernel(): Promise<{
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);

// [#7036] The app's lifecycle hook, bound the way `AppPlugin` binds it from
// `defineStack({ hooks })`. This kernel is assembled by hand (no AppPlugin),
Expand Down Expand Up @@ -359,6 +363,9 @@ describe('#7036 — completing a task is possible for a normal user', () => {
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
// A real `single` install's Default Organization: the system writes below
// derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
await installDefaultOrganization(objectql);
// NB: no `bindHooks` — that is the whole fixture.

const id = await newTask(data);
Expand Down
Loading
Loading