From 769dda87a342f6ce72fa653ec1e7916862163b81 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:11:19 +0800 Subject: [PATCH] fix(showcase): stop passing a non-existent org column when seeding the phone demo user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Claude Opus 4.8 --- .../src/security/seed-approval-demo.ts | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/examples/app-showcase/src/security/seed-approval-demo.ts b/examples/app-showcase/src/security/seed-approval-demo.ts index 778d2a199..114cf43de 100644 --- a/examples/app-showcase/src/security/seed-approval-demo.ts +++ b/examples/app-showcase/src/security/seed-approval-demo.ts @@ -126,18 +126,16 @@ async function assignAdminPositions( } /** Provision a phone-based demo user (best-effort; renders the phone surfaces). */ -async function ensurePhoneDemoUser(ctx: ApprovalDemoContext, organizationId: string | null): Promise { +async function ensurePhoneDemoUser(ctx: ApprovalDemoContext): Promise { const existing = await findOne(ctx, 'sys_user', { email: PHONE_DEMO_USER.email }); if (existing) return; try { - await ctx.ql.insert( - 'sys_user', - { - ...PHONE_DEMO_USER, - ...(organizationId ? { organization_id: organizationId } : {}), - }, - { context: SYS }, - ); + // `sys_user` carries NO org column — org membership lives on `sys_member` + // (see the resolution in `run` below). An `organization_id` key here is not + // silently dropped: it reaches SQL as a real column and the insert dies with + // "table sys_user has no column named organization_id", so the demo user is + // never provisioned and the phone surfaces render empty. + await ctx.ql.insert('sys_user', { ...PHONE_DEMO_USER }, { context: SYS }); ctx.logger?.info?.('[showcase] approval-demo phone user provisioned', { email: PHONE_DEMO_USER.email }); } catch (err) { // Non-fatal: sign-in still needs a better-auth account; this row just makes @@ -215,20 +213,18 @@ export function registerShowcaseApprovalDemo(ctx: ApprovalDemoContext): void { return; } const adminId = String(admin.id); - // The active org lives on the better-auth membership (`sys_member`), NOT on - // `sys_user.organization_id` (which is null for the dev admin). Both the - // position rows AND the requests must carry this org, or the org-scoped - // approver resolution (`sys_user_position` filtered by org) and `getRequest` - // (org-scoped read that the inbox drawer uses) silently return nothing. - let organizationId = (admin.organization_id as string | undefined) ?? null; - if (!organizationId) { - const ownerMember = await findOne(ctx, 'sys_member', { user_id: adminId, role: 'owner' }); - const anyMember = ownerMember ?? (await findOne(ctx, 'sys_member', { user_id: adminId })); - organizationId = (anyMember?.organization_id as string | undefined) ?? null; - } + // The active org lives on the better-auth membership (`sys_member`). + // `sys_user` has no org column at all, so there is nothing to read off the + // admin row first. Both the position rows AND the requests must carry this + // org, or the org-scoped approver resolution (`sys_user_position` filtered + // by org) and `getRequest` (the org-scoped read behind the inbox drawer) + // silently return nothing. + const ownerMember = await findOne(ctx, 'sys_member', { user_id: adminId, role: 'owner' }); + const anyMember = ownerMember ?? (await findOne(ctx, 'sys_member', { user_id: adminId })); + const organizationId = (anyMember?.organization_id as string | undefined) ?? null; await assignAdminPositions(ctx, adminId, organizationId); - await ensurePhoneDemoUser(ctx, organizationId); + await ensurePhoneDemoUser(ctx); let engine: AutomationEngineLike | undefined; try {