feat(cli): record 写入 --typecast/--order/--field-key-type + 附件字段 URL 导入 skill 文档 (0.2.0)
This commit is contained in:
+110
-4
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user