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>
This commit is contained in:
uncle6me-web
2026-08-15 19:33:55 +08:00
parent 576afa2364
commit 87f6de7c9f
9 changed files with 3504 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
/**
* 動作 → API 的映射測試=「kbdb-動作對照表」✅ 那半的機器版。
* 表若改(路由變動),這裡會先紅——防對照表漂移(表自己的「過期時會教錯」段)。
*/
import { describe, expect, it } from 'vitest';
import { buildRequest } from '../nodes/Kbdb/actions';
describe('對照表 ✅ 那半:每個動作恰好對到一支現行 API', () => {
it('Append record → POST /kbdb/records', () => {
expect(buildRequest('record', 'append', { sheet: 'contact', values: { name: '王小明' } })).toEqual({
method: 'POST',
path: '/kbdb/records',
body: { template: 'contact', values: { name: '王小明' } },
});
});
it('Get a record → GET /kbdb/records/:recordId', () => {
expect(buildRequest('record', 'get', { recordId: 'rec_1' })).toEqual({
method: 'GET',
path: '/kbdb/records/rec_1',
});
});
it('Get many records → GET /kbdb/records/by-template/:sheet', () => {
expect(buildRequest('record', 'getMany', { sheet: 'contact' })).toEqual({
method: 'GET',
path: '/kbdb/records/by-template/contact',
});
});
it('Update a record → PATCH /kbdb/records/:recordId(只帶要改的欄)', () => {
expect(buildRequest('record', 'update', { recordId: 'rec_1', values: { phone: '0912' } })).toEqual({
method: 'PATCH',
path: '/kbdb/records/rec_1',
body: { values: { phone: '0912' } },
});
});
it('Create a sheet → POST /kbdb/templatesname + slots[]', () => {
expect(buildRequest('sheet', 'create', { sheet: 'teacher_list', slots: ['name', 'hired_at'] })).toEqual({
method: 'POST',
path: '/kbdb/templates',
body: { name: 'teacher_list', slots: ['name', 'hired_at'] },
});
});
it('Get sheet schema → GET /kbdb/templates/:idOrName', () => {
expect(buildRequest('sheet', 'getSchema', { sheet: 'contact' })).toEqual({
method: 'GET',
path: '/kbdb/templates/contact',
});
});
it('List sheets → GET /kbdb/templates', () => {
expect(buildRequest('sheet', 'list', {})).toEqual({ method: 'GET', path: '/kbdb/templates' });
});
it('Search → GET /kbdb/searchq 必填、modefilters 透傳)', () => {
expect(
buildRequest('search', 'search', {
query: '牛肉麵',
mode: 'keyword',
filters: { library: 'general' },
}),
).toEqual({
method: 'GET',
path: '/kbdb/search',
qs: { q: '牛肉麵', mode: 'keyword', library: 'general' },
});
});
it('路徑參數會被 URL encode(中文 sheet 名不炸)', () => {
expect(buildRequest('record', 'getMany', { sheet: '通訊錄' }).path).toBe(
`/kbdb/records/by-template/${encodeURIComponent('通訊錄')}`,
);
});
});
describe('對照表 🔴/◐ 那半:沒有的能力要誠實丟錯,不拼裝(07-thin-shell §3.1', () => {
const missing: Array<[string, string]> = [
['sheet', 'addField'], // ◐→實測:PATCH /templates 是整組覆蓋且 proxy 未暴露 ⇒ 對外沒有這能力
['sheet', 'archive'], // 🔴 API 連 DELETE 都沒有
['record', 'archive'], // 🔴 只有 DELETE(真的刪掉),語意不符不掛上來
['record', 'link'], // 🔴 關係整組沒有 API
['record', 'addToSheet'], // 🔴 同上
['record', 'getLinked'], // 🔴 同上
['record', 'unlink'], // 🔴 同上
];
for (const [resource, operation] of missing) {
it(`${resource}.${operation} → 明確丟「沒有這個能力」`, () => {
expect(() => buildRequest(resource, operation, {})).toThrow(/沒有.*能力/);
});
}
});
describe('必要參數缺漏要在薄殼層就講清楚', () => {
it('append 缺 values → 錯誤訊息點名 values', () => {
expect(() => buildRequest('record', 'append', { sheet: 'contact' })).toThrow(/values/);
});
it('create sheet 缺欄位 → 錯誤訊息點名 fields', () => {
expect(() => buildRequest('sheet', 'create', { sheet: 'x' })).toThrow(/fields/);
});
it('create sheet 空欄位陣列 → 擋', () => {
expect(() => buildRequest('sheet', 'create', { sheet: 'x', slots: [] })).toThrow(/至少要有一欄/);
});
it('search 缺 query → 錯誤訊息點名 query', () => {
expect(() => buildRequest('search', 'search', {})).toThrow(/query/);
});
});