From c618d09f10c9989107e74798615edadb260a3829 Mon Sep 17 00:00:00 2001 From: Andrzej Chmielewski Date: Tue, 7 Apr 2026 21:17:30 +0200 Subject: [PATCH] feat: add db update command for schema modification and option management Adds 'notion db update ' with flags to modify database schemas: --add-prop Add new properties (reuses existing prop definition parser) --remove-prop Remove properties --rename-prop Rename properties --set-options Replace select/multi_select options --title Update database title Uses the Notion SDK's dataSources.update() API. Includes 28 new tests covering all operations and error cases. Closes #21 Closes #24 --- src/cli.ts | 2 + src/commands/db/update.ts | 113 +++++++ src/services/database.service.ts | 119 +++++++ tests/commands/db-update.test.ts | 267 +++++++++++++++ .../services/database.service.update.test.ts | 305 ++++++++++++++++++ 5 files changed, 806 insertions(+) create mode 100644 src/commands/db/update.ts create mode 100644 tests/commands/db-update.test.ts create mode 100644 tests/services/database.service.update.test.ts diff --git a/src/cli.ts b/src/cli.ts index c861fdd..30d6c18 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,7 @@ import { createPageCommand } from './commands/create-page.js'; import { dbCreateCommand } from './commands/db/create.js'; import { dbQueryCommand } from './commands/db/query.js'; import { dbSchemaCommand } from './commands/db/schema.js'; +import { dbUpdateCommand } from './commands/db/update.js'; import { deleteBlockCommand } from './commands/delete-block.js'; import { editPageCommand } from './commands/edit-page.js'; import { initCommand } from './commands/init.js'; @@ -126,6 +127,7 @@ const dbCmd = new Command('db').description('Database operations'); dbCmd.addCommand(dbCreateCommand()); dbCmd.addCommand(dbSchemaCommand()); dbCmd.addCommand(dbQueryCommand()); +dbCmd.addCommand(dbUpdateCommand()); program.addCommand(dbCmd); // --- Utilities --- diff --git a/src/commands/db/update.ts b/src/commands/db/update.ts new file mode 100644 index 0000000..0bae411 --- /dev/null +++ b/src/commands/db/update.ts @@ -0,0 +1,113 @@ +import { Command } from 'commander'; +import { resolveToken } from '../../config/token.js'; +import { CliError } from '../../errors/cli-error.js'; +import { ErrorCodes } from '../../errors/codes.js'; +import { withErrorHandling } from '../../errors/error-handler.js'; +import { createNotionClient } from '../../notion/client.js'; +import { formatJSON, getOutputMode } from '../../output/format.js'; +import { reportTokenSource } from '../../output/stderr.js'; +import { + buildDatabaseUpdatePayload, + fetchDatabaseSchema, + resolveDataSourceId, + updateDatabaseSchema, +} from '../../services/database.service.js'; + +interface DbUpdateOpts { + addProp: string[]; + removeProp: string[]; + renameProp: string[]; + setOptions: string[]; + title?: string; +} + +function collectProps(val: string, acc: string[]): string[] { + acc.push(val); + return acc; +} + +export function dbUpdateCommand(): Command { + return new Command('update') + .description( + 'Update database schema (add/remove/rename properties, manage options)', + ) + .argument('', 'database ID or URL') + .option( + '--add-prop ', + 'add property (repeatable): --add-prop "Name:type:options"', + collectProps, + [], + ) + .option( + '--remove-prop ', + 'remove property (repeatable)', + collectProps, + [], + ) + .option( + '--rename-prop ', + 'rename property (repeatable): --rename-prop "Old:New"', + collectProps, + [], + ) + .option( + '--set-options ', + 'set select/multi_select options (repeatable): --set-options "Status:A,B,C"', + collectProps, + [], + ) + .option('--title ', 'update database title') + .action( + withErrorHandling(async (id: string, opts: DbUpdateOpts) => { + const hasOperations = + opts.addProp.length > 0 || + opts.removeProp.length > 0 || + opts.renameProp.length > 0 || + opts.setOptions.length > 0 || + opts.title !== undefined; + + if (!hasOperations) { + throw new CliError( + ErrorCodes.INVALID_ARG, + 'No update operations specified', + 'Provide at least one of: --add-prop, --remove-prop, --rename-prop, --set-options, --title', + ); + } + + const { token, source } = await resolveToken(); + reportTokenSource(source); + const client = createNotionClient(token); + + const dsId = await resolveDataSourceId(client, id); + + // Fetch schema when rename or set-options operations need it + const needsSchema = + opts.renameProp.length > 0 || opts.setOptions.length > 0; + const schema = needsSchema + ? await fetchDatabaseSchema(client, dsId) + : { id: dsId, databaseId: dsId, title: '', properties: {} }; + + const payload = buildDatabaseUpdatePayload( + { + addProps: opts.addProp, + removeProps: opts.removeProp, + renameProps: opts.renameProp, + setOptions: opts.setOptions, + title: opts.title, + }, + schema, + ); + + const response = await updateDatabaseSchema(client, dsId, payload); + + if (getOutputMode() === 'json') { + process.stdout.write(`${formatJSON(response)}\n`); + return; + } + + if ('url' in response) { + process.stdout.write(`${response.url}\n`); + } + }), + ); +} diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 5ba548d..0bd29ce 100644 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -4,6 +4,8 @@ import type { DatabaseObjectResponse, PageObjectResponse, QueryDataSourceParameters, + UpdateDataSourceParameters, + UpdateDataSourceResponse, } from '@notionhq/client/build/src/api-endpoints.js'; import { CliError } from '../errors/cli-error.js'; import { ErrorCodes } from '../errors/codes.js'; @@ -413,6 +415,123 @@ export async function resolveDataSourceId( ); } +// --- Database schema update --- + +export interface DatabaseUpdateOptions { + addProps: string[]; + removeProps: string[]; + renameProps: string[]; + setOptions: string[]; + title?: string; +} + +type UpdatePayload = Omit<UpdateDataSourceParameters, 'data_source_id'>; + +/** + * Build the `dataSources.update()` payload from CLI flag values. + * + * For `--rename-prop` and `--set-options`, the current schema is required to + * look up property IDs and types. + */ +export function buildDatabaseUpdatePayload( + opts: DatabaseUpdateOptions, + schema: DatabaseSchema, +): UpdatePayload { + const properties: Record<string, unknown> = {}; + + // --add-prop: reuse existing parsePropertyDefinition + for (const def of opts.addProps) { + const { name, config } = parsePropertyDefinition(def); + properties[name] = config; + } + + // --remove-prop: set to null + for (const name of opts.removeProps) { + properties[name] = null; + } + + // --rename-prop: parse "OldName:NewName", look up property ID + for (const raw of opts.renameProps) { + const colonIdx = raw.indexOf(':'); + if (colonIdx === -1) { + throw new CliError( + ErrorCodes.INVALID_ARG, + `Invalid rename format: "${raw}"`, + 'Use format: --rename-prop "OldName:NewName"', + ); + } + const oldName = raw.slice(0, colonIdx).trim(); + const newName = raw.slice(colonIdx + 1).trim(); + const propConfig = schema.properties[oldName]; + if (!propConfig) { + const available = Object.keys(schema.properties).join(', '); + throw new CliError( + ErrorCodes.INVALID_ARG, + `Property "${oldName}" not found in schema`, + `Available properties: ${available}`, + ); + } + properties[propConfig.id] = { name: newName }; + } + + // --set-options: parse "PropName:opt1,opt2,opt3" + for (const raw of opts.setOptions) { + const colonIdx = raw.indexOf(':'); + if (colonIdx === -1) { + throw new CliError( + ErrorCodes.INVALID_ARG, + `Invalid set-options format: "${raw}"`, + 'Use format: --set-options "PropertyName:opt1,opt2,opt3"', + ); + } + const propName = raw.slice(0, colonIdx).trim(); + const optionsStr = raw.slice(colonIdx + 1).trim(); + const propConfig = schema.properties[propName]; + if (!propConfig) { + const available = Object.keys(schema.properties).join(', '); + throw new CliError( + ErrorCodes.INVALID_ARG, + `Property "${propName}" not found in schema`, + `Available properties: ${available}`, + ); + } + if (propConfig.type !== 'select' && propConfig.type !== 'multi_select') { + throw new CliError( + ErrorCodes.INVALID_ARG, + `Property "${propName}" is of type "${propConfig.type}" — only select and multi_select properties support --set-options`, + ); + } + const options = optionsStr.split(',').map((opt) => ({ name: opt.trim() })); + properties[propName] = { [propConfig.type]: { options } }; + } + + const payload: UpdatePayload = {}; + + if (opts.title !== undefined) { + payload.title = [{ type: 'text', text: { content: opts.title } }]; + } + + if (Object.keys(properties).length > 0) { + payload.properties = properties as UpdateDataSourceParameters['properties']; + } + + return payload; +} + +/** + * Update a Notion data source schema via the API. + */ +export async function updateDatabaseSchema( + client: Client, + dataSourceId: string, + payload: UpdatePayload, +): Promise<UpdateDataSourceResponse> { + return client.dataSources.update({ + data_source_id: dataSourceId, + ...payload, + }); +} + export function displayPropertyValue(prop: PropValue): string { switch (prop.type) { case 'title': diff --git a/tests/commands/db-update.test.ts b/tests/commands/db-update.test.ts new file mode 100644 index 0000000..0c46207 --- /dev/null +++ b/tests/commands/db-update.test.ts @@ -0,0 +1,267 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { + mockResolveDataSourceId, + mockFetchDatabaseSchema, + mockBuildDatabaseUpdatePayload, + mockUpdateDatabaseSchema, +} = vi.hoisted(() => ({ + mockResolveDataSourceId: vi + .fn() + .mockResolvedValue('aabbccdd-1122-3344-5566-778899aabbcc'), + mockFetchDatabaseSchema: vi.fn().mockResolvedValue({ + id: 'aabbccdd-1122-3344-5566-778899aabbcc', + databaseId: 'db-id-123', + title: 'My DB', + properties: { + Name: { id: 'title-id', name: 'Name', type: 'title' }, + Status: { + id: 'status-id', + name: 'Status', + type: 'select', + options: [{ name: 'Todo' }, { name: 'Done' }], + }, + Tags: { + id: 'tags-id', + name: 'Tags', + type: 'multi_select', + options: [{ name: 'bug' }], + }, + }, + }), + mockBuildDatabaseUpdatePayload: vi.fn().mockReturnValue({ properties: {} }), + mockUpdateDatabaseSchema: vi.fn().mockResolvedValue({ + object: 'data_source', + id: 'aabbccdd-1122-3344-5566-778899aabbcc', + url: 'https://notion.so/db-123', + }), +})); + +vi.mock('../../src/config/token.js', () => ({ + resolveToken: vi + .fn() + .mockResolvedValue({ token: 'test-token', source: 'env' }), +})); + +vi.mock('../../src/output/stderr.js', () => ({ + reportTokenSource: vi.fn(), +})); + +vi.mock('../../src/notion/client.js', () => ({ + createNotionClient: vi.fn(() => ({})), +})); + +vi.mock('../../src/services/database.service.js', () => ({ + resolveDataSourceId: mockResolveDataSourceId, + fetchDatabaseSchema: mockFetchDatabaseSchema, + buildDatabaseUpdatePayload: mockBuildDatabaseUpdatePayload, + updateDatabaseSchema: mockUpdateDatabaseSchema, +})); + +import { dbUpdateCommand } from '../../src/commands/db/update.js'; +import { setOutputMode } from '../../src/output/format.js'; + +describe('dbUpdateCommand', () => { + let stdoutSpy: ReturnType<typeof vi.spyOn>; + let stderrSpy: ReturnType<typeof vi.spyOn>; + let exitSpy: ReturnType<typeof vi.spyOn>; + + beforeEach(() => { + vi.clearAllMocks(); + setOutputMode('auto'); + stdoutSpy = vi + .spyOn(process.stdout, 'write') + .mockImplementation(() => true); + stderrSpy = vi + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + exitSpy = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + exitSpy.mockRestore(); + }); + + it('errors when no operation flags are provided', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync(['node', 'test', 'b55c9c91384d452b81dbd1ef79372b75']); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('No update operations specified'), + ); + }); + + it('calls buildDatabaseUpdatePayload with parsed flags and calls updateDatabaseSchema', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--add-prop', + 'Priority:select:High,Low', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ addProps: ['Priority:select:High,Low'] }), + expect.anything(), + ); + expect(mockUpdateDatabaseSchema).toHaveBeenCalledWith( + expect.anything(), + 'aabbccdd-1122-3344-5566-778899aabbcc', + expect.anything(), + ); + }); + + it('passes remove-prop flag to buildDatabaseUpdatePayload', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--remove-prop', + 'Status', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ removeProps: ['Status'] }), + expect.anything(), + ); + }); + + it('fetches schema and passes rename-prop flag to buildDatabaseUpdatePayload', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--rename-prop', + 'Status:State', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockFetchDatabaseSchema).toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ renameProps: ['Status:State'] }), + expect.anything(), + ); + }); + + it('fetches schema and passes set-options flag to buildDatabaseUpdatePayload', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--set-options', + 'Status:Todo,In Progress,Done', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockFetchDatabaseSchema).toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ setOptions: ['Status:Todo,In Progress,Done'] }), + expect.anything(), + ); + }); + + it('does not fetch schema when only add-prop and remove-prop are used', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--add-prop', + 'Score:number', + '--remove-prop', + 'Tags', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockFetchDatabaseSchema).not.toHaveBeenCalled(); + }); + + it('passes title flag to buildDatabaseUpdatePayload', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--title', + 'New Title', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ title: 'New Title' }), + expect.anything(), + ); + }); + + it('outputs JSON when global output mode is json', async () => { + setOutputMode('json'); + + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--title', + 'New Title', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + const output = stdoutSpy.mock.calls[0][0] as string; + const parsed = JSON.parse(output); + expect(parsed).toEqual( + expect.objectContaining({ + id: 'aabbccdd-1122-3344-5566-778899aabbcc', + }), + ); + }); + + it('outputs URL by default (non-JSON mode)', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--title', + 'New Title', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(stdoutSpy).toHaveBeenCalledWith('https://notion.so/db-123\n'); + }); + + it('passes all flags to buildDatabaseUpdatePayload in one call', async () => { + const cmd = dbUpdateCommand(); + await cmd.parseAsync([ + 'node', + 'test', + 'b55c9c91384d452b81dbd1ef79372b75', + '--title', + 'New Title', + '--add-prop', + 'Score:number', + '--remove-prop', + 'Tags', + ]); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(mockBuildDatabaseUpdatePayload).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'New Title', + addProps: ['Score:number'], + removeProps: ['Tags'], + }), + expect.anything(), + ); + }); +}); diff --git a/tests/services/database.service.update.test.ts b/tests/services/database.service.update.test.ts new file mode 100644 index 0000000..36256f5 --- /dev/null +++ b/tests/services/database.service.update.test.ts @@ -0,0 +1,305 @@ +import type { Client } from '@notionhq/client'; +import { describe, expect, it, vi } from 'vitest'; +import { CliError } from '../../src/errors/cli-error.js'; +import { + buildDatabaseUpdatePayload, + updateDatabaseSchema, +} from '../../src/services/database.service.js'; + +// Minimal schema fixture used across tests +const SCHEMA = { + id: 'ds-id', + databaseId: 'db-id', + title: 'My DB', + properties: { + Name: { id: 'title-id', name: 'Name', type: 'title' }, + Status: { + id: 'status-id', + name: 'Status', + type: 'select', + options: [{ name: 'Todo' }, { name: 'Done' }], + }, + Tags: { + id: 'tags-id', + name: 'Tags', + type: 'multi_select', + options: [{ name: 'bug' }], + }, + }, +}; + +describe('buildDatabaseUpdatePayload', () => { + describe('--add-prop', () => { + it('adds a simple property', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: ['Score:number'], + removeProps: [], + renameProps: [], + setOptions: [], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ Score: { number: {} } }), + ); + }); + + it('adds a select property with options', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: ['Priority:select:High,Low'], + removeProps: [], + renameProps: [], + setOptions: [], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ + Priority: { + select: { options: [{ name: 'High' }, { name: 'Low' }] }, + }, + }), + ); + }); + }); + + describe('--remove-prop', () => { + it('sets property to null', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: ['Status'], + renameProps: [], + setOptions: [], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ Status: null }), + ); + }); + }); + + describe('--rename-prop', () => { + it('uses property id as key with new name', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: ['Status:State'], + setOptions: [], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ 'status-id': { name: 'State' } }), + ); + }); + + it('throws CliError when property does not exist', () => { + expect(() => + buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: ['NonExistent:NewName'], + setOptions: [], + }, + SCHEMA, + ), + ).toThrow(CliError); + }); + + it('throws CliError for invalid rename format (missing colon)', () => { + expect(() => + buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: ['StatusOnly'], + setOptions: [], + }, + SCHEMA, + ), + ).toThrow(CliError); + }); + }); + + describe('--set-options', () => { + it('replaces select options', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: ['Status:Todo,In Progress,Done'], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ + Status: { + select: { + options: [ + { name: 'Todo' }, + { name: 'In Progress' }, + { name: 'Done' }, + ], + }, + }, + }), + ); + }); + + it('replaces multi_select options', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: ['Tags:bug,feature'], + }, + SCHEMA, + ); + expect(payload.properties).toEqual( + expect.objectContaining({ + Tags: { + multi_select: { + options: [{ name: 'bug' }, { name: 'feature' }], + }, + }, + }), + ); + }); + + it('throws CliError when property does not exist', () => { + expect(() => + buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: ['NonExistent:A,B'], + }, + SCHEMA, + ), + ).toThrow(CliError); + }); + + it('throws CliError when property is not select or multi_select', () => { + expect(() => + buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: ['Name:A,B'], + }, + SCHEMA, + ), + ).toThrow(CliError); + }); + + it('throws CliError for invalid set-options format (missing colon)', () => { + expect(() => + buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: ['StatusOnly'], + }, + SCHEMA, + ), + ).toThrow(CliError); + }); + }); + + describe('--title', () => { + it('sets title in payload', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: [], + removeProps: [], + renameProps: [], + setOptions: [], + title: 'New Title', + }, + SCHEMA, + ); + expect(payload.title).toEqual([ + { type: 'text', text: { content: 'New Title' } }, + ]); + }); + + it('omits title when not provided', () => { + const payload = buildDatabaseUpdatePayload( + { addProps: [], removeProps: [], renameProps: [], setOptions: [] }, + SCHEMA, + ); + expect(payload.title).toBeUndefined(); + }); + }); + + it('merges multiple operations into one payload', () => { + const payload = buildDatabaseUpdatePayload( + { + addProps: ['Score:number'], + removeProps: ['Tags'], + renameProps: ['Status:State'], + setOptions: [], + title: 'Updated DB', + }, + SCHEMA, + ); + expect(payload.title).toEqual([ + { type: 'text', text: { content: 'Updated DB' } }, + ]); + expect(payload.properties).toEqual( + expect.objectContaining({ + Score: { number: {} }, + Tags: null, + 'status-id': { name: 'State' }, + }), + ); + }); +}); + +describe('updateDatabaseSchema', () => { + it('calls client.dataSources.update with the payload', async () => { + const mockUpdate = vi.fn().mockResolvedValue({ + object: 'data_source', + id: 'ds-id', + url: 'https://notion.so/db-123', + }); + const client = { + dataSources: { update: mockUpdate }, + } as unknown as Client; + + const payload = { + title: [{ type: 'text' as const, text: { content: 'New' } }], + }; + await updateDatabaseSchema(client, 'ds-id', payload); + + expect(mockUpdate).toHaveBeenCalledWith({ + data_source_id: 'ds-id', + ...payload, + }); + }); + + it('returns the API response', async () => { + const response = { + object: 'data_source', + id: 'ds-id', + url: 'https://notion.so/db-123', + }; + const client = { + dataSources: { update: vi.fn().mockResolvedValue(response) }, + } as unknown as Client; + + const result = await updateDatabaseSchema(client, 'ds-id', {}); + expect(result).toEqual(response); + }); +});