Skip to content

Commit be4580a

Browse files
sweetmantechclaude
andcommitted
fix(chat-workflow): Next.js build — discriminated-union narrowing + supabase type depth
Two production-build issues surfaced by Vercel that local pnpm test + tsc didn't catch (vitest uses esbuild transpile, no type check; tsc's errors were all in __tests__ unrelated to this PR). 1. `compareAndSetChatActiveStreamId.ts` — `if (result.ok) { ... }` narrowing wasn't kicking in under Next.js's strict TS plugin. Switched to `if ("error" in result)` (in-operator narrowing) which reliably discriminates the union members regardless of literal-type inference quirks. 2. `lib/supabase/chats/updateChat.ts` — `let query = supabase.from(...) .update(...).eq(...)` + reassignment in a `for` loop (`.is()` / `.eq()` per where entry) caused "type instantiation is excessively deep" — Supabase's PostgrestFilterBuilder is heavily generic and the reassignment kept expanding the type. Rewrote as: split where map into equality matches (one `.match(obj)` call) + nullable columns (reduced with `.is(col, null)` typed back to the original builder). Both bugs were behavior-neutral — the function shape and contract are unchanged. 37/37 tests in touched files green; full suite 2955/2955; lint clean; `pnpm build` now succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 21bd131 commit be4580a

3 files changed

Lines changed: 41 additions & 11 deletions

File tree

‎lib/chat/compareAndSetChatActiveStreamId.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export async function compareAndSetChatActiveStreamId(
4141
{active_stream_id: next},
4242
);
4343

44-
if(!result.ok){
44+
if("error"inresult){
4545
return{ok: false,error: result.error};
4646
}
4747

‎lib/supabase/chats/__tests__/updateChat.test.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { updateChat } from "@/lib/supabase/chats/updateChat";
33

44
constupdateChain=vi.fn();
55
consteqChain=vi.fn();
6+
constmatchChain=vi.fn();
67
constisChain=vi.fn();
78
constselectChain=vi.fn();
89

@@ -14,10 +15,12 @@ vi.mock("@/lib/supabase/serverClient", () => ({
1415

1516
beforeEach(()=>{
1617
vi.clearAllMocks();
17-
// Any number of chained .eq() / .is() calls return the same fluent builder.
18-
constbuilder={eq: eqChain,is: isChain,select: selectChain};
18+
// Fluent builder mock — every method returns the same builder so we can
19+
// chain .eq / .match / .is / .select in any order without per-step setup.
20+
constbuilder={eq: eqChain,match: matchChain,is: isChain,select: selectChain};
1921
updateChain.mockReturnValue(builder);
2022
eqChain.mockReturnValue(builder);
23+
matchChain.mockReturnValue(builder);
2124
isChain.mockReturnValue(builder);
2225
});
2326

@@ -33,6 +36,8 @@ describe("updateChat", () => {
3336
expect(result.row).toEqual(row);
3437
expect(updateChain).toHaveBeenCalledWith({title: "renamed"});
3538
expect(eqChain).toHaveBeenCalledWith("id","chat-1");
39+
// With no where filter, match is called with an empty object.
40+
expect(matchChain).toHaveBeenCalledWith({});
3641
});
3742

3843
it("returns ok:false with error on Supabase failure",async()=>{
@@ -52,25 +57,29 @@ describe("updateChat", () => {
5257
{active_stream_id: "wrun_x"},
5358
);
5459
expect(isChain).toHaveBeenCalledWith("active_stream_id",null);
60+
// No non-null fields → match called with empty {}
61+
expect(matchChain).toHaveBeenCalledWith({});
5562
});
5663

57-
it("emits `eq` for non-null values (e.g. CAS expecting a specific run id)",async()=>{
64+
it("emits `match()` for non-null values (e.g. CAS expecting a specific run id)",async()=>{
5865
selectChain.mockResolvedValue({data: [{id: "c-1"}],error: null});
5966
awaitupdateChat(
6067
{id: "c-1",where: {active_stream_id: "wrun_old"}},
6168
{active_stream_id: "wrun_new"},
6269
);
63-
expect(eqChain).toHaveBeenCalledWith("active_stream_id","wrun_old");
70+
expect(matchChain).toHaveBeenCalledWith({active_stream_id: "wrun_old"});
71+
// No null fields → is() not called
72+
expect(isChain).not.toHaveBeenCalled();
6473
});
6574

66-
it("AND-s multiple where columns together",async()=>{
75+
it("AND-s nullable + equality where columns together",async()=>{
6776
selectChain.mockResolvedValue({data: [{id: "c-1"}],error: null});
6877
awaitupdateChat(
6978
{id: "c-1",where: {active_stream_id: null,model_id: "anthropic/claude-haiku-4.5"}},
7079
{title: "x"},
7180
);
7281
expect(isChain).toHaveBeenCalledWith("active_stream_id",null);
73-
expect(eqChain).toHaveBeenCalledWith("model_id","anthropic/claude-haiku-4.5");
82+
expect(matchChain).toHaveBeenCalledWith({model_id:"anthropic/claude-haiku-4.5"});
7483
});
7584

7685
it("returns ok:true rowsUpdated:0 when the predicate matches no row (race lost)",async()=>{

‎lib/supabase/chats/updateChat.ts‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,33 @@ export async function updateChat(
4646
filter: UpdateChatFilter,
4747
updates: ChatMutableFields,
4848
): Promise<UpdateChatResult>{
49-
letquery=supabase.from("chats").update(updates).eq("id",filter.id);
50-
for(const[column,value]ofObject.entries(filter.where??{})){
51-
query=value===null ? query.is(column,null) : query.eq(column,value);
49+
// Split the optional `where` map into nullable vs equality predicates so we
50+
// can apply each as a single chained call (`.match()` for equalities,
51+
// `.is(col, null)` per nullable). Iterating with `let query = ...` and
52+
// reassigning in a for-loop confuses Supabase's deeply generic builder
53+
// types ("type instantiation is excessively deep") in the Next.js build.
54+
constentries=Object.entries(filter.where??{});
55+
constequalityMatches: Record<string,unknown>={};
56+
constnullColumns: string[]=[];
57+
for(const[column,value]ofentries){
58+
if(value===null){
59+
nullColumns.push(column);
60+
}else{
61+
equalityMatches[column]=value;
62+
}
5263
}
5364

54-
const{ data, error }=awaitquery.select();
65+
constbaseQuery=supabase
66+
.from("chats")
67+
.update(updates)
68+
.eq("id",filter.id)
69+
.match(equalityMatches);
70+
constfinalQuery=nullColumns.reduce<typeofbaseQuery>(
71+
(q,column)=>q.is(column,null)astypeofbaseQuery,
72+
baseQuery,
73+
);
74+
75+
const{ data, error }=awaitfinalQuery.select();
5576
if(error){
5677
console.error("[updateChat] error:",error);
5778
return{ok: false,error: error.message};

0 commit comments

Comments
 (0)