"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.presetConfigFromOptions = exports.baseResourceCommands = void 0; /** * `inkreach-teable base|table|field|record|view` — resource CRUD commands. * * These endpoints are compatible with official teable open-api. */ const commander_1 = require("commander"); const ctx_1 = require("../lib/ctx"); const output_1 = require("../lib/output"); const client_1 = require("../lib/client"); const field_normalize_1 = require("../lib/field-normalize"); const field_display_1 = require("../lib/field-display"); /** * Fetch a field by ID or display name. A direct ID hit needs no extra call; * a 400/404 miss (the API rejects non-ID paths) falls back to resolving the * arg against the table's field list. Unresolvable input rethrows the * original error so the user sees the underlying cause. */ async function getFieldFlexible(client, tableId, fieldIdOrName) { try { return (await client.getField(tableId, fieldIdOrName)); } catch (err) { if (err instanceof client_1.ApiError && (err.status === 400 || err.status === 404)) { const list = (await client.listFields(tableId)); const hit = (0, field_normalize_1.findFieldByNameOrId)(list, fieldIdOrName); if (hit) return (await client.getField(tableId, hit.id)); } throw err; } } /** fieldId → display-name map of a table, to humanize visibilityFilter references. */ async function fieldNameMap(client, tableId) { const list = (await client.listFields(tableId)); return Object.fromEntries(list.map((f) => [f.id, f.name])); } function baseResourceCommands() { return [baseCmd(), tableCmd(), fieldCmd(), recordCmd(), viewCmd(), presetCmd()]; } exports.baseResourceCommands = baseResourceCommands; // ===================== base ===================== function baseCmd() { const cmd = new commander_1.Command('base'); cmd.description('Manage bases (database containers)'); cmd .command('list') .description('List all accessible bases') .action((0, ctx_1.run)(async (ctx) => { const list = (await ctx.client.listBases()); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No bases found.\n'); return; } (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, ], list.map((b) => ({ id: (0, output_1.truncate)(b.id), name: (0, output_1.truncate)(b.name, 30) }))); } })); cmd .command('get ') .description('Get base details') .action((0, ctx_1.run)(async (ctx, args) => { const baseId = args[0]; const base = await ctx.client.getBase(baseId); if (ctx.json) (0, output_1.printJson)(base); else { const b = base; (0, output_1.printKv)(Object.entries(b).map(([k, v]) => [k, v])); } })); return cmd; } // ===================== table ===================== function tableCmd() { const cmd = new commander_1.Command('table'); cmd.description('Manage tables within a base'); cmd .command('list') .description('List tables in a base') .option('--base-id ', 'Base id (overrides default)') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const list = (await ctx.client.listTables(baseId)); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No tables found.\n'); return; } (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'dbTableName', header: 'DB TABLE' }, ], list.map((t) => ({ id: (0, output_1.truncate)(t.id), name: (0, output_1.truncate)(t.name, 30), dbTableName: (0, output_1.truncate)(t.dbTableName ?? ''), }))); } })); cmd .command('get ') .description('Get table details') .action((0, ctx_1.run)(async (ctx, args) => { const tableId = args[0]; const table = await ctx.client.getTable(tableId); if (ctx.json) (0, output_1.printJson)(table); else { const t = table; (0, output_1.printKv)(Object.entries(t).map(([k, v]) => [k, v])); } })); cmd .command('create') .description('Create a new table') .requiredOption('--name ', 'Table name') .option('--base-id ', 'Base id (overrides default)') .option('--fields ', 'Field definitions JSON array, e.g. \'[{"name":"Title","type":"singleLineText"}]\'') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const body = { name: opts.name }; if (opts.fields) { body.fields = (0, field_normalize_1.normalizeFieldRoArray)(JSON.parse(opts.fields)); // Resolve foreignTableName references against existing tables in the base. const fields = body.fields; const tables = await ctx.client.listTables(baseId).catch(() => []); for (const f of fields) { const options = f?.options; if (options && typeof options.foreignTableName === 'string' && !options.foreignTableId) { const t = (0, field_normalize_1.findTableByNameOrId)(tables || [], options.foreignTableName); if (t) options.foreignTableId = t.id; } } } const table = await ctx.client.createTable(baseId, body); if (ctx.json) (0, output_1.printJson)(table); else { const t = table; (0, output_1.printKv)([ ['id', t.id], ['name', t.name], ]); } })); cmd .command('update ') .description('Update table (rename)') .requiredOption('--name ', 'New table name') .action((0, ctx_1.run)(async (ctx, args, o) => { const tableId = args[0]; const opts = o; const table = await ctx.client.updateTable(tableId, { name: opts.name }); if (ctx.json) (0, output_1.printJson)(table); else process.stdout.write(`✓ Table ${tableId} updated\n`); })); cmd .command('delete ') .description('Delete a table') .action((0, ctx_1.run)(async (ctx, args) => { const tableId = args[0]; await ctx.client.deleteTable(tableId); if (ctx.json) (0, output_1.printJson)({ ok: true, id: tableId }); else process.stdout.write(`✓ Table ${tableId} deleted\n`); })); return cmd; } // ===================== field ===================== function fieldCmd() { const cmd = new commander_1.Command('field'); cmd.description('Manage fields (columns) in a table'); cmd .command('list ') .description('List fields in a table') .option('--with-options', 'Add an OPTIONS summary column and expand select choices with their visibility conditions') .action((0, ctx_1.run)(async (ctx, args, o) => { const opts = o; const tableId = args[0]; const list = (await ctx.client.listFields(tableId)); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No fields found.\n'); return; } if (opts.withOptions) { const nameMap = Object.fromEntries(list.map((f) => [String(f.id), String(f.name)])); (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'type', header: 'TYPE' }, { key: 'options', header: 'OPTIONS' }, ], list.map((f) => ({ id: (0, output_1.truncate)(f.id), name: (0, output_1.truncate)(f.name, 30), type: f.type, options: (0, field_display_1.summarizeFieldOptions)(f), }))); const selectFields = list.filter((f) => Array.isArray(f.options?.choices)); for (const f of selectFields) { const rows = (0, field_display_1.choiceRows)(f, nameMap); const conditional = rows.filter((r) => r.when !== '(always)').length; process.stdout.write(`\n◇ ${f.name} (${f.id}) — ${rows.length} choices · ${conditional} conditional\n`); for (const r of rows) { const color = r.color ? ` [${r.color}]` : ''; process.stdout.write(` ${r.name}${color}\n`); if (r.when !== '(always)') { process.stdout.write(` when: ${r.when}\n`); } } } } else { (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'type', header: 'TYPE' }, ], list.map((f) => ({ id: (0, output_1.truncate)(f.id), name: (0, output_1.truncate)(f.name, 30), type: f.type, }))); } } })); cmd .command('get ') .description('Get field details by id or name (full options; select choices rendered with visibility conditions)') .action((0, ctx_1.run)(async (ctx, args) => { const [tableId, fieldIdOrName] = args; const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName); if (ctx.json) (0, output_1.printJson)(field); else { const choices = field.options?.choices; const names = Array.isArray(choices) ? await fieldNameMap(ctx.client, tableId) : undefined; process.stdout.write((0, field_display_1.formatFieldDetail)(field, names) + '\n'); } })); cmd .command('create') .description('Create a new field') .requiredOption('--table-id ', 'Table id') .requiredOption('--name ', 'Field name') .option('--type ', 'Field type — full value (singleLineText, singleSelect, link…) or alias (text, sel, num…). Omitted: inferred from --options shape / --name') .option('--options ', 'Field options JSON (type-specific; select choices accept plain strings)') .option('--description ', 'Field description') .option('--lookup-options ', 'Lookup options JSON (names like linkFieldName/lookupFieldName are resolved to IDs)') .option('--is-lookup', 'Mark the field as a lookup field') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const body = { name: opts.name }; if (opts.options) body.options = JSON.parse(opts.options); if (opts.description) body.description = opts.description; if (opts.lookupOptions) body.lookupOptions = JSON.parse(opts.lookupOptions); if (opts.isLookup) body.isLookup = true; body.type = opts.type ? (0, field_normalize_1.resolveFieldType)(opts.type) : (0, field_normalize_1.inferFieldType)(opts.name, body.options); (0, field_normalize_1.normalizeFieldRo)(body); const errors = await (0, field_normalize_1.resolveFieldReferences)(body, { listTables: (baseId) => ctx.client.listTables(baseId), listFields: (tableId) => ctx.client.listFields(tableId), baseId: ctx.baseId, tableId: opts.tableId, }); if (errors.length) throw new Error(errors.join('; ')); const field = await ctx.client.createField(opts.tableId, body); if (ctx.json) (0, output_1.printJson)(field); else { const f = field; (0, output_1.printKv)([ ['id', f.id], ['name', f.name], ['type', f.type], ]); } })); cmd .command('update ') .description('Update a field (by id or name). --name renames (PATCH); --options rewrites options via the convert API (the update API does not accept options)') .option('--name ', 'New field name') .option('--options ', 'New field options JSON (written via convert API)') .action((0, ctx_1.run)(async (ctx, args, o) => { const [tableId, fieldIdOrName] = args; const opts = o; if (!opts.name && !opts.options) { throw new Error('Nothing to update: pass --name and/or --options'); } if (opts.name && !opts.options) { const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName)); const field = await ctx.client.updateField(tableId, current.id, { name: opts.name }); if (ctx.json) (0, output_1.printJson)(field); else process.stdout.write(`✓ Field ${current.id} renamed\n`); return; } // Options (choices / visibilityFilter / ...) must go through convert. // Fetch the current field to reuse its type; merge name if given. const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName)); const body = { type: current.type, options: JSON.parse(opts.options), }; if (opts.name) body.name = opts.name; (0, field_normalize_1.normalizeFieldRo)(body); const field = await ctx.client.convertField(tableId, current.id, body); if (ctx.json) (0, output_1.printJson)(field); else process.stdout.write(`✓ Field ${current.id} options updated (via convert)\n`); })); cmd .command('visible-choices ') .description('Evaluate select-choice visibility for a record (respects choice visibilityFilter dependencies)') .action((0, ctx_1.run)(async (ctx, args) => { const [tableId, recordId, fieldIdOrName] = args; const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName); const res = await ctx.client.getVisibleChoices(tableId, recordId, String(field.id)); if (ctx.json) (0, output_1.printJson)(res); else { (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'color', header: 'COLOR' }, ], res.visibleChoices.map((c) => ({ ...c, color: c.color ?? '' }))); } })); cmd .command('delete ') .description('Delete a field') .action((0, ctx_1.run)(async (ctx, args) => { const [tableId, fieldId] = args; await ctx.client.deleteField(tableId, fieldId); if (ctx.json) (0, output_1.printJson)({ ok: true, id: fieldId }); else process.stdout.write(`✓ Field ${fieldId} deleted\n`); })); return cmd; } // ===================== record ===================== function recordCmd() { const cmd = new commander_1.Command('record'); cmd.description('Manage records (rows) in a table'); cmd .command('get') .description('Query records with filters, pagination, and search') .requiredOption('--table-id ', 'Table id') .option('--filter ', 'Filter conditions JSON') .option('--projection ', 'Field projection JSON array') .option('--page-size ', 'Records per page') .option('--page ', 'Page number (1-based)') .option('--search ', 'Search keyword') .option('--base-id ', 'Base id (overrides default)') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; // validate baseId is present (even though getRecords only needs tableId, // we follow the convention that base-scoped commands require baseId config) (0, ctx_1.requireBaseId)(ctx, opts.baseId); const query = {}; if (opts.filter) query.filter = JSON.parse(opts.filter); if (opts.projection) query.projection = JSON.parse(opts.projection); if (opts.search) query.search = opts.search; const page = opts.page ? parseInt(opts.page, 10) : undefined; const pageSize = opts.pageSize ? parseInt(opts.pageSize, 10) : undefined; if (pageSize) query.take = pageSize; if (page) { const take = pageSize || 100; query.skip = (page - 1) * take; if (!pageSize) query.take = take; } const result = (await ctx.client.getRecords(opts.tableId, query)); if (ctx.json) { (0, output_1.printJson)(result); } else { if (!result.records?.length) { process.stdout.write('No records found.\n'); return; } const firstFields = result.records[0].fields || {}; const fieldKeys = Object.keys(firstFields); const columns = fieldKeys.map((k) => ({ key: k, header: k })); (0, output_1.printTable)(columns, result.records.map((rec) => { const f = rec.fields || {}; const row = { id: (0, output_1.truncate)(rec.id) }; for (const k of fieldKeys) { row[k] = (0, output_1.truncate)(f[k], 30); } return row; })); if (result.total !== undefined) { process.stdout.write(`\n${result.records.length} of ${result.total} record(s).\n`); } else { process.stdout.write(`\n${result.records.length} record(s).\n`); } } })); cmd .command('get-one ') .description('Get a single record by id') .action((0, ctx_1.run)(async (ctx, args) => { const recordId = args[0]; const record = await ctx.client.getRecord(recordId); if (ctx.json) (0, output_1.printJson)(record); else { const rec = record; const fields = rec.fields || {}; (0, output_1.printKv)([ ['id', rec.id], ...Object.entries(fields).map(([k, v]) => [k, v]), ]); } })); cmd .command('create') .description('Create record(s). Use --fields for a single record, --records for bulk.') .requiredOption('--table-id ', 'Table id') .option('--fields ', 'Fields object JSON, e.g. \'{"Title":"hello"}\'') .option('--records ', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; let body; if (opts.records) { body = { records: JSON.parse(opts.records) }; } else if (opts.fields) { body = { fields: JSON.parse(opts.fields) }; } else { throw Object.assign(new Error('Provide --fields or --records'), { code: 'MISSING_FIELDS', }); } const result = await ctx.client.createRecord(opts.tableId, body); if (ctx.json) (0, output_1.printJson)(result); else process.stdout.write(`✓ Record(s) created\n`); })); cmd .command('update') .description('Batch update records') .requiredOption('--table-id ', 'Table id') .requiredOption('--records ', 'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\'') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const body = { records: JSON.parse(opts.records) }; const result = await ctx.client.updateRecord(opts.tableId, body); if (ctx.json) (0, output_1.printJson)(result); else process.stdout.write(`✓ Records updated\n`); })); cmd .command('delete') .description('Batch delete records by ids') .requiredOption('--table-id ', 'Table id') .requiredOption('--record-ids ', 'Record ids (comma-separated)') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const ids = opts.recordIds .split(',') .map((s) => s.trim()) .filter(Boolean); const result = await ctx.client.deleteRecords(opts.tableId, ids); if (ctx.json) (0, output_1.printJson)(result); else process.stdout.write(`✓ ${ids.length} record(s) deleted\n`); })); return cmd; } // ===================== view ===================== function viewCmd() { const cmd = new commander_1.Command('view'); cmd.description('Manage views in a table'); cmd .command('list ') .description('List views in a table') .action((0, ctx_1.run)(async (ctx, args) => { const tableId = args[0]; const list = (await ctx.client.listViews(tableId)); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No views found.\n'); return; } (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'type', header: 'TYPE' }, ], list.map((v) => ({ id: (0, output_1.truncate)(v.id), name: (0, output_1.truncate)(v.name, 30), type: v.type, }))); } })); cmd .command('get ') .description('Get view details') .action((0, ctx_1.run)(async (ctx, args) => { const viewId = args[0]; const view = await ctx.client.getView(viewId); if (ctx.json) (0, output_1.printJson)(view); else { const v = view; (0, output_1.printKv)(Object.entries(v).map(([k, val]) => [k, val])); } })); cmd .command('create') .description('Create a new view') .requiredOption('--table-id ', 'Table id') .requiredOption('--name ', 'View name') .option('--type ', 'View type (grid, calendar, kanban, gallery, form)', 'grid') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const body = { name: opts.name, type: opts.type }; const view = await ctx.client.createView(opts.tableId, body); if (ctx.json) (0, output_1.printJson)(view); else { const v = view; (0, output_1.printKv)([ ['id', v.id], ['name', v.name], ['type', v.type], ]); } })); cmd .command('update ') .description('Update a view (rename)') .option('--name ', 'New view name') .action((0, ctx_1.run)(async (ctx, args, o) => { const viewId = args[0]; const opts = o; const body = {}; if (opts.name) body.name = opts.name; const view = await ctx.client.updateView(viewId, body); if (ctx.json) (0, output_1.printJson)(view); else process.stdout.write(`✓ View ${viewId} updated\n`); })); cmd .command('delete ') .description('Delete a view') .action((0, ctx_1.run)(async (ctx, args) => { const viewId = args[0]; await ctx.client.deleteView(viewId); if (ctx.json) (0, output_1.printJson)({ ok: true, id: viewId }); else process.stdout.write(`✓ View ${viewId} deleted\n`); })); return cmd; } // ===================== view preset ===================== /** Parse a --filter/--sort/... JSON option; empty/absent → undefined. */ function parseJsonOption(value, optionName) { if (value == null || value === '') return undefined; try { return JSON.parse(value); } catch (err) { throw new Error(`Invalid JSON for ${optionName}: ${err.message}`); } } /** Collect preset config keys from options; absent keys stay out of the config. */ function presetConfigFromOptions(opts) { const config = {}; const entries = [ ['filter', '--filter'], ['sort', '--sort'], ['group', '--group'], ['options', '--options'], ['columnMeta', '--column-meta'], ]; for (const [key, optionName] of entries) { const parsed = parseJsonOption(opts[key], optionName); if (parsed !== undefined) config[key] = parsed; } return config; } exports.presetConfigFromOptions = presetConfigFromOptions; function presetCmd() { const cmd = new commander_1.Command('preset'); cmd.description('Manage preset view configurations (named filter/sort/group bundles, shared with all collaborators)'); cmd .command('list ') .description('List view presets of a table (pass --view-id to scope to one view)') .option('--view-id ', 'Only presets bound to this view') .action((0, ctx_1.run)(async (ctx, args, o) => { const tableId = args[0]; const opts = o; const list = (await ctx.client.listViewPresets(tableId, opts.viewId)); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No view presets found.\n'); return; } (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'config', header: 'CONFIG' }, ], list.map((p) => { const config = (p.config ?? {}); const keys = Object.keys(config).filter((k) => config[k] != null); return { id: (0, output_1.truncate)(p.id), name: (0, output_1.truncate)(p.name, 30), config: keys.join(',') || '(empty)', }; })); } })); cmd .command('create') .description('Create a view preset bound to one view (keys not passed stay unset — they keep the view values on apply)') .requiredOption('--table-id ', 'Table id') .requiredOption('--view-id ', 'View id the preset belongs to') .requiredOption('--name ', 'Preset name') .option('--description ', 'Preset description') .option('--filter ', 'Filter config, e.g. \'{"conjunction":"and","filterSet":[{"fieldId":"fldX","operator":"is","value":"Pending"}]}\'') .option('--sort ', 'Sort config, e.g. \'{"sortObjs":[{"fieldId":"fldX","order":"asc"}]}\'') .option('--group ', 'Group config (JSON array of group items)') .option('--options ', 'View options (JSON object)') .option('--column-meta ', 'Column meta (field visibility/order, JSON object)') .action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const body = { name: opts.name, viewId: opts.viewId, config: presetConfigFromOptions(opts), }; if (opts.description) body.description = opts.description; const preset = (await ctx.client.createViewPreset(opts.tableId, body)); if (ctx.json) (0, output_1.printJson)(preset); else { (0, output_1.printKv)([ ['id', preset.id], ['name', preset.name], ['order', preset.order], ]); } })); cmd .command('update ') .description('Update a view preset (rename and/or replace config keys)') .requiredOption('--table-id ', 'Table id') .option('--name ', 'New preset name') .option('--description ', 'New preset description') .option('--filter ', 'Replace filter config (JSON)') .option('--sort ', 'Replace sort config (JSON)') .option('--group ', 'Replace group config (JSON)') .option('--options ', 'Replace view options (JSON object)') .option('--column-meta ', 'Replace column meta (JSON object)') .action((0, ctx_1.run)(async (ctx, args, o) => { const presetId = args[0]; const opts = o; const body = {}; if (opts.name) body.name = opts.name; if (opts.description !== undefined) body.description = opts.description; const config = presetConfigFromOptions(opts); if (Object.keys(config).length) body.config = config; await ctx.client.updateViewPreset(opts.tableId, presetId, body); if (ctx.json) (0, output_1.printJson)({ ok: true, id: presetId }); else process.stdout.write(`✓ Preset ${presetId} updated\n`); })); cmd .command('delete ') .description('Delete a view preset') .requiredOption('--table-id ', 'Table id') .action((0, ctx_1.run)(async (ctx, args, o) => { const presetId = args[0]; const opts = o; await ctx.client.deleteViewPreset(opts.tableId, presetId); if (ctx.json) (0, output_1.printJson)({ ok: true, id: presetId }); else process.stdout.write(`✓ Preset ${presetId} deleted\n`); })); return cmd; }