fix: clean default Feishu bitable scaffold
This commit is contained in:
parent
baaa4d5d81
commit
ca6db50215
@ -909,6 +909,236 @@
|
|||||||
return createdFields;
|
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) {
|
async function writeFeishuBitableRecords(options) {
|
||||||
const settings = options || {};
|
const settings = options || {};
|
||||||
const token = settings.tenantAccessToken;
|
const token = settings.tenantAccessToken;
|
||||||
@ -970,13 +1200,26 @@
|
|||||||
title: settings.title,
|
title: settings.title,
|
||||||
fetchImpl,
|
fetchImpl,
|
||||||
});
|
});
|
||||||
await createFeishuBitableTextFields({
|
await prepareFeishuBitableFields({
|
||||||
tenantAccessToken,
|
tenantAccessToken,
|
||||||
appToken: bitable.appToken,
|
appToken: bitable.appToken,
|
||||||
tableId: bitable.tableId,
|
tableId: bitable.tableId,
|
||||||
fields,
|
fields,
|
||||||
fetchImpl,
|
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 bitableRecords = buildFeishuBitableRecords(records, fields);
|
||||||
const writeResult = await writeFeishuBitableRecords({
|
const writeResult = await writeFeishuBitableRecords({
|
||||||
tenantAccessToken,
|
tenantAccessToken,
|
||||||
@ -2444,6 +2687,10 @@
|
|||||||
createFeishuBitableTextFields,
|
createFeishuBitableTextFields,
|
||||||
createExportController,
|
createExportController,
|
||||||
createFeishuSpreadsheet,
|
createFeishuSpreadsheet,
|
||||||
|
deleteFeishuBitableDefaultRecords,
|
||||||
|
deleteFeishuBitableExtraFields,
|
||||||
|
deleteFeishuBitableField,
|
||||||
|
deleteFeishuBitableRecords,
|
||||||
extractBloggerId,
|
extractBloggerId,
|
||||||
exportRecordsToFeishuBitable,
|
exportRecordsToFeishuBitable,
|
||||||
exportRecordsToFeishuSpreadsheet,
|
exportRecordsToFeishuSpreadsheet,
|
||||||
@ -2452,7 +2699,11 @@
|
|||||||
getFieldLabel,
|
getFieldLabel,
|
||||||
getFeishuFirstSheetId,
|
getFeishuFirstSheetId,
|
||||||
getFeishuTenantAccessToken,
|
getFeishuTenantAccessToken,
|
||||||
|
listFeishuBitableFields,
|
||||||
|
listFeishuBitableRecords,
|
||||||
parseCreatorInputs,
|
parseCreatorInputs,
|
||||||
|
prepareFeishuBitableFields,
|
||||||
|
updateFeishuBitableTextField,
|
||||||
writeFeishuBitableRecords,
|
writeFeishuBitableRecords,
|
||||||
writeFeishuSheetValues,
|
writeFeishuSheetValues,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -195,6 +195,7 @@ test("exportRecordsToFeishuSpreadsheet creates spreadsheet then writes values",
|
|||||||
|
|
||||||
test("exportRecordsToFeishuBitable creates app fields and records", async () => {
|
test("exportRecordsToFeishuBitable creates app fields and records", async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
|
let fieldsGetCount = 0;
|
||||||
|
|
||||||
async function fetchImpl(url, options) {
|
async function fetchImpl(url, options) {
|
||||||
const body = options && options.body ? JSON.parse(options.body) : null;
|
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" } } });
|
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")) {
|
if (String(url).includes("/records/batch_create")) {
|
||||||
return okJson({ code: 0, data: { records: [{ record_id: "rec-1" }] } });
|
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.tableId, "tbl-default");
|
||||||
assert.equal(result.url, "https://feishu.example/base-token");
|
assert.equal(result.url, "https://feishu.example/base-token");
|
||||||
assert.equal(result.rowCount, 1);
|
assert.equal(result.rowCount, 1);
|
||||||
assert.equal(calls.length, 5);
|
assert.equal(calls.length, 12);
|
||||||
assert.deepEqual(calls[0].body, {
|
assert.deepEqual(calls[0].body, {
|
||||||
app_id: "cli_xxx",
|
app_id: "cli_xxx",
|
||||||
app_secret: "secret",
|
app_secret: "secret",
|
||||||
@ -259,21 +308,56 @@ test("exportRecordsToFeishuBitable creates app fields and records", async () =>
|
|||||||
name: "测试多维表格",
|
name: "测试多维表格",
|
||||||
});
|
});
|
||||||
assert.equal(calls[2].url.endsWith("/bitable/v1/apps/base-token/tables/tbl-default/fields"), true);
|
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",
|
field_name: "达人ID",
|
||||||
type: 1,
|
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: "达人昵称",
|
field_name: "达人昵称",
|
||||||
type: 1,
|
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(
|
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",
|
"/bitable/v1/apps/base-token/tables/tbl-default/records/batch_create",
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
assert.deepEqual(calls[4].body, {
|
assert.deepEqual(calls[11].body, {
|
||||||
records: [
|
records: [
|
||||||
{
|
{
|
||||||
fields: {
|
fields: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user