Files
Arcrun/n8n-nodes-kbdb/nodes/Kbdb/Kbdb.node.ts
T
uncle6me-web 87f6de7c9f feat(n8n): KBDB community node 薄殼——對照表 那半的八個動作
leo 2026-08-15「有效才正式開發」下一格:紙上考卷驗過的動作集做成真的 n8n 節點。
詞彙照定案(sheet/field/record,InkStoneCo system-dev/wiki/kbdb-詞彙表.md)。

- 動作 ↔ API 映射收斂在 nodes/Kbdb/actions.ts(純函式),20 個 vitest 釘死
  ——就是動作對照表  那半的機器版;🔴/◐ 的動作刻意不存在,呼叫丟
  「沒有這個能力」(07-thin-shell §3.1:API 沒有的能力不拼裝)
- 對外走 cypher-executor /kbdb/* proxy(X-Arcrun-API-Key),不直連 kbdb worker
- Add a field 不掛:proxy 無 PATCH /kbdb/templates(實測 404);base 端
  PATCH /templates/:id 語意是 slots 整組覆蓋(本地實測),不是加一欄

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:42:53 +08:00

284 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IHttpRequestMethods,
IDataObject,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { buildRequest, type ActionParams } from './actions';
/**
* KBDB node(薄殼)。
*
* 詞彙依 leo 2026-08-15 定案(InkStoneCo `system-dev/wiki/kbdb-詞彙表.md`):
* 對外只有三個字——sheet(一張表=一個看法)/field(一欄)/record(一筆記錄,
* 有自己的 ID,同一筆可以出現在好幾張表裡)。
*
* 動作集=動作對照表標 ✅ 的那半。標 🔴(關係整組、Archive 兩個)與 ◐ 裡
* 語意不成立的(Add a field),**刻意不在這裡**——API 沒有的能力,薄殼不拼裝。
*/
export class Kbdb implements INodeType {
description: INodeTypeDescription = {
displayName: 'KBDB',
name: 'kbdb',
icon: 'file:kbdb.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description:
'KBDB:把東西和「怎麼看東西」拆開的資料庫。先把 record 丟進去,再用 sheet 決定怎麼看它——不建表、不用 SQL、不用工程師。',
defaults: { name: 'KBDB' },
inputs: ['main'],
outputs: ['main'],
credentials: [{ name: 'kbdbApi', required: true }],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{ name: 'Record', value: 'record', description: '一筆記錄。有自己的 ID' },
{ name: 'Sheet', value: 'sheet', description: '一張表=一個看法。收起它,記錄不會消失' },
{ name: 'Search', value: 'search', description: '搜尋你存進 KBDB 的內容' },
],
default: 'record',
},
// ── record operations ────────────────────────────────────────────
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['record'] } },
options: [
{
name: 'Append Record',
value: 'append',
description: '在一張 sheet 裡新增一筆記錄',
action: 'Append a record to a sheet',
},
{
name: 'Get a Record',
value: 'get',
description: '用 record ID 取回一筆記錄',
action: 'Get a record',
},
{
name: 'Get Many Records',
value: 'getMany',
description: '列出一張 sheet 裡(你 namespace 底下)的所有記錄',
action: 'Get many records from a sheet',
},
{
name: 'Update a Record',
value: 'update',
description: '改一筆既有記錄的欄位值(只改你給的欄,其他欄不動)',
action: 'Update a record',
},
],
default: 'append',
},
// ── sheet operations ─────────────────────────────────────────────
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['sheet'] } },
options: [
{
name: 'Create a Sheet',
value: 'create',
description: '開一張新表=寫下你想看哪幾個欄位。不建實體表、不用等人',
action: 'Create a sheet',
},
{
name: 'Get Sheet Schema',
value: 'getSchema',
description: '看一張 sheet 有哪些欄位',
action: 'Get sheet schema',
},
{
name: 'List Sheets',
value: 'list',
description: '列出所有 sheet',
action: 'List sheets',
},
],
default: 'list',
},
// ── search operations ────────────────────────────────────────────
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['search'] } },
options: [
{
name: 'Search',
value: 'search',
description:
'全文/語意搜尋。搜的是內容本身:命中回「那一格的內容」(含 entry id),不是整筆 record,也不含同筆的其他欄——目前沒有從命中反查所屬 record 的能力',
action: 'Search KBDB',
},
],
default: 'search',
},
// ── shared fields ────────────────────────────────────────────────
{
displayName: 'Sheet',
name: 'sheet',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['record', 'sheet'],
operation: ['append', 'getMany', 'create', 'getSchema'],
},
},
description: 'Sheet 名稱(或 ID',
},
{
displayName: 'Record ID',
name: 'recordId',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['record'], operation: ['get', 'update'] } },
description: '記錄自己的 IDrec_…)',
},
{
displayName: 'Values',
name: 'values',
type: 'json',
default: '{}',
required: true,
displayOptions: { show: { resource: ['record'], operation: ['append', 'update'] } },
description: '欄位值,JSON 物件:{"欄名": "內容"}。Update 時只需給要改的欄',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['sheet'], operation: ['create'] } },
placeholder: 'name, phone, email',
description: '這張表要看哪幾欄,逗號分隔',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
displayOptions: { show: { resource: ['sheet'], operation: ['create'] } },
description: '這張 sheet 是做什麼的(選填)',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['search'] } },
description: '要找什麼',
},
{
displayName: 'Mode',
name: 'mode',
type: 'options',
options: [
{ name: 'Keyword', value: 'keyword' },
{ name: 'Semantic', value: 'semantic' },
],
default: 'keyword',
displayOptions: { show: { resource: ['search'] } },
description: 'Keyword=字面命中;Semantic=語意相近(實例沒開語意模組時會誠實降級成 keyword)',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: { show: { resource: ['search'] } },
options: [
{ displayName: 'Library', name: 'library', type: 'string', default: '' },
{ displayName: 'Source', name: 'source', type: 'string', default: '' },
{ displayName: 'Entry Type', name: 'entry_type', type: 'string', default: '' },
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('kbdbApi');
const baseUrl = String(credentials.baseUrl).replace(/\/$/, '');
for (let i = 0; i < items.length; i++) {
try {
const resource = this.getNodeParameter('resource', i) as string;
const operation = this.getNodeParameter('operation', i) as string;
const params: ActionParams = {};
if (resource === 'record') {
if (operation === 'append' || operation === 'getMany') {
params.sheet = this.getNodeParameter('sheet', i) as string;
}
if (operation === 'get' || operation === 'update') {
params.recordId = this.getNodeParameter('recordId', i) as string;
}
if (operation === 'append' || operation === 'update') {
const raw = this.getNodeParameter('values', i);
params.values = (typeof raw === 'string' ? JSON.parse(raw) : raw) as Record<string, string>;
}
} else if (resource === 'sheet') {
if (operation !== 'list') {
params.sheet = this.getNodeParameter('sheet', i) as string;
}
if (operation === 'create') {
params.slots = (this.getNodeParameter('fields', i) as string)
.split(',')
.map((s) => s.trim())
.filter(Boolean);
params.description = this.getNodeParameter('description', i, '') as string;
}
} else if (resource === 'search') {
params.query = this.getNodeParameter('query', i) as string;
params.mode = this.getNodeParameter('mode', i, 'keyword') as 'keyword' | 'semantic';
params.filters = this.getNodeParameter('filters', i, {}) as ActionParams['filters'];
}
const req = buildRequest(resource, operation, params);
const response = await this.helpers.httpRequestWithAuthentication.call(this, 'kbdbApi', {
method: req.method as IHttpRequestMethods,
url: `${baseUrl}${req.path}`,
qs: req.qs,
body: req.body,
json: true,
});
const asArray = Array.isArray(response) ? response : [response];
for (const entry of asArray) {
returnData.push({ json: entry as IDataObject, pairedItem: { item: i } });
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
continue;
}
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
}
}
return [returnData];
}
}