Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 ---
Expand Down
113 changes: 113 additions & 0 deletions src/commands/db/update.ts
Original file line number Diff line number Diff line change
@@ -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('<id/url>', 'database ID or URL')
.option(
'--add-prop <definition>',
'add property (repeatable): --add-prop "Name:type:options"',
collectProps,
[],
)
.option(
'--remove-prop <name>',
'remove property (repeatable)',
collectProps,
[],
)
.option(
'--rename-prop <old:new>',
'rename property (repeatable): --rename-prop "Old:New"',
collectProps,
[],
)
.option(
'--set-options <prop:opts>',
'set select/multi_select options (repeatable): --set-options "Status:A,B,C"',
collectProps,
[],
)
.option('--title <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`);
}
}),
);
}
119 changes: 119 additions & 0 deletions src/services/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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':
Expand Down
Loading
Loading