Skip to content

Commit f8c9594

Browse files
os-zhuangclaude
andauthored
fix(showcase): stop passing a non-existent org column when seeding the phone demo user (#3408)
The approval demo's `ensurePhoneDemoUser` inserted `sys_user` with an `organization_id` key. `sys_user` has no such field — not in the object definition (25 fields, none org/tenant) and not physically (26 columns, no `organization_id`); org membership lives on `sys_member`. The key is not dropped on the way down: it reaches SQL as a real column and the insert dies with `table sys_user has no column named organization_id`. The insert is wrapped in a best-effort try/catch, so this surfaced only as a warn line and an ERROR in the boot log — and the demo user was **never** provisioned on any boot, leaving the phone surfaces (#3358 §6 "Phone sign-in surfaces": All Users list, record detail) empty with nothing to look at. Also drop the dead `admin.organization_id` read in `run`: the same missing field made it permanently `undefined`, so only the `sys_member` fallback ever ran. Resolve from `sys_member` directly and correct the comment, which claimed the column existed but was null. Verified on a wiped dev DB: boot log goes from 1 ERROR to 0, and `sys_user` now holds `usr_showcase_phone_demo` / `Mei Phone (demo)` / `+8613800138000`. Approval fixtures unaffected (2 pending requests + 3 admin positions still stamped with the org). `tsc --noEmit` passes. Follow-up to #3364; found while collecting evidence for #3358 §1. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f243727 commit f8c9594

1 file changed

Lines changed: 17 additions & 21 deletions

File tree

examples/app-showcase/src/security/seed-approval-demo.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,16 @@ async function assignAdminPositions(
126126
}
127127

128128
/** Provision a phone-based demo user (best-effort; renders the phone surfaces). */
129-
async function ensurePhoneDemoUser(ctx: ApprovalDemoContext, organizationId: string | null): Promise<void> {
129+
async function ensurePhoneDemoUser(ctx: ApprovalDemoContext): Promise<void> {
130130
const existing = await findOne(ctx, 'sys_user', { email: PHONE_DEMO_USER.email });
131131
if (existing) return;
132132
try {
133-
await ctx.ql.insert(
134-
'sys_user',
135-
{
136-
...PHONE_DEMO_USER,
137-
...(organizationId ? { organization_id: organizationId } : {}),
138-
},
139-
{ context: SYS },
140-
);
133+
// `sys_user` carries NO org column — org membership lives on `sys_member`
134+
// (see the resolution in `run` below). An `organization_id` key here is not
135+
// silently dropped: it reaches SQL as a real column and the insert dies with
136+
// "table sys_user has no column named organization_id", so the demo user is
137+
// never provisioned and the phone surfaces render empty.
138+
await ctx.ql.insert('sys_user', { ...PHONE_DEMO_USER }, { context: SYS });
141139
ctx.logger?.info?.('[showcase] approval-demo phone user provisioned', { email: PHONE_DEMO_USER.email });
142140
} catch (err) {
143141
// Non-fatal: sign-in still needs a better-auth account; this row just makes
@@ -215,20 +213,18 @@ export function registerShowcaseApprovalDemo(ctx: ApprovalDemoContext): void {
215213
return;
216214
}
217215
const adminId = String(admin.id);
218-
// The active org lives on the better-auth membership (`sys_member`), NOT on
219-
// `sys_user.organization_id` (which is null for the dev admin). Both the
220-
// position rows AND the requests must carry this org, or the org-scoped
221-
// approver resolution (`sys_user_position` filtered by org) and `getRequest`
222-
// (org-scoped read that the inbox drawer uses) silently return nothing.
223-
let organizationId = (admin.organization_id as string | undefined) ?? null;
224-
if (!organizationId) {
225-
const ownerMember = await findOne(ctx, 'sys_member', { user_id: adminId, role: 'owner' });
226-
const anyMember = ownerMember ?? (await findOne(ctx, 'sys_member', { user_id: adminId }));
227-
organizationId = (anyMember?.organization_id as string | undefined) ?? null;
228-
}
216+
// The active org lives on the better-auth membership (`sys_member`).
217+
// `sys_user` has no org column at all, so there is nothing to read off the
218+
// admin row first. Both the position rows AND the requests must carry this
219+
// org, or the org-scoped approver resolution (`sys_user_position` filtered
220+
// by org) and `getRequest` (the org-scoped read behind the inbox drawer)
221+
// silently return nothing.
222+
const ownerMember = await findOne(ctx, 'sys_member', { user_id: adminId, role: 'owner' });
223+
const anyMember = ownerMember ?? (await findOne(ctx, 'sys_member', { user_id: adminId }));
224+
const organizationId = (anyMember?.organization_id as string | undefined) ?? null;
229225

230226
await assignAdminPositions(ctx, adminId, organizationId);
231-
await ensurePhoneDemoUser(ctx, organizationId);
227+
await ensurePhoneDemoUser(ctx);
232228

233229
let engine: AutomationEngineLike | undefined;
234230
try {

0 commit comments

Comments
 (0)