From 18be0ee582d04c145dd988a5a483da4c704f22c8 Mon Sep 17 00:00:00 2001 From: Lyu Date: Sat, 27 Jun 2026 16:49:48 -0700 Subject: [PATCH] feat(cli): support custom schemas in records commands via --schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit records list/create/update/delete gain a --schema option that maps to PostgREST's Accept-Profile (reads) / Content-Profile (writes) header, leaving the table path bare. Omitted → backend default (public). Pairs with v2.2.3 custom-schema exposure (InsForge migration 056) and the SDK .schema() support. Co-Authored-By: Claude Opus 4.8 --- src/commands/records/create.ts | 3 +++ src/commands/records/delete.ts | 4 +++- src/commands/records/list.ts | 4 +++- src/commands/records/profile.test.ts | 22 ++++++++++++++++++++++ src/commands/records/profile.ts | 15 +++++++++++++++ src/commands/records/update.ts | 3 +++ 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/commands/records/profile.test.ts create mode 100644 src/commands/records/profile.ts diff --git a/src/commands/records/create.ts b/src/commands/records/create.ts index 713f9741..54dbc601 100644 --- a/src/commands/records/create.ts +++ b/src/commands/records/create.ts @@ -4,12 +4,14 @@ import { requireAuth } from '../../lib/credentials.js'; import { handleError, getRootOpts, CLIError } from '../../lib/errors.js'; import { outputJson, outputSuccess } from '../../lib/output.js'; import { trackCommandUsage } from '../../lib/command-telemetry.js'; +import { schemaProfileHeaders } from './profile.js'; export function registerRecordsCreateCommand(recordsCmd: Command): void { recordsCmd .command('create ') .description('Create record(s) in a table') .option('--data ', 'JSON data to insert (object or array of objects)') + .option('--schema ', 'Schema to target (default: public)') .action(async (table: string, opts, cmd) => { const { json } = getRootOpts(cmd); try { @@ -32,6 +34,7 @@ export function registerRecordsCreateCommand(recordsCmd: Command): void { { method: 'POST', body: JSON.stringify(records), + headers: schemaProfileHeaders('write', opts.schema), }, ); diff --git a/src/commands/records/delete.ts b/src/commands/records/delete.ts index f3dde133..686af872 100644 --- a/src/commands/records/delete.ts +++ b/src/commands/records/delete.ts @@ -4,12 +4,14 @@ import { requireAuth } from '../../lib/credentials.js'; import { handleError, getRootOpts, CLIError } from '../../lib/errors.js'; import { outputJson, outputSuccess } from '../../lib/output.js'; import { trackCommandUsage } from '../../lib/command-telemetry.js'; +import { schemaProfileHeaders } from './profile.js'; export function registerRecordsDeleteCommand(recordsCmd: Command): void { recordsCmd .command('delete
') .description('Delete records from a table matching a filter') .option('--filter ', 'Filter expression (e.g. "id=eq.123")') + .option('--schema ', 'Schema to target (default: public)') .action(async (table: string, opts, cmd) => { const { json } = getRootOpts(cmd); try { @@ -25,7 +27,7 @@ export function registerRecordsDeleteCommand(recordsCmd: Command): void { const res = await ossFetch( `/api/database/records/${encodeURIComponent(table)}?${params}`, - { method: 'DELETE' }, + { method: 'DELETE', headers: schemaProfileHeaders('write', opts.schema) }, ); const data = await res.json() as { data?: unknown[] }; diff --git a/src/commands/records/list.ts b/src/commands/records/list.ts index d7c55c5e..9a81c290 100644 --- a/src/commands/records/list.ts +++ b/src/commands/records/list.ts @@ -4,6 +4,7 @@ import { requireAuth } from '../../lib/credentials.js'; import { handleError, getRootOpts } from '../../lib/errors.js'; import { outputJson, outputTable } from '../../lib/output.js'; import { trackCommandUsage } from '../../lib/command-telemetry.js'; +import { schemaProfileHeaders } from './profile.js'; export function registerRecordsCommands(recordsCmd: Command): void { recordsCmd @@ -14,6 +15,7 @@ export function registerRecordsCommands(recordsCmd: Command): void { .option('--order ', 'Order by (e.g. "created_at.desc")') .option('--limit ', 'Limit number of records', parseInt) .option('--offset ', 'Offset for pagination', parseInt) + .option('--schema ', 'Schema to target (default: public)') .action(async (table: string, opts, cmd) => { const { json } = getRootOpts(cmd); try { @@ -28,7 +30,7 @@ export function registerRecordsCommands(recordsCmd: Command): void { const query = params.toString(); const path = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ''}`; - const res = await ossFetch(path); + const res = await ossFetch(path, { headers: schemaProfileHeaders('read', opts.schema) }); const data = await res.json() as { data?: Record[] }; const records = data.data ?? []; diff --git a/src/commands/records/profile.test.ts b/src/commands/records/profile.test.ts new file mode 100644 index 00000000..d4279c62 --- /dev/null +++ b/src/commands/records/profile.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { schemaProfileHeaders } from './profile.js'; + +describe('schemaProfileHeaders', () => { + it('returns no headers when no schema is given (defaults to public)', () => { + expect(schemaProfileHeaders('read')).toEqual({}); + expect(schemaProfileHeaders('write', undefined)).toEqual({}); + expect(schemaProfileHeaders('read', '')).toEqual({}); + }); + + it('uses Accept-Profile for reads', () => { + expect(schemaProfileHeaders('read', 'analytics')).toEqual({ + 'Accept-Profile': 'analytics', + }); + }); + + it('uses Content-Profile for writes', () => { + expect(schemaProfileHeaders('write', 'analytics')).toEqual({ + 'Content-Profile': 'analytics', + }); + }); +}); diff --git a/src/commands/records/profile.ts b/src/commands/records/profile.ts new file mode 100644 index 00000000..edae8244 --- /dev/null +++ b/src/commands/records/profile.ts @@ -0,0 +1,15 @@ +/** + * PostgREST selects the target schema via a profile header: `Accept-Profile` + * for reads and `Content-Profile` for writes/RPC. The table path stays bare. + * + * Returns the header(s) to send for a `--schema` option, or `{}` when it is + * unset (the backend then uses its default schema, `public`). + */ +export function schemaProfileHeaders( + kind: 'read' | 'write', + schema?: string, +): Record { + if (!schema) return {}; + const header = kind === 'read' ? 'Accept-Profile' : 'Content-Profile'; + return { [header]: schema }; +} diff --git a/src/commands/records/update.ts b/src/commands/records/update.ts index 4e3b4ec1..cea82e6c 100644 --- a/src/commands/records/update.ts +++ b/src/commands/records/update.ts @@ -4,6 +4,7 @@ import { requireAuth } from '../../lib/credentials.js'; import { handleError, getRootOpts, CLIError } from '../../lib/errors.js'; import { outputJson, outputSuccess } from '../../lib/output.js'; import { trackCommandUsage } from '../../lib/command-telemetry.js'; +import { schemaProfileHeaders } from './profile.js'; export function registerRecordsUpdateCommand(recordsCmd: Command): void { recordsCmd @@ -11,6 +12,7 @@ export function registerRecordsUpdateCommand(recordsCmd: Command): void { .description('Update records in a table matching a filter') .option('--filter ', 'Filter expression (e.g. "id=eq.123")') .option('--data ', 'JSON data to update') + .option('--schema ', 'Schema to target (default: public)') .action(async (table: string, opts, cmd) => { const { json } = getRootOpts(cmd); try { @@ -39,6 +41,7 @@ export function registerRecordsUpdateCommand(recordsCmd: Command): void { { method: 'PATCH', body: JSON.stringify(body), + headers: schemaProfileHeaders('write', opts.schema), }, );