fix: clean default Feishu bitable scaffold
This commit is contained in:
parent
baaa4d5d81
commit
ca6db50215
@ -909,6 +909,236 @@
|
||||
return createdFields;
|
||||
}
|
||||
|
||||
async function updateFeishuBitableTextField(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const appToken = settings.appToken;
|
||||
const tableId = settings.tableId;
|
||||
const fieldId = settings.fieldId;
|
||||
const fieldName = settings.fieldName;
|
||||
if (!token || !appToken || !tableId || !fieldId || !fieldName) {
|
||||
throw new Error("缺少飞书多维表格字段更新参数。");
|
||||
}
|
||||
|
||||
const json = await feishuApiRequest(
|
||||
`/bitable/v1/apps/${encodeURIComponent(appToken)}/tables/${encodeURIComponent(
|
||||
tableId,
|
||||
)}/fields/${encodeURIComponent(fieldId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: {
|
||||
field_name: fieldName,
|
||||
type: 1,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: `更新飞书多维表格字段 ${fieldName}`,
|
||||
},
|
||||
);
|
||||
return json?.data?.field || json?.data || {};
|
||||
}
|
||||
|
||||
async function listFeishuBitableFields(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const appToken = settings.appToken;
|
||||
const tableId = settings.tableId;
|
||||
if (!token || !appToken || !tableId) {
|
||||
throw new Error("缺少飞书多维表格字段查询参数。");
|
||||
}
|
||||
|
||||
const json = await feishuApiRequest(
|
||||
`/bitable/v1/apps/${encodeURIComponent(appToken)}/tables/${encodeURIComponent(
|
||||
tableId,
|
||||
)}/fields`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "获取飞书多维表格字段",
|
||||
},
|
||||
);
|
||||
return json?.data?.items || json?.data?.fields || [];
|
||||
}
|
||||
|
||||
function pickReusableFeishuBitablePrimaryField(fields) {
|
||||
const list = Array.isArray(fields) ? fields : [];
|
||||
return (
|
||||
list.find((field) => field.is_primary || field.isPrimary) ||
|
||||
list.find((field) => field.field_id || field.fieldId || field.id) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareFeishuBitableFields(options) {
|
||||
const settings = options || {};
|
||||
const fields = Array.isArray(settings.fields) ? settings.fields : [];
|
||||
if (!fields.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingFields = await listFeishuBitableFields(settings);
|
||||
const reusableField = pickReusableFeishuBitablePrimaryField(existingFields);
|
||||
const createdFields = [];
|
||||
let remainingFields = fields.slice();
|
||||
|
||||
if (reusableField) {
|
||||
const fieldId = reusableField.field_id || reusableField.fieldId || reusableField.id;
|
||||
createdFields.push(
|
||||
await updateFeishuBitableTextField({
|
||||
...settings,
|
||||
fieldId,
|
||||
fieldName: getFieldLabel(fields[0]),
|
||||
}),
|
||||
);
|
||||
remainingFields = fields.slice(1);
|
||||
}
|
||||
|
||||
createdFields.push(
|
||||
...(await createFeishuBitableTextFields({
|
||||
...settings,
|
||||
fields: remainingFields,
|
||||
})),
|
||||
);
|
||||
return createdFields;
|
||||
}
|
||||
|
||||
async function deleteFeishuBitableField(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const appToken = settings.appToken;
|
||||
const tableId = settings.tableId;
|
||||
const fieldId = settings.fieldId;
|
||||
if (!token || !appToken || !tableId || !fieldId) {
|
||||
throw new Error("缺少飞书多维表格字段删除参数。");
|
||||
}
|
||||
|
||||
await feishuApiRequest(
|
||||
`/bitable/v1/apps/${encodeURIComponent(appToken)}/tables/${encodeURIComponent(
|
||||
tableId,
|
||||
)}/fields/${encodeURIComponent(fieldId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "删除飞书多维表格默认字段",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteFeishuBitableExtraFields(options) {
|
||||
const settings = options || {};
|
||||
const selectedLabels = new Set(
|
||||
(Array.isArray(settings.fields) ? settings.fields : []).map((field) =>
|
||||
getFieldLabel(field),
|
||||
),
|
||||
);
|
||||
const fields = await listFeishuBitableFields(settings);
|
||||
const removableFields = fields.filter((field) => {
|
||||
const fieldName = field.field_name || field.fieldName || field.name;
|
||||
const fieldId = field.field_id || field.fieldId || field.id;
|
||||
return fieldId && fieldName && !selectedLabels.has(fieldName);
|
||||
});
|
||||
|
||||
for (const field of removableFields) {
|
||||
await deleteFeishuBitableField({
|
||||
...settings,
|
||||
fieldId: field.field_id || field.fieldId || field.id,
|
||||
});
|
||||
}
|
||||
return { deletedCount: removableFields.length };
|
||||
}
|
||||
|
||||
async function listFeishuBitableRecords(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const appToken = settings.appToken;
|
||||
const tableId = settings.tableId;
|
||||
if (!token || !appToken || !tableId) {
|
||||
throw new Error("缺少飞书多维表格记录查询参数。");
|
||||
}
|
||||
|
||||
const records = [];
|
||||
let pageToken = "";
|
||||
do {
|
||||
const suffix = pageToken ? `?page_token=${encodeURIComponent(pageToken)}` : "";
|
||||
const json = await feishuApiRequest(
|
||||
`/bitable/v1/apps/${encodeURIComponent(appToken)}/tables/${encodeURIComponent(
|
||||
tableId,
|
||||
)}/records${suffix}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "获取飞书多维表格默认记录",
|
||||
},
|
||||
);
|
||||
const data = json?.data || {};
|
||||
records.push(...(data.items || data.records || []));
|
||||
pageToken = data.has_more ? data.page_token || "" : "";
|
||||
} while (pageToken);
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
async function deleteFeishuBitableRecords(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
const appToken = settings.appToken;
|
||||
const tableId = settings.tableId;
|
||||
const recordIds = Array.isArray(settings.recordIds) ? settings.recordIds : [];
|
||||
if (!token || !appToken || !tableId) {
|
||||
throw new Error("缺少飞书多维表格记录删除参数。");
|
||||
}
|
||||
if (!recordIds.length) {
|
||||
return { deletedCount: 0 };
|
||||
}
|
||||
|
||||
const batchSize = 500;
|
||||
let deletedCount = 0;
|
||||
for (let index = 0; index < recordIds.length; index += batchSize) {
|
||||
const batch = recordIds.slice(index, index + batchSize);
|
||||
await feishuApiRequest(
|
||||
`/bitable/v1/apps/${encodeURIComponent(appToken)}/tables/${encodeURIComponent(
|
||||
tableId,
|
||||
)}/records/batch_delete`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: {
|
||||
records: batch,
|
||||
},
|
||||
fetchImpl: settings.fetchImpl,
|
||||
actionName: "删除飞书多维表格默认记录",
|
||||
},
|
||||
);
|
||||
deletedCount += batch.length;
|
||||
}
|
||||
|
||||
return { deletedCount };
|
||||
}
|
||||
|
||||
async function deleteFeishuBitableDefaultRecords(options) {
|
||||
const records = await listFeishuBitableRecords(options);
|
||||
const recordIds = records
|
||||
.map((record) => record.record_id || record.recordId || record.id)
|
||||
.filter(Boolean);
|
||||
return deleteFeishuBitableRecords({
|
||||
...options,
|
||||
recordIds,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeFeishuBitableRecords(options) {
|
||||
const settings = options || {};
|
||||
const token = settings.tenantAccessToken;
|
||||
@ -970,13 +1200,26 @@
|
||||
title: settings.title,
|
||||
fetchImpl,
|
||||
});
|
||||
await createFeishuBitableTextFields({
|
||||
await prepareFeishuBitableFields({
|
||||
tenantAccessToken,
|
||||
appToken: bitable.appToken,
|
||||
tableId: bitable.tableId,
|
||||
fields,
|
||||
fetchImpl,
|
||||
});
|
||||
await deleteFeishuBitableExtraFields({
|
||||
tenantAccessToken,
|
||||
appToken: bitable.appToken,
|
||||
tableId: bitable.tableId,
|
||||
fields,
|
||||
fetchImpl,
|
||||
});
|
||||
await deleteFeishuBitableDefaultRecords({
|
||||
tenantAccessToken,
|
||||
appToken: bitable.appToken,
|
||||
tableId: bitable.tableId,
|
||||
fetchImpl,
|
||||
});
|
||||
const bitableRecords = buildFeishuBitableRecords(records, fields);
|
||||
const writeResult = await writeFeishuBitableRecords({
|
||||
tenantAccessToken,
|
||||
@ -2444,6 +2687,10 @@
|
||||
createFeishuBitableTextFields,
|
||||
createExportController,
|
||||
createFeishuSpreadsheet,
|
||||
deleteFeishuBitableDefaultRecords,
|
||||
deleteFeishuBitableExtraFields,
|
||||
deleteFeishuBitableField,
|
||||
deleteFeishuBitableRecords,
|
||||
extractBloggerId,
|
||||
exportRecordsToFeishuBitable,
|
||||
exportRecordsToFeishuSpreadsheet,
|
||||
@ -2452,7 +2699,11 @@
|
||||
getFieldLabel,
|
||||
getFeishuFirstSheetId,
|
||||
getFeishuTenantAccessToken,
|
||||
listFeishuBitableFields,
|
||||
listFeishuBitableRecords,
|
||||
parseCreatorInputs,
|
||||
prepareFeishuBitableFields,
|
||||
updateFeishuBitableTextField,
|
||||
writeFeishuBitableRecords,
|
||||
writeFeishuSheetValues,
|
||||
};
|
||||
|
||||
@ -195,6 +195,7 @@ test("exportRecordsToFeishuSpreadsheet creates spreadsheet then writes values",
|
||||
|
||||
test("exportRecordsToFeishuBitable creates app fields and records", async () => {
|
||||
const calls = [];
|
||||
let fieldsGetCount = 0;
|
||||
|
||||
async function fetchImpl(url, options) {
|
||||
const body = options && options.body ? JSON.parse(options.body) : null;
|
||||
@ -220,9 +221,57 @@ test("exportRecordsToFeishuBitable creates app fields and records", async () =>
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/fields")) {
|
||||
if (String(url).endsWith("/fields") && options.method === "POST") {
|
||||
return okJson({ code: 0, data: { field: { field_id: "fld-created" } } });
|
||||
}
|
||||
if (String(url).includes("/fields/") && options.method === "PUT") {
|
||||
return okJson({ code: 0, data: { field: { field_id: "fld-default-text" } } });
|
||||
}
|
||||
if (String(url).endsWith("/fields") && options.method === "GET") {
|
||||
fieldsGetCount += 1;
|
||||
if (fieldsGetCount === 1) {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [
|
||||
{ field_id: "fld-default-text", field_name: "文本", is_primary: true },
|
||||
{ field_id: "fld-default-select", field_name: "单选" },
|
||||
{ field_id: "fld-default-date", field_name: "日期" },
|
||||
{ field_id: "fld-default-attachment", field_name: "附件" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [
|
||||
{ field_id: "fld-default-text", field_name: "达人ID", is_primary: true },
|
||||
{ field_id: "fld-default-select", field_name: "单选" },
|
||||
{ field_id: "fld-default-date", field_name: "日期" },
|
||||
{ field_id: "fld-default-attachment", field_name: "附件" },
|
||||
{ field_id: "fld-name", field_name: "达人昵称" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/fields/")) {
|
||||
return okJson({ code: 0, data: {} });
|
||||
}
|
||||
if (String(url).endsWith("/records") && options.method === "GET") {
|
||||
return okJson({
|
||||
code: 0,
|
||||
data: {
|
||||
items: Array.from({ length: 10 }, (_, index) => ({
|
||||
record_id: `rec-empty-${index + 1}`,
|
||||
fields: {},
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (String(url).includes("/records/batch_delete")) {
|
||||
return okJson({ code: 0, data: {} });
|
||||
}
|
||||
if (String(url).includes("/records/batch_create")) {
|
||||
return okJson({ code: 0, data: { records: [{ record_id: "rec-1" }] } });
|
||||
}
|
||||
@ -250,7 +299,7 @@ test("exportRecordsToFeishuBitable creates app fields and records", async () =>
|
||||
assert.equal(result.tableId, "tbl-default");
|
||||
assert.equal(result.url, "https://feishu.example/base-token");
|
||||
assert.equal(result.rowCount, 1);
|
||||
assert.equal(calls.length, 5);
|
||||
assert.equal(calls.length, 12);
|
||||
assert.deepEqual(calls[0].body, {
|
||||
app_id: "cli_xxx",
|
||||
app_secret: "secret",
|
||||
@ -259,21 +308,56 @@ test("exportRecordsToFeishuBitable creates app fields and records", async () =>
|
||||
name: "测试多维表格",
|
||||
});
|
||||
assert.equal(calls[2].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/fields"), true);
|
||||
assert.deepEqual(calls[2].body, {
|
||||
assert.equal(calls[2].method, "GET");
|
||||
assert.equal(calls[3].url.endsWith("/fields/fld-default-text"), true);
|
||||
assert.equal(calls[3].method, "PUT");
|
||||
assert.deepEqual(calls[3].body, {
|
||||
field_name: "达人ID",
|
||||
type: 1,
|
||||
});
|
||||
assert.deepEqual(calls[3].body, {
|
||||
assert.equal(calls[4].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/fields"), true);
|
||||
assert.equal(calls[4].method, "POST");
|
||||
assert.deepEqual(calls[4].body, {
|
||||
field_name: "达人昵称",
|
||||
type: 1,
|
||||
});
|
||||
assert.equal(calls[5].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/fields"), true);
|
||||
assert.equal(calls[5].method, "GET");
|
||||
assert.equal(calls[6].url.endsWith("/fields/fld-default-select"), true);
|
||||
assert.equal(calls[6].method, "DELETE");
|
||||
assert.equal(calls[7].url.endsWith("/fields/fld-default-date"), true);
|
||||
assert.equal(calls[7].method, "DELETE");
|
||||
assert.equal(calls[8].url.endsWith("/fields/fld-default-attachment"), true);
|
||||
assert.equal(calls[8].method, "DELETE");
|
||||
assert.equal(calls[9].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/records"), true);
|
||||
assert.equal(calls[9].method, "GET");
|
||||
assert.equal(
|
||||
calls[4].url.endsWith(
|
||||
calls[10].url.endsWith(
|
||||
"/bitable/v1/apps/base-token/tables/tbl-default/records/batch_delete",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(calls[10].body, {
|
||||
records: [
|
||||
"rec-empty-1",
|
||||
"rec-empty-2",
|
||||
"rec-empty-3",
|
||||
"rec-empty-4",
|
||||
"rec-empty-5",
|
||||
"rec-empty-6",
|
||||
"rec-empty-7",
|
||||
"rec-empty-8",
|
||||
"rec-empty-9",
|
||||
"rec-empty-10",
|
||||
],
|
||||
});
|
||||
assert.equal(
|
||||
calls[11].url.endsWith(
|
||||
"/bitable/v1/apps/base-token/tables/tbl-default/records/batch_create",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(calls[4].body, {
|
||||
assert.deepEqual(calls[11].body, {
|
||||
records: [
|
||||
{
|
||||
fields: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user