From 3c4b37bac9e97e5b0dfda87a9448573fb5956214 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:30:53 +0000 Subject: [PATCH 1/2] refactor(stack-supabase): derive hasEncryptedColumns from the shared rule ColumnMap.hasEncryptedColumns re-derived the plaintext-table rule from encryptedColumnNames instead of calling tableHasEncryptedColumns, the rule create.ts uses to choose what Encryption receives. It now reads that function once in the constructor, so the two cannot drift. Also routes decryptResults' Supabase-error branch through respond(), and folds the two near-identical test client builders into one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Jmn7nhALwP5BxDDr1reNV --- .../supabase-plaintext-table.test.ts | 38 ++++++++++--------- .../packages/stack-supabase/src/column-map.ts | 11 +++--- .../stack-supabase/src/query-results.ts | 14 +------ 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts b/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts index eab1a26c8..4e7e8da92 100644 --- a/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts +++ b/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts @@ -105,29 +105,31 @@ const auditRows = [ { id: 2, action: 'logout' }, ] -async function introspectedClient(resultData: unknown) { - const encryption = registryEnforcingFactory() - const supabase = createMockSupabase(resultData) - const es = await makeEncryptedSupabase(encryption.factory, introspector)( - supabase.client as never, - { databaseUrl: 'postgres://x' }, - ) - return { es, encryption, supabase } -} - const users = encryptedTable('users', { email: types.TextSearch('email') }) const auditLog = encryptedTable('audit_log', {}) -async function declaredClient(resultData: unknown) { - const encryption = registryEnforcingFactory() - const supabase = createMockSupabase(resultData) - const es = await makeEncryptedSupabase(encryption.factory, null)( - supabase.client as never, - { schemas: { users, audit_log: auditLog } }, - ) - return { es, encryption, supabase } +function clientFor( + modeIntrospector: Introspector | null, + config: { databaseUrl: string } | { schemas: Record }, +) { + return async (resultData: unknown) => { + const encryption = registryEnforcingFactory() + const supabase = createMockSupabase(resultData) + const es = await makeEncryptedSupabase( + encryption.factory, + modeIntrospector, + )(supabase.client as never, config as never) + return { es, encryption, supabase } + } } +const introspectedClient = clientFor(introspector, { + databaseUrl: 'postgres://x', +}) +const declaredClient = clientFor(null, { + schemas: { users, audit_log: auditLog }, +}) + describe.each([ ['introspection mode', introspectedClient], ['declared mode', declaredClient], diff --git a/languages/typescript/packages/stack-supabase/src/column-map.ts b/languages/typescript/packages/stack-supabase/src/column-map.ts index 101c66547..c8bb5a89e 100644 --- a/languages/typescript/packages/stack-supabase/src/column-map.ts +++ b/languages/typescript/packages/stack-supabase/src/column-map.ts @@ -154,6 +154,10 @@ export class ColumnMap { private readonly dbToProp: Record /** Column builders keyed by BOTH property name and DB name. */ private readonly v3Columns: Record + /** False for a plaintext table, which the encrypt and decrypt steps must keep + * away from the encryption client. Read from {@link tableHasEncryptedColumns}, + * the same rule `create.ts` uses to choose what `Encryption` receives. */ + readonly hasEncryptedColumns: boolean constructor( tableName: string, @@ -210,12 +214,7 @@ export class ColumnMap { } this.encryptedColumnNames = Object.keys(this.v3Columns) - } - - /** False for a plaintext table, which the encrypt and decrypt steps must keep - * away from the encryption client (see {@link tableHasEncryptedColumns}). */ - get hasEncryptedColumns(): boolean { - return this.encryptedColumnNames.length > 0 + this.hasEncryptedColumns = tableHasEncryptedColumns(table) } /** Resolve a JS property name to its DB column name. `Object.hasOwn` guards diff --git a/languages/typescript/packages/stack-supabase/src/query-results.ts b/languages/typescript/packages/stack-supabase/src/query-results.ts index 95c3433c0..b2d2c72ef 100644 --- a/languages/typescript/packages/stack-supabase/src/query-results.ts +++ b/languages/typescript/packages/stack-supabase/src/query-results.ts @@ -180,18 +180,8 @@ export async function decryptResults( // If there's an error from Supabase, pass it through if (result.error) { - return { - data: null, - error: { - message: result.error.message, - details: result.error.details, - hint: result.error.hint, - code: result.error.code, - }, - count: result.count ?? null, - status: result.status, - statusText: result.statusText, - } + const { message, details, hint, code } = result.error + return { ...respond(null), error: { message, details, hint, code } } } // No data to decrypt From 520e1d879fa848ea0e95ec0cd2e00a937b349fc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 01:49:18 +0000 Subject: [PATCH 2/2] refactor(stack-supabase): keep test options type-checked, group public fields clientFor took its options as a hand-written union and cast them to never, so a renamed EncryptedSupabaseOptions key no longer failed the build. It now takes a connect callback, keeping the options literal at the makeEncryptedSupabase call where the overloads check it. Also moves ColumnMap.hasEncryptedColumns up with the other public readonly fields. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012Jmn7nhALwP5BxDDr1reNV --- .../supabase-plaintext-table.test.ts | 26 +++++++++---------- .../packages/stack-supabase/src/column-map.ts | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts b/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts index 4e7e8da92..a6fdfb014 100644 --- a/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts +++ b/languages/typescript/packages/stack-supabase/__tests__/supabase-plaintext-table.test.ts @@ -108,27 +108,27 @@ const auditRows = [ const users = encryptedTable('users', { email: types.TextSearch('email') }) const auditLog = encryptedTable('audit_log', {}) -function clientFor( - modeIntrospector: Introspector | null, - config: { databaseUrl: string } | { schemas: Record }, +function clientFor( + connect: (factory: EncryptionFactory, client: never) => Promise, ) { return async (resultData: unknown) => { const encryption = registryEnforcingFactory() const supabase = createMockSupabase(resultData) - const es = await makeEncryptedSupabase( - encryption.factory, - modeIntrospector, - )(supabase.client as never, config as never) + const es = await connect(encryption.factory, supabase.client as never) return { es, encryption, supabase } } } -const introspectedClient = clientFor(introspector, { - databaseUrl: 'postgres://x', -}) -const declaredClient = clientFor(null, { - schemas: { users, audit_log: auditLog }, -}) +const introspectedClient = clientFor((factory, client) => + makeEncryptedSupabase(factory, introspector)(client, { + databaseUrl: 'postgres://x', + }), +) +const declaredClient = clientFor((factory, client) => + makeEncryptedSupabase(factory, null)(client, { + schemas: { users, audit_log: auditLog }, + }), +) describe.each([ ['introspection mode', introspectedClient], diff --git a/languages/typescript/packages/stack-supabase/src/column-map.ts b/languages/typescript/packages/stack-supabase/src/column-map.ts index c8bb5a89e..132bb7aab 100644 --- a/languages/typescript/packages/stack-supabase/src/column-map.ts +++ b/languages/typescript/packages/stack-supabase/src/column-map.ts @@ -146,6 +146,10 @@ export class ColumnMap { * Filters and select strings address columns by both, so recognition must * cover both. */ readonly encryptedColumnNames: string[] + /** False for a plaintext table, which the encrypt and decrypt steps must keep + * away from the encryption client. Read from {@link tableHasEncryptedColumns}, + * the same rule `create.ts` uses to choose what `Encryption` receives. */ + readonly hasEncryptedColumns: boolean /** DB column name → JS property name — the inverse of {@link propToDb}, used * to expand `select('*')` back into property names. Null prototype: a DB @@ -154,10 +158,6 @@ export class ColumnMap { private readonly dbToProp: Record /** Column builders keyed by BOTH property name and DB name. */ private readonly v3Columns: Record - /** False for a plaintext table, which the encrypt and decrypt steps must keep - * away from the encryption client. Read from {@link tableHasEncryptedColumns}, - * the same rule `create.ts` uses to choose what `Encryption` receives. */ - readonly hasEncryptedColumns: boolean constructor( tableName: string,