feat(cli): record 写入 --typecast/--order/--field-key-type + 附件字段 URL 导入 skill 文档 (0.2.0)

This commit is contained in:
yeuimu
2026-10-14 00:21:00 +00:00
parent a3fa01e25f
commit 4462475a6f
9 changed files with 206 additions and 18 deletions
+110 -4
View File
@@ -20,6 +20,55 @@ import {
} from '../lib/field-normalize';
import { formatFieldDetail, summarizeFieldOptions, choiceRows } from '../lib/field-display';
const FIELD_KEY_TYPES = ['name', 'id', 'dbFieldName'] as const;
type FieldKeyTypeCli = (typeof FIELD_KEY_TYPES)[number];
/** Validate --field-key-type (backend body key `fieldKeyType`, default "name"). */
function parseFieldKeyType(raw: string | undefined): FieldKeyTypeCli | undefined {
if (raw === undefined) return undefined;
if (!(FIELD_KEY_TYPES as readonly string[]).includes(raw)) {
throw Object.assign(
new Error(`--field-key-type must be one of: ${FIELD_KEY_TYPES.join(', ')}`),
{ code: 'BAD_FIELD_KEY_TYPE' }
);
}
return raw as FieldKeyTypeCli;
}
type RecordOrderRo = { viewId: string; anchorId: string; position: 'before' | 'after' };
/**
* Validate --order (backend body key `order` on record create/update):
* place the written records before/after an anchor record in a
* manually-sorted view.
*/
function parseRecordOrder(raw: string): RecordOrderRo {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw Object.assign(
new Error('--order must be JSON: {"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'),
{ code: 'BAD_JSON' }
);
}
const obj = parsed as Record<string, unknown>;
const { viewId, anchorId, position } = obj ?? {};
if (
typeof viewId !== 'string' ||
typeof anchorId !== 'string' ||
(position !== 'before' && position !== 'after')
) {
throw Object.assign(
new Error(
'--order needs string viewId + anchorId and position "before"|"after", e.g. {"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'
),
{ code: 'BAD_ORDER' }
);
}
return { viewId, anchorId, position };
}
/**
* 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
@@ -677,10 +726,34 @@ function recordCmd(): Command {
.requiredOption('--table-id <id>', 'Table id')
.option('--fields <json>', 'Fields object JSON, e.g. \'{"Title":"hello"}\'')
.option('--records <json>', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'')
.option(
'--typecast',
'Send typecast=true (accepts human-readable values; attachment fields accept http(s) urls)'
)
.option(
'--field-key-type <type>',
'Keys in --fields/--records are field ids ("id"), names ("name", default) or dbFieldName'
)
.option(
'--order <json>',
'Insert position in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}'
)
.action(
run(async (ctx, _args, o) => {
const opts = o as { tableId: string; fields?: string; records?: string };
let body: unknown;
const opts = o as {
tableId: string;
fields?: string;
records?: string;
typecast?: boolean;
fieldKeyType?: string;
order?: string;
};
let body: {
typecast?: boolean;
fieldKeyType?: FieldKeyTypeCli;
order?: RecordOrderRo;
records: unknown;
};
if (opts.records) {
body = { records: JSON.parse(opts.records) };
} else if (opts.fields) {
@@ -692,6 +765,11 @@ function recordCmd(): Command {
code: 'MISSING_FIELDS',
});
}
// typecast/fieldKeyType/order are BODY keys of createRecordsRoSchema (not query params)
if (opts.typecast) body.typecast = true;
const fieldKeyType = parseFieldKeyType(opts.fieldKeyType);
if (fieldKeyType) body.fieldKeyType = fieldKeyType;
if (opts.order !== undefined) body.order = parseRecordOrder(opts.order);
const result = await ctx.client.createRecord(opts.tableId, body);
if (ctx.json) printJson(result);
else process.stdout.write(`✓ Record(s) created\n`);
@@ -706,10 +784,38 @@ function recordCmd(): Command {
'--records <json>',
'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\''
)
.option(
'--typecast',
'Send typecast=true (accepts human-readable values; attachment fields accept http(s) urls)'
)
.option(
'--field-key-type <type>',
'Keys in --records fields are field ids ("id"), names ("name", default) or dbFieldName'
)
.option(
'--order <json>',
'Move the updated record(s) before/after an anchor in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}'
)
.action(
run(async (ctx, _args, o) => {
const opts = o as { tableId: string; records: string };
const body = { records: JSON.parse(opts.records) };
const opts = o as {
tableId: string;
records: string;
typecast?: boolean;
fieldKeyType?: string;
order?: string;
};
const body: {
typecast?: boolean;
fieldKeyType?: FieldKeyTypeCli;
order?: RecordOrderRo;
records: unknown;
} = { records: JSON.parse(opts.records) };
// typecast/fieldKeyType/order are BODY keys of updateRecordsRoSchema (not query params)
if (opts.typecast) body.typecast = true;
const fieldKeyType = parseFieldKeyType(opts.fieldKeyType);
if (fieldKeyType) body.fieldKeyType = fieldKeyType;
if (opts.order !== undefined) body.order = parseRecordOrder(opts.order);
const result = await ctx.client.updateRecord(opts.tableId, body);
if (ctx.json) printJson(result);
else process.stdout.write(`✓ Records updated\n`);
+9 -6
View File
@@ -153,14 +153,17 @@ export class Client {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call<unknown>('GET', `/api/table/${tableId}/record/${recordId}${qs}`);
}
createRecord(tableId: string, body: unknown) {
return this.call<unknown>('POST', `/api/table/${tableId}/record`, body);
createRecord(tableId: string, body: unknown, query?: Record<string, unknown>) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call<unknown>('POST', `/api/table/${tableId}/record${qs}`, body);
}
updateRecord(tableId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/table/${tableId}/record`, body);
updateRecord(tableId: string, body: unknown, query?: Record<string, unknown>) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call<unknown>('PATCH', `/api/table/${tableId}/record${qs}`, body);
}
updateRecordOne(tableId: string, recordId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/table/${tableId}/record/${recordId}`, body);
updateRecordOne(tableId: string, recordId: string, body: unknown, query?: Record<string, unknown>) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call<unknown>('PATCH', `/api/table/${tableId}/record/${recordId}${qs}`, body);
}
deleteRecords(tableId: string, recordIds: string[]) {
if (recordIds.length === 1) {