"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.subscriptionCommand = void 0; /** * `inkreach-teable subscription` — 订阅中心全配置项 CRUD + 事件查看 + 立即发送。 * * 覆盖订阅中心全部配置:name/table/watchCreated/watchUpdated/fieldIds/recordIds/ * displayFieldId/attachCsv/filter(原生 IFilter JSON)/triggerOnEnter(进入条件语义)/ * sendMode/recipients/notifyInApp/scheduleType/scheduleTime/scheduleWeekday/ * scheduleInterval/isActive。 * * filter 示例(与表格视图筛选同构): * --filter '{"conjunction":"and","filterSet":[{"fieldId":"fldX","operator":"isExactly","value":"已完成"}]}' * 日期筛选值结构:{"mode":"exactDate","exactDate":"2026-08-21","timeZone":"Asia/Shanghai"} * 用户筛选值:["usrXXX"]('Me' 由后端解析为订阅创建者)。 */ const commander_1 = require("commander"); const ctx_1 = require("../lib/ctx"); const output_1 = require("../lib/output"); function parseJsonFlag(raw, flagName) { if (raw === undefined) return undefined; try { return JSON.parse(raw); } catch (err) { throw new Error(`--${flagName} is not valid JSON: ${err.message}\n got: ${raw}`); } } /** 'a@x.com,b@y.com' | '["a@x.com"]' → string[] */ function parseRecipients(raw) { if (raw === undefined) return undefined; const trimmed = raw.trim(); if (trimmed.startsWith('[')) { return parseJsonFlag(raw, 'recipients'); } return trimmed .split(',') .map((s) => s.trim()) .filter(Boolean); } /** 从 CLI 选项构建 RO body;create 时 name/tableId 必填。 */ function buildRo(opts, forCreate) { const body = {}; if (opts.name !== undefined) body.name = opts.name; if (opts.tableId !== undefined) body.tableId = opts.tableId; if (forCreate) { if (!body.name) throw new Error('--name is required for create'); if (!body.tableId) throw new Error('--table-id is required for create'); } if (opts.watchCreated !== undefined) body.watchCreated = opts.watchCreated; if (opts.watchUpdated !== undefined) body.watchUpdated = opts.watchUpdated; const fieldIds = parseJsonFlag(opts.fieldIds, 'field-ids'); if (fieldIds !== undefined) body.fieldIds = fieldIds; const recordIds = parseJsonFlag(opts.recordIds, 'record-ids'); if (recordIds !== undefined) body.recordIds = recordIds; if (opts.displayFieldId !== undefined) { body.displayFieldId = opts.displayFieldId === '' ? null : opts.displayFieldId; } if (opts.attachCsv !== undefined) body.attachCsv = opts.attachCsv; const filter = parseJsonFlag(opts.filter, 'filter'); if (filter !== undefined) body.filter = filter; if (opts.triggerOnEnter !== undefined) body.triggerOnEnter = opts.triggerOnEnter; if (opts.sendMode !== undefined) { if (opts.sendMode !== 'digest' && opts.sendMode !== 'instant') { throw new Error(`--send-mode must be digest|instant, got: ${opts.sendMode}`); } body.sendMode = opts.sendMode; } const recipients = parseRecipients(opts.recipients); if (recipients !== undefined) body.recipients = recipients; if (opts.notifyInApp !== undefined) body.notifyInApp = opts.notifyInApp; if (opts.scheduleType !== undefined) { const t = opts.scheduleType; if (!['daily', 'weekly', 'hours', 'minutes'].includes(t)) { throw new Error(`--schedule-type must be daily|weekly|hours|minutes, got: ${t}`); } body.scheduleType = t; } if (opts.scheduleTime !== undefined) body.scheduleTime = opts.scheduleTime; if (opts.scheduleWeekday !== undefined) { const n = Number(opts.scheduleWeekday); if (!Number.isInteger(n) || n < 0 || n > 6) { throw new Error(`--schedule-weekday must be 0-6 (0=Sunday), got: ${opts.scheduleWeekday}`); } body.scheduleWeekday = n; } if (opts.scheduleInterval !== undefined) { const n = Number(opts.scheduleInterval); if (!Number.isInteger(n) || n < 1) { throw new Error(`--schedule-interval must be a positive integer, got: ${opts.scheduleInterval}`); } body.scheduleInterval = n; } if (opts.isActive !== undefined) body.isActive = opts.isActive; return body; } function addConfigOptions(cmd) { return cmd .option('--base-id ', 'Base id (overrides default)') .option('--name ', 'Subscription name') .option('--table-id ', 'Watched table id (tblXXX)') .option('--no-watch-created', 'Disable watching record creation (default: on)') .option('--no-watch-updated', 'Disable watching record updates (default: on)') .option('--field-ids ', 'Watched field ids as JSON array, e.g. \'["fldA","fldB"]\' (omit = all fields)') .option('--record-ids ', 'Record-level subscription: tracked record ids as JSON array (omit = all records)') .option('--display-field-id ', 'Field shown as record title in emails ("" = primary field)') .option('--attach-csv', 'Attach CSV of changes to digest emails (needs export permission)') .option('--no-attach-csv', 'Do not attach CSV') .option('--filter ', 'Native IFilter JSON (same shape as view filters); null/omit = all records') .option('--trigger-on-enter', 'Only notify on the transition INTO the filter condition (requires --filter)') .option('--no-trigger-on-enter', 'Notify on every matching change (state semantics)') .option('--send-mode ', 'digest | instant') .option('--recipients ', 'Recipient emails: comma-separated or JSON array (default = creator)') .option('--notify-in-app', 'Also send in-app notifications (default)') .option('--no-notify-in-app', 'Disable in-app notifications') .option('--schedule-type ', 'daily | weekly | hours | minutes') .option('--schedule-time ', 'Send time for daily/weekly, e.g. 09:00') .option('--schedule-weekday ', '0-6 (0=Sunday), for weekly') .option('--schedule-interval ', 'Interval for hours/minutes') .option('--active', 'Subscription active (default)') .option('--inactive', 'Pause the subscription'); } function sendModeBadge(s) { return s.sendMode === 'instant' ? 'instant' : 'digest'; } function subscriptionCommand() { const cmd = new commander_1.Command('subscription'); cmd.description('Manage the subscription center (record subscriptions) — full config support'); cmd .command('list') .description('List subscriptions in the base') .option('--table-id ', 'Filter by table') .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.listSubscriptions(baseId, opts.tableId); if (ctx.json) (0, output_1.printJson)(list); else { if (!list.length) { process.stdout.write('No subscriptions found.\n'); return; } (0, output_1.printTable)([ { key: 'id', header: 'ID' }, { key: 'name', header: 'NAME' }, { key: 'tableId', header: 'TABLE' }, { key: 'mode', header: 'MODE' }, { key: 'onEnter', header: 'ON-ENTER' }, { key: 'schedule', header: 'SCHEDULE' }, { key: 'pending', header: 'PENDING' }, { key: 'active', header: 'ACTIVE' }, ], list.map((s) => ({ id: s.id, name: (0, output_1.truncate)(s.name, 24), tableId: s.tableId, mode: sendModeBadge(s), onEnter: s.triggerOnEnter ? '✓' : '', schedule: s.sendMode === 'instant' ? '-' : s.scheduleType, pending: s.pendingCount ?? 0, active: s.isActive ? '✓' : '', }))); process.stdout.write(`\n${list.length} subscription(s). Use 'subscription get ' for details.\n`); } })); const getCmd = cmd.command('get ').description('Show full subscription config'); getCmd.option('--base-id ', 'Base id (overrides default)').action((0, ctx_1.run)(async (ctx, args, o) => { const id = args[0]; const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const list = await ctx.client.listSubscriptions(baseId); const sub = list.find((s) => s.id === id); if (!sub) throw new Error(`Subscription ${id} not found in base ${baseId}`); if (ctx.json) (0, output_1.printJson)(sub); else { (0, output_1.printKv)([ ['id', sub.id], ['name', sub.name], ['tableId', sub.tableId], ['watchCreated', String(sub.watchCreated)], ['watchUpdated', String(sub.watchUpdated)], ['fieldIds', sub.fieldIds?.length ? sub.fieldIds.join(', ') : '(all fields)'], [ 'recordIds', sub.recordIds?.length ? `${sub.recordIds.length} record(s)` : '(all records)', ], ['displayFieldId', sub.displayFieldId ?? '(primary)'], ['attachCsv', String(sub.attachCsv)], ['filter', sub.filter ? JSON.stringify(sub.filter) : '(none)'], ['triggerOnEnter', String(sub.triggerOnEnter)], ['sendMode', sendModeBadge(sub)], ['recipients', (sub.recipients ?? []).join(', ')], ['notifyInApp', String(sub.notifyInApp)], ['scheduleType', sub.scheduleType], ['scheduleTime', sub.scheduleTime ?? ''], ['scheduleWeekday', sub.scheduleWeekday != null ? String(sub.scheduleWeekday) : ''], ['scheduleInterval', sub.scheduleInterval != null ? String(sub.scheduleInterval) : ''], ['isActive', String(sub.isActive)], ['pendingCount', String(sub.pendingCount ?? 0)], ['lastSentTime', sub.lastSentTime ?? ''], ]); } })); const createCmd = cmd .command('create') .description('Create a subscription (--name and --table-id required)'); addConfigOptions(createCmd).action((0, ctx_1.run)(async (ctx, _args, o) => { const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const body = buildRo(opts, true); const vo = await ctx.client.createSubscription(baseId, body); if (ctx.json) (0, output_1.printJson)(vo); else (0, output_1.printKv)([ ['id', vo.id], ['name', vo.name], ['created', '✓'], ]); })); const updateCmd = cmd .command('update ') .description('Update a subscription (only provided options change)'); addConfigOptions(updateCmd).action((0, ctx_1.run)(async (ctx, args, o) => { const id = args[0]; const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const body = buildRo(opts, false); if (Object.keys(body).length === 0) { throw new Error('No options provided — nothing to update'); } const vo = await ctx.client.updateSubscription(baseId, id, body); if (ctx.json) (0, output_1.printJson)(vo); else (0, output_1.printKv)([ ['id', vo.id], ['updated', '✓'], ]); })); cmd .command('delete ') .description('Delete a subscription') .option('--base-id ', 'Base id (overrides default)') .action((0, ctx_1.run)(async (ctx, args, o) => { const id = args[0]; const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); await ctx.client.deleteSubscription(baseId, id); if (ctx.json) (0, output_1.printJson)({ id }); else process.stdout.write(`Deleted ${id}\n`); })); cmd .command('events ') .description('List captured events (pending + sent) of a subscription') .option('--base-id ', 'Base id (overrides default)') .action((0, ctx_1.run)(async (ctx, args, o) => { const id = args[0]; const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const events = (await ctx.client.getSubscriptionEvents(baseId, id)); if (ctx.json) (0, output_1.printJson)(events); else { if (!events.length) { process.stdout.write('No events captured.\n'); return; } (0, output_1.printTable)([ { key: 'recordId', header: 'RECORD' }, { key: 'changeType', header: 'CHANGE' }, { key: 'createdTime', header: 'CAPTURED' }, { key: 'sentTime', header: 'SENT' }, ], events.map((e) => ({ recordId: e.recordId, changeType: e.changeType, createdTime: e.createdTime, sentTime: e.sentTime ?? '(pending)', }))); } })); cmd .command('send ') .description('Send pending digest events immediately (flush now)') .option('--base-id ', 'Base id (overrides default)') .action((0, ctx_1.run)(async (ctx, args, o) => { const id = args[0]; const opts = o; const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId); const result = await ctx.client.sendSubscriptionNow(baseId, id); if (ctx.json) (0, output_1.printJson)(result ?? { id }); else (0, output_1.printKv)([ ['id', id], ['sent', '✓'], ]); })); return cmd; } exports.subscriptionCommand = subscriptionCommand;