affine-mcp-server 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -6
- package/dist/index.js +7 -2
- package/dist/tools/docs.js +704 -169
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted or cloud). It exposes AFFiNE workspaces and documents to AI assistants over stdio (default) or HTTP (`/mcp`).
|
|
4
4
|
|
|
5
|
-
[](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
6
6
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
7
7
|
[](https://github.com/dawncr0w/affine-mcp-server/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
@@ -16,16 +16,16 @@ A Model Context Protocol (MCP) server that integrates with AFFiNE (self‑hosted
|
|
|
16
16
|
- Purpose: Manage AFFiNE workspaces and documents through MCP
|
|
17
17
|
- Transport: stdio (default) and optional HTTP (`/mcp`) for remote MCP deployments
|
|
18
18
|
- Auth: Token, Cookie, or Email/Password (priority order)
|
|
19
|
-
- Tools:
|
|
19
|
+
- Tools: 46 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.8.0: Added database cell read/write tools, fixed Kanban row title persistence, and added CLI version commands.
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- Workspace: create (with initial doc), read, update, delete
|
|
27
27
|
- Documents: list/get/read/publish/revoke + create/append/replace/delete + markdown import/export + tags (WebSocket‑based)
|
|
28
|
-
- Database workflows: create database blocks,
|
|
28
|
+
- Database workflows: create database blocks, add columns and rows, and read or update cell values via MCP tools
|
|
29
29
|
- Comments: full CRUD and resolve
|
|
30
30
|
- Version History: list
|
|
31
31
|
- Users & Tokens: current user, sign in, profile/settings, and personal access tokens
|
|
@@ -95,6 +95,7 @@ The MCP server will use these credentials automatically.
|
|
|
95
95
|
Other CLI commands:
|
|
96
96
|
- `affine-mcp status` — show current config and test connection
|
|
97
97
|
- `affine-mcp logout` — remove stored credentials
|
|
98
|
+
- `affine-mcp --version` / `-v` / `version` — print the installed CLI version and exit
|
|
98
99
|
|
|
99
100
|
### Environment variables
|
|
100
101
|
|
|
@@ -326,7 +327,10 @@ Endpoints currently available:
|
|
|
326
327
|
- `append_paragraph` – append a paragraph block (WebSocket)
|
|
327
328
|
- `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`data_view` currently falls back to database)
|
|
328
329
|
- `add_database_column` – add a column to a database block (`rich-text`, `select`, `multi-select`, `number`, `checkbox`, `link`, `date`)
|
|
329
|
-
- `add_database_row` – add a row to a database block with values mapped by column name/ID
|
|
330
|
+
- `add_database_row` – add a row to a database block with values mapped by column name/ID (`title` / `Title` updates the built-in row title)
|
|
331
|
+
- `read_database_cells` – read row titles plus decoded database cell values with optional row / column filters
|
|
332
|
+
- `update_database_cell` – update a single database cell or the built-in row title (`createOption` defaults to `true` for select fields)
|
|
333
|
+
- `update_database_row` – batch update multiple cells on a database row (`createOption` defaults to `true` for select fields)
|
|
330
334
|
- `append_markdown` – append markdown content to an existing document
|
|
331
335
|
- `replace_doc_with_markdown` – replace the main note content with markdown content
|
|
332
336
|
- `delete_doc` – delete a document (WebSocket)
|
|
@@ -374,7 +378,7 @@ npm run pack:check
|
|
|
374
378
|
- CI validates that `registerTool(...)` declarations match the manifest exactly.
|
|
375
379
|
- For full tool-surface verification, run `npm run test:comprehensive`.
|
|
376
380
|
- For full environment verification, run `npm run test:e2e` (Docker + MCP + Playwright).
|
|
377
|
-
- Additional focused runners: `npm run test:db-create`, `npm run test:bearer`, `npm run test:playwright`.
|
|
381
|
+
- Additional focused runners: `npm run test:db-create`, `npm run test:db-cells`, `npm run test:bearer`, `npm run test:cli-version`, `npm run test:playwright`.
|
|
378
382
|
|
|
379
383
|
## Troubleshooting
|
|
380
384
|
|
|
@@ -409,6 +413,25 @@ Workspace visibility
|
|
|
409
413
|
|
|
410
414
|
## Version History
|
|
411
415
|
|
|
416
|
+
### 1.8.0 (2026‑03‑09)
|
|
417
|
+
- Added `read_database_cells`, `update_database_cell`, and `update_database_row` for database cell-level workflows
|
|
418
|
+
- Fixed `add_database_row` so `title` / `Title` persists to the Kanban card header text
|
|
419
|
+
- Added CLI version commands: `affine-mcp --version`, `affine-mcp -v`, and `affine-mcp version`
|
|
420
|
+
- Added focused regression runners for database cells and CLI version support
|
|
421
|
+
- Verified release gates with `npm run ci`, `npm run test:cli-version`, and live `npm run test:db-cells`
|
|
422
|
+
|
|
423
|
+
### 1.7.2 (2026‑03‑04)
|
|
424
|
+
- Fixed MCP tag persistence to use AFFiNE canonical tag option IDs so tags are visible in Web/App UI
|
|
425
|
+
- Added backward-compatible tag normalization for legacy string tag entries
|
|
426
|
+
- Added tag visibility regression coverage (`tests/test-tag-visibility.mjs`, `tests/playwright/verify-tag-visibility.pw.ts`)
|
|
427
|
+
- Hardened E2E credential bootstrap with configurable health retries, retry attempts, and Docker diagnostics on failure
|
|
428
|
+
- Verified CI gates (`validate`, `e2e`) for PR #46 and local `npm run ci`
|
|
429
|
+
|
|
430
|
+
### 1.7.1 (2026‑03‑03)
|
|
431
|
+
- Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling)
|
|
432
|
+
- Fixed callout text rendering parity in AFFiNE UI for MCP-created blocks
|
|
433
|
+
- Added regression assertions for visibility-sensitive document creation paths
|
|
434
|
+
|
|
412
435
|
### 1.7.0 (2026‑02‑27)
|
|
413
436
|
- Added Streamable HTTP MCP support on `/mcp` for remote hosting while keeping legacy SSE compatibility paths (`/sse`, `/messages`)
|
|
414
437
|
- Added HTTP deployment controls: `AFFINE_MCP_HTTP_HOST`, `AFFINE_MCP_HTTP_TOKEN`, `AFFINE_MCP_HTTP_ALLOWED_ORIGINS`, `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS`
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,13 @@ import { loginWithPassword } from "./auth.js";
|
|
|
15
15
|
import { registerAuthTools } from "./tools/auth.js";
|
|
16
16
|
import { runCli } from "./cli.js";
|
|
17
17
|
import { startHttpMcpServer } from "./sse.js";
|
|
18
|
-
// CLI
|
|
19
|
-
const
|
|
18
|
+
// CLI commands: affine-mcp login|status|logout|version
|
|
19
|
+
const rawArgs = process.argv.slice(2);
|
|
20
|
+
const subcommand = rawArgs[0] === "--" ? rawArgs[1] : rawArgs[0];
|
|
21
|
+
if (subcommand === "--version" || subcommand === "-v" || subcommand === "version") {
|
|
22
|
+
console.log(VERSION);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
20
25
|
if (subcommand && await runCli(subcommand)) {
|
|
21
26
|
process.exit(0);
|
|
22
27
|
}
|
package/dist/tools/docs.js
CHANGED
|
@@ -118,6 +118,11 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
118
118
|
}
|
|
119
119
|
return normalized;
|
|
120
120
|
}
|
|
121
|
+
const TAG_OPTION_COLORS = [
|
|
122
|
+
"var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
|
|
123
|
+
"var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
|
|
124
|
+
"var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
|
|
125
|
+
];
|
|
121
126
|
function getStringArray(value) {
|
|
122
127
|
if (!(value instanceof Y.Array)) {
|
|
123
128
|
return [];
|
|
@@ -146,23 +151,235 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
146
151
|
target.set(key, next);
|
|
147
152
|
return next;
|
|
148
153
|
}
|
|
149
|
-
function
|
|
150
|
-
const
|
|
151
|
-
|
|
154
|
+
function getYMap(target, key) {
|
|
155
|
+
const value = target.get(key);
|
|
156
|
+
if (!(value instanceof Y.Map)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
152
160
|
}
|
|
153
|
-
function
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
function ensureYMap(target, key) {
|
|
162
|
+
const current = getYMap(target, key);
|
|
163
|
+
if (current) {
|
|
164
|
+
return current;
|
|
165
|
+
}
|
|
166
|
+
const next = new Y.Map();
|
|
167
|
+
target.set(key, next);
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
function getWorkspaceTagOptionsArray(meta) {
|
|
171
|
+
const properties = getYMap(meta, "properties");
|
|
172
|
+
if (!properties) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const tags = getYMap(properties, "tags");
|
|
176
|
+
if (!tags) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const options = tags.get("options");
|
|
180
|
+
if (!(options instanceof Y.Array)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return options;
|
|
184
|
+
}
|
|
185
|
+
function ensureWorkspaceTagOptionsArray(meta) {
|
|
186
|
+
const properties = ensureYMap(meta, "properties");
|
|
187
|
+
const tags = ensureYMap(properties, "tags");
|
|
188
|
+
const existing = tags.get("options");
|
|
189
|
+
if (existing instanceof Y.Array) {
|
|
190
|
+
return existing;
|
|
191
|
+
}
|
|
192
|
+
const next = new Y.Array();
|
|
193
|
+
tags.set("options", next);
|
|
194
|
+
return next;
|
|
195
|
+
}
|
|
196
|
+
function asNumberOrNull(value) {
|
|
197
|
+
return typeof value === "number" ? value : null;
|
|
198
|
+
}
|
|
199
|
+
function parseWorkspaceTagOption(raw) {
|
|
200
|
+
let id;
|
|
201
|
+
let value;
|
|
202
|
+
let color;
|
|
203
|
+
let createDate;
|
|
204
|
+
let updateDate;
|
|
205
|
+
if (raw instanceof Y.Map) {
|
|
206
|
+
id = raw.get("id");
|
|
207
|
+
value = raw.get("value");
|
|
208
|
+
color = raw.get("color");
|
|
209
|
+
createDate = raw.get("createDate");
|
|
210
|
+
updateDate = raw.get("updateDate");
|
|
211
|
+
}
|
|
212
|
+
else if (raw && typeof raw === "object") {
|
|
213
|
+
id = raw.id;
|
|
214
|
+
value = raw.value;
|
|
215
|
+
color = raw.color;
|
|
216
|
+
createDate = raw.createDate;
|
|
217
|
+
updateDate = raw.updateDate;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
id,
|
|
230
|
+
value,
|
|
231
|
+
color: typeof color === "string" && color.trim().length > 0 ? color : TAG_OPTION_COLORS[0],
|
|
232
|
+
createDate: asNumberOrNull(createDate),
|
|
233
|
+
updateDate: asNumberOrNull(updateDate),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function getWorkspaceTagOptions(meta) {
|
|
237
|
+
const options = getWorkspaceTagOptionsArray(meta);
|
|
238
|
+
if (!options) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const parsed = [];
|
|
242
|
+
options.forEach((raw) => {
|
|
243
|
+
const option = parseWorkspaceTagOption(raw);
|
|
244
|
+
if (option) {
|
|
245
|
+
parsed.push(option);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
250
|
+
function getWorkspaceTagOptionMaps(meta) {
|
|
251
|
+
const options = getWorkspaceTagOptions(meta);
|
|
252
|
+
const byId = new Map();
|
|
253
|
+
const byValueLower = new Map();
|
|
254
|
+
for (const option of options) {
|
|
255
|
+
if (!byId.has(option.id)) {
|
|
256
|
+
byId.set(option.id, option);
|
|
257
|
+
}
|
|
258
|
+
const key = option.value.toLocaleLowerCase();
|
|
259
|
+
if (!byValueLower.has(key)) {
|
|
260
|
+
byValueLower.set(key, option);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { options, byId, byValueLower };
|
|
264
|
+
}
|
|
265
|
+
function resolveTagLabels(tagEntries, byId) {
|
|
266
|
+
const deduped = new Set();
|
|
267
|
+
const resolved = [];
|
|
268
|
+
for (const entry of tagEntries) {
|
|
269
|
+
const raw = entry.trim();
|
|
270
|
+
if (!raw) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const option = byId.get(raw);
|
|
274
|
+
const label = (option ? option.value : raw).trim();
|
|
275
|
+
if (!label) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const dedupeKey = label.toLocaleLowerCase();
|
|
279
|
+
if (deduped.has(dedupeKey)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
deduped.add(dedupeKey);
|
|
283
|
+
resolved.push(label);
|
|
284
|
+
}
|
|
285
|
+
return resolved;
|
|
286
|
+
}
|
|
287
|
+
function ensureWorkspaceTagOption(meta, tag) {
|
|
288
|
+
const normalizedTag = normalizeTag(tag);
|
|
289
|
+
const maps = getWorkspaceTagOptionMaps(meta);
|
|
290
|
+
const existing = maps.byValueLower.get(normalizedTag.toLocaleLowerCase());
|
|
291
|
+
if (existing) {
|
|
292
|
+
return { option: existing, created: false };
|
|
293
|
+
}
|
|
294
|
+
const optionsArray = ensureWorkspaceTagOptionsArray(meta);
|
|
295
|
+
const color = TAG_OPTION_COLORS[maps.options.length % TAG_OPTION_COLORS.length];
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
const option = {
|
|
298
|
+
id: generateId(),
|
|
299
|
+
value: normalizedTag,
|
|
300
|
+
color,
|
|
301
|
+
createDate: now,
|
|
302
|
+
updateDate: now,
|
|
303
|
+
};
|
|
304
|
+
const optionMap = new Y.Map();
|
|
305
|
+
optionMap.set("id", option.id);
|
|
306
|
+
optionMap.set("value", option.value);
|
|
307
|
+
optionMap.set("color", option.color);
|
|
308
|
+
optionMap.set("createDate", now);
|
|
309
|
+
optionMap.set("updateDate", now);
|
|
310
|
+
optionsArray.push([optionMap]);
|
|
311
|
+
return { option, created: true };
|
|
312
|
+
}
|
|
313
|
+
function collectMatchingTagIndexes(tags, requestedTag, option, ignoreCase) {
|
|
314
|
+
const normalizedRequested = ignoreCase ? requestedTag.toLocaleLowerCase() : requestedTag;
|
|
315
|
+
const normalizedOptionId = option
|
|
316
|
+
? (ignoreCase ? option.id.toLocaleLowerCase() : option.id)
|
|
317
|
+
: null;
|
|
318
|
+
const normalizedOptionValue = option
|
|
319
|
+
? (ignoreCase ? option.value.toLocaleLowerCase() : option.value)
|
|
320
|
+
: null;
|
|
321
|
+
const indexes = [];
|
|
322
|
+
tags.forEach((entry, index) => {
|
|
323
|
+
if (typeof entry !== "string") {
|
|
158
324
|
return;
|
|
159
325
|
}
|
|
160
326
|
const current = ignoreCase ? entry.toLocaleLowerCase() : entry;
|
|
161
|
-
if (current ===
|
|
162
|
-
|
|
327
|
+
if (current === normalizedRequested ||
|
|
328
|
+
(normalizedOptionId && current === normalizedOptionId) ||
|
|
329
|
+
(normalizedOptionValue && current === normalizedOptionValue)) {
|
|
330
|
+
indexes.push(index);
|
|
163
331
|
}
|
|
164
332
|
});
|
|
165
|
-
return
|
|
333
|
+
return indexes;
|
|
334
|
+
}
|
|
335
|
+
function deleteArrayIndexes(arr, indexes) {
|
|
336
|
+
if (indexes.length === 0) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const sorted = [...indexes].sort((a, b) => b - a);
|
|
340
|
+
for (const index of sorted) {
|
|
341
|
+
arr.delete(index, 1);
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
function syncTagArrayToOption(tags, requestedTag, option) {
|
|
346
|
+
const optionId = option.id.toLocaleLowerCase();
|
|
347
|
+
const optionValue = option.value.toLocaleLowerCase();
|
|
348
|
+
const requested = requestedTag.toLocaleLowerCase();
|
|
349
|
+
let existed = false;
|
|
350
|
+
let hasCanonicalId = false;
|
|
351
|
+
const removeIndexes = [];
|
|
352
|
+
tags.forEach((entry, index) => {
|
|
353
|
+
if (typeof entry !== "string") {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const current = entry.toLocaleLowerCase();
|
|
357
|
+
const matched = current === optionId || current === optionValue || current === requested;
|
|
358
|
+
if (!matched) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
existed = true;
|
|
362
|
+
if (current === optionId) {
|
|
363
|
+
if (hasCanonicalId) {
|
|
364
|
+
removeIndexes.push(index);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
hasCanonicalId = true;
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
removeIndexes.push(index);
|
|
372
|
+
});
|
|
373
|
+
let changed = deleteArrayIndexes(tags, removeIndexes);
|
|
374
|
+
if (!hasCanonicalId) {
|
|
375
|
+
tags.push([option.id]);
|
|
376
|
+
changed = true;
|
|
377
|
+
}
|
|
378
|
+
return { existed, changed };
|
|
379
|
+
}
|
|
380
|
+
function hasTag(tagValues, tag, ignoreCase) {
|
|
381
|
+
const normalizedTag = ignoreCase ? tag.toLocaleLowerCase() : tag;
|
|
382
|
+
return tagValues.some((entry) => (ignoreCase ? entry.toLocaleLowerCase() : entry) === normalizedTag);
|
|
166
383
|
}
|
|
167
384
|
function getWorkspacePageEntries(meta) {
|
|
168
385
|
const pages = meta.get("pages");
|
|
@@ -1373,9 +1590,9 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1373
1590
|
}
|
|
1374
1591
|
return tableData;
|
|
1375
1592
|
}
|
|
1376
|
-
function collectDocForMarkdown(doc) {
|
|
1593
|
+
function collectDocForMarkdown(doc, tagOptionsById = new Map()) {
|
|
1377
1594
|
const meta = doc.getMap("meta");
|
|
1378
|
-
const tags = getStringArray(getTagArray(meta));
|
|
1595
|
+
const tags = resolveTagLabels(getStringArray(getTagArray(meta)), tagOptionsById);
|
|
1379
1596
|
const blocks = doc.getMap("blocks");
|
|
1380
1597
|
const pageId = findBlockIdByFlavour(blocks, "affine:page");
|
|
1381
1598
|
const noteId = findBlockIdByFlavour(blocks, "affine:note");
|
|
@@ -1640,8 +1857,10 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1640
1857
|
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
|
|
1641
1858
|
const meta = wsDoc.getMap("meta");
|
|
1642
1859
|
const pages = getWorkspacePageEntries(meta);
|
|
1860
|
+
const { byId } = getWorkspaceTagOptionMaps(meta);
|
|
1643
1861
|
for (const page of pages) {
|
|
1644
|
-
|
|
1862
|
+
const tagEntries = getStringArray(page.tagsArray);
|
|
1863
|
+
tagsByDocId.set(page.id, resolveTagLabels(tagEntries, byId));
|
|
1645
1864
|
}
|
|
1646
1865
|
}
|
|
1647
1866
|
}
|
|
@@ -1700,9 +1919,10 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1700
1919
|
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
|
|
1701
1920
|
const meta = wsDoc.getMap("meta");
|
|
1702
1921
|
const pages = getWorkspacePageEntries(meta);
|
|
1922
|
+
const { options, byId } = getWorkspaceTagOptionMaps(meta);
|
|
1703
1923
|
const tagCounts = new Map();
|
|
1704
|
-
for (const
|
|
1705
|
-
const normalized =
|
|
1924
|
+
for (const option of options) {
|
|
1925
|
+
const normalized = option.value.trim();
|
|
1706
1926
|
if (!normalized || tagCounts.has(normalized)) {
|
|
1707
1927
|
continue;
|
|
1708
1928
|
}
|
|
@@ -1710,7 +1930,8 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1710
1930
|
}
|
|
1711
1931
|
for (const page of pages) {
|
|
1712
1932
|
const uniqueTags = new Set();
|
|
1713
|
-
|
|
1933
|
+
const resolved = resolveTagLabels(getStringArray(page.tagsArray), byId);
|
|
1934
|
+
for (const tag of resolved) {
|
|
1714
1935
|
const normalized = tag.trim();
|
|
1715
1936
|
if (!normalized) {
|
|
1716
1937
|
continue;
|
|
@@ -1761,18 +1982,22 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1761
1982
|
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
|
|
1762
1983
|
const meta = wsDoc.getMap("meta");
|
|
1763
1984
|
const pages = getWorkspacePageEntries(meta);
|
|
1985
|
+
const { byId } = getWorkspaceTagOptionMaps(meta);
|
|
1764
1986
|
const docs = pages
|
|
1765
1987
|
.map((page) => {
|
|
1766
|
-
const
|
|
1988
|
+
const rawTags = getStringArray(page.tagsArray);
|
|
1989
|
+
const tags = resolveTagLabels(rawTags, byId);
|
|
1767
1990
|
return {
|
|
1768
1991
|
id: page.id,
|
|
1769
1992
|
title: page.title,
|
|
1770
1993
|
createDate: page.createDate,
|
|
1771
1994
|
updatedDate: page.updatedDate,
|
|
1772
1995
|
tags,
|
|
1996
|
+
rawTags,
|
|
1773
1997
|
};
|
|
1774
1998
|
})
|
|
1775
|
-
.filter((page) => hasTag(page.tags, tag, ignoreCase))
|
|
1999
|
+
.filter((page) => hasTag(page.tags, tag, ignoreCase) || hasTag(page.rawTags, tag, ignoreCase))
|
|
2000
|
+
.map(({ rawTags: _rawTags, ...page }) => page);
|
|
1776
2001
|
return text({
|
|
1777
2002
|
workspaceId,
|
|
1778
2003
|
tag,
|
|
@@ -1813,11 +2038,10 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1813
2038
|
Y.applyUpdate(wsDoc, Buffer.from(snapshot.missing, "base64"));
|
|
1814
2039
|
const prevSV = Y.encodeStateVector(wsDoc);
|
|
1815
2040
|
const meta = wsDoc.getMap("meta");
|
|
1816
|
-
const
|
|
1817
|
-
if (
|
|
2041
|
+
const { created } = ensureWorkspaceTagOption(meta, tag);
|
|
2042
|
+
if (!created) {
|
|
1818
2043
|
return text({ workspaceId, tag, created: false });
|
|
1819
2044
|
}
|
|
1820
|
-
registry.push([tag]);
|
|
1821
2045
|
const delta = Y.encodeStateAsUpdate(wsDoc, prevSV);
|
|
1822
2046
|
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(delta).toString("base64"));
|
|
1823
2047
|
return text({ workspaceId, tag, created: true });
|
|
@@ -1857,17 +2081,11 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1857
2081
|
if (!page) {
|
|
1858
2082
|
throw new Error(`docId ${parsed.docId} is not present in workspace ${workspaceId}`);
|
|
1859
2083
|
}
|
|
2084
|
+
const { option, created: optionCreated } = ensureWorkspaceTagOption(wsMeta, tag);
|
|
1860
2085
|
const pageTags = ensureTagArray(page.entry);
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
}
|
|
1865
|
-
const registry = ensureTagArray(wsMeta);
|
|
1866
|
-
const existedInRegistry = findTagIndex(registry, tag, true) >= 0;
|
|
1867
|
-
if (!existedInRegistry) {
|
|
1868
|
-
registry.push([tag]);
|
|
1869
|
-
}
|
|
1870
|
-
if (!existedInDoc || !existedInRegistry) {
|
|
2086
|
+
const pageSync = syncTagArrayToOption(pageTags, tag, option);
|
|
2087
|
+
const wsChanged = optionCreated || pageSync.changed;
|
|
2088
|
+
if (wsChanged) {
|
|
1871
2089
|
const wsDelta = Y.encodeStateAsUpdate(wsDoc, wsPrevSV);
|
|
1872
2090
|
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(wsDelta).toString("base64"));
|
|
1873
2091
|
}
|
|
@@ -1883,20 +2101,20 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1883
2101
|
const docPrevSV = Y.encodeStateVector(doc);
|
|
1884
2102
|
const docMeta = doc.getMap("meta");
|
|
1885
2103
|
const docTags = ensureTagArray(docMeta);
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1888
|
-
docTags.push([tag]);
|
|
2104
|
+
const docSync = syncTagArrayToOption(docTags, tag, option);
|
|
2105
|
+
if (docSync.changed) {
|
|
1889
2106
|
const docDelta = Y.encodeStateAsUpdate(doc, docPrevSV);
|
|
1890
2107
|
await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(docDelta).toString("base64"));
|
|
1891
2108
|
}
|
|
1892
2109
|
docMetaSynced = true;
|
|
1893
2110
|
}
|
|
2111
|
+
const { byId } = getWorkspaceTagOptionMaps(wsMeta);
|
|
1894
2112
|
return text({
|
|
1895
2113
|
workspaceId,
|
|
1896
2114
|
docId: parsed.docId,
|
|
1897
2115
|
tag,
|
|
1898
|
-
added: !
|
|
1899
|
-
tags: getStringArray(pageTags),
|
|
2116
|
+
added: !pageSync.existed,
|
|
2117
|
+
tags: resolveTagLabels(getStringArray(pageTags), byId),
|
|
1900
2118
|
docMetaSynced,
|
|
1901
2119
|
warning,
|
|
1902
2120
|
});
|
|
@@ -1937,10 +2155,11 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1937
2155
|
if (!page) {
|
|
1938
2156
|
throw new Error(`docId ${parsed.docId} is not present in workspace ${workspaceId}`);
|
|
1939
2157
|
}
|
|
2158
|
+
const option = getWorkspaceTagOptionMaps(wsMeta).byValueLower.get(tag.toLocaleLowerCase()) || null;
|
|
1940
2159
|
const pageTags = ensureTagArray(page.entry);
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
2160
|
+
const pageTagIndexes = collectMatchingTagIndexes(pageTags, tag, option, true);
|
|
2161
|
+
const pageRemoved = deleteArrayIndexes(pageTags, pageTagIndexes);
|
|
2162
|
+
if (pageRemoved) {
|
|
1944
2163
|
const wsDelta = Y.encodeStateAsUpdate(wsDoc, wsPrevSV);
|
|
1945
2164
|
await pushDocUpdate(socket, workspaceId, workspaceId, Buffer.from(wsDelta).toString("base64"));
|
|
1946
2165
|
}
|
|
@@ -1956,20 +2175,20 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1956
2175
|
const docPrevSV = Y.encodeStateVector(doc);
|
|
1957
2176
|
const docMeta = doc.getMap("meta");
|
|
1958
2177
|
const docTags = ensureTagArray(docMeta);
|
|
1959
|
-
const
|
|
1960
|
-
if (
|
|
1961
|
-
docTags.delete(docTagIndex, 1);
|
|
2178
|
+
const docTagIndexes = collectMatchingTagIndexes(docTags, tag, option, true);
|
|
2179
|
+
if (deleteArrayIndexes(docTags, docTagIndexes)) {
|
|
1962
2180
|
const docDelta = Y.encodeStateAsUpdate(doc, docPrevSV);
|
|
1963
2181
|
await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(docDelta).toString("base64"));
|
|
1964
2182
|
}
|
|
1965
2183
|
docMetaSynced = true;
|
|
1966
2184
|
}
|
|
2185
|
+
const { byId } = getWorkspaceTagOptionMaps(wsMeta);
|
|
1967
2186
|
return text({
|
|
1968
2187
|
workspaceId,
|
|
1969
2188
|
docId: parsed.docId,
|
|
1970
2189
|
tag,
|
|
1971
|
-
removed:
|
|
1972
|
-
tags: getStringArray(pageTags),
|
|
2190
|
+
removed: pageRemoved,
|
|
2191
|
+
tags: resolveTagLabels(getStringArray(pageTags), byId),
|
|
1973
2192
|
docMetaSynced,
|
|
1974
2193
|
warning,
|
|
1975
2194
|
});
|
|
@@ -2014,6 +2233,13 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2014
2233
|
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
2015
2234
|
try {
|
|
2016
2235
|
await joinWorkspace(socket, workspaceId);
|
|
2236
|
+
let tagOptionsById = new Map();
|
|
2237
|
+
const workspaceSnapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
2238
|
+
if (workspaceSnapshot.missing) {
|
|
2239
|
+
const workspaceDoc = new Y.Doc();
|
|
2240
|
+
Y.applyUpdate(workspaceDoc, Buffer.from(workspaceSnapshot.missing, "base64"));
|
|
2241
|
+
tagOptionsById = getWorkspaceTagOptionMaps(workspaceDoc.getMap("meta")).byId;
|
|
2242
|
+
}
|
|
2017
2243
|
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
2018
2244
|
if (!snapshot.missing) {
|
|
2019
2245
|
return text({
|
|
@@ -2029,7 +2255,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2029
2255
|
const doc = new Y.Doc();
|
|
2030
2256
|
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
2031
2257
|
const meta = doc.getMap("meta");
|
|
2032
|
-
const tags = getStringArray(getTagArray(meta));
|
|
2258
|
+
const tags = resolveTagLabels(getStringArray(getTagArray(meta)), tagOptionsById);
|
|
2033
2259
|
const blocks = doc.getMap("blocks");
|
|
2034
2260
|
const pageId = findBlockIdByFlavour(blocks, "affine:page");
|
|
2035
2261
|
const noteId = findBlockIdByFlavour(blocks, "affine:note");
|
|
@@ -2238,6 +2464,13 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2238
2464
|
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
2239
2465
|
try {
|
|
2240
2466
|
await joinWorkspace(socket, workspaceId);
|
|
2467
|
+
let tagOptionsById = new Map();
|
|
2468
|
+
const workspaceSnapshot = await loadDoc(socket, workspaceId, workspaceId);
|
|
2469
|
+
if (workspaceSnapshot.missing) {
|
|
2470
|
+
const wsDoc = new Y.Doc();
|
|
2471
|
+
Y.applyUpdate(wsDoc, Buffer.from(workspaceSnapshot.missing, "base64"));
|
|
2472
|
+
tagOptionsById = getWorkspaceTagOptionMaps(wsDoc.getMap("meta")).byId;
|
|
2473
|
+
}
|
|
2241
2474
|
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
2242
2475
|
if (!snapshot.missing) {
|
|
2243
2476
|
return text({
|
|
@@ -2256,7 +2489,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2256
2489
|
}
|
|
2257
2490
|
const doc = new Y.Doc();
|
|
2258
2491
|
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
2259
|
-
const collected = collectDocForMarkdown(doc);
|
|
2492
|
+
const collected = collectDocForMarkdown(doc, tagOptionsById);
|
|
2260
2493
|
const rendered = renderBlocksToMarkdown({
|
|
2261
2494
|
rootBlockIds: collected.rootBlockIds,
|
|
2262
2495
|
blocksById: collected.blocksById,
|
|
@@ -2490,7 +2723,6 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2490
2723
|
description: 'Delete a document and remove from workspace list',
|
|
2491
2724
|
inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
|
|
2492
2725
|
}, deleteDocHandler);
|
|
2493
|
-
// ── helpers for database select columns ──
|
|
2494
2726
|
/** Read column definitions including select options from a database block */
|
|
2495
2727
|
function readColumnDefs(dbBlock) {
|
|
2496
2728
|
const columnsRaw = dbBlock.get("prop:columns");
|
|
@@ -2528,17 +2760,111 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2528
2760
|
});
|
|
2529
2761
|
return defs;
|
|
2530
2762
|
}
|
|
2763
|
+
function isTitleAliasKey(value) {
|
|
2764
|
+
return value.trim().toLowerCase() === "title";
|
|
2765
|
+
}
|
|
2766
|
+
function buildDatabaseColumnLookup(columnDefs) {
|
|
2767
|
+
const colById = new Map();
|
|
2768
|
+
const colByName = new Map();
|
|
2769
|
+
const colByNameLower = new Map();
|
|
2770
|
+
let titleCol = null;
|
|
2771
|
+
for (const col of columnDefs) {
|
|
2772
|
+
colById.set(col.id, col);
|
|
2773
|
+
if (col.name) {
|
|
2774
|
+
colByName.set(col.name, col);
|
|
2775
|
+
colByNameLower.set(col.name.trim().toLowerCase(), col);
|
|
2776
|
+
}
|
|
2777
|
+
if (!titleCol && col.type === "title") {
|
|
2778
|
+
titleCol = col;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return { columnDefs, colById, colByName, colByNameLower, titleCol };
|
|
2782
|
+
}
|
|
2783
|
+
function findDatabaseColumn(key, lookup) {
|
|
2784
|
+
return lookup.colByName.get(key)
|
|
2785
|
+
|| lookup.colById.get(key)
|
|
2786
|
+
|| lookup.colByNameLower.get(key.trim().toLowerCase())
|
|
2787
|
+
|| null;
|
|
2788
|
+
}
|
|
2789
|
+
function availableDatabaseColumns(lookup) {
|
|
2790
|
+
return ["title", ...lookup.columnDefs.map(col => col.name || col.id)].join(", ");
|
|
2791
|
+
}
|
|
2792
|
+
function getDatabaseRowIds(dbBlock) {
|
|
2793
|
+
return childIdsFrom(dbBlock.get("sys:children"));
|
|
2794
|
+
}
|
|
2795
|
+
function readDatabaseRowTitle(rowBlock) {
|
|
2796
|
+
return asText(rowBlock.get("prop:text"));
|
|
2797
|
+
}
|
|
2798
|
+
function resolveDatabaseTitleValue(cells, lookup) {
|
|
2799
|
+
if (lookup.titleCol) {
|
|
2800
|
+
const value = cells[lookup.titleCol.name] ?? cells[lookup.titleCol.id];
|
|
2801
|
+
if (value !== undefined) {
|
|
2802
|
+
return String(value ?? "");
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
for (const [key, value] of Object.entries(cells)) {
|
|
2806
|
+
if (isTitleAliasKey(key)) {
|
|
2807
|
+
return String(value ?? "");
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
const namedTitleColumn = lookup.colByNameLower.get("title");
|
|
2811
|
+
if (namedTitleColumn) {
|
|
2812
|
+
const value = cells[namedTitleColumn.name] ?? cells[namedTitleColumn.id];
|
|
2813
|
+
if (value !== undefined) {
|
|
2814
|
+
return String(value ?? "");
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return "";
|
|
2818
|
+
}
|
|
2819
|
+
function ensureDatabaseRowCells(cellsMap, rowBlockId) {
|
|
2820
|
+
const existing = cellsMap.get(rowBlockId);
|
|
2821
|
+
if (existing instanceof Y.Map) {
|
|
2822
|
+
return existing;
|
|
2823
|
+
}
|
|
2824
|
+
const rowCells = new Y.Map();
|
|
2825
|
+
cellsMap.set(rowBlockId, rowCells);
|
|
2826
|
+
return rowCells;
|
|
2827
|
+
}
|
|
2828
|
+
function getDatabaseRowBlock(blocks, databaseBlockId, rowBlockId) {
|
|
2829
|
+
const rowBlock = findBlockById(blocks, rowBlockId);
|
|
2830
|
+
if (!rowBlock) {
|
|
2831
|
+
throw new Error(`Row block '${rowBlockId}' not found`);
|
|
2832
|
+
}
|
|
2833
|
+
if (rowBlock.get("sys:parent") !== databaseBlockId) {
|
|
2834
|
+
throw new Error(`Row block '${rowBlockId}' does not belong to database '${databaseBlockId}'`);
|
|
2835
|
+
}
|
|
2836
|
+
if (rowBlock.get("sys:flavour") !== "affine:paragraph") {
|
|
2837
|
+
throw new Error(`Row block '${rowBlockId}' is not a database row paragraph`);
|
|
2838
|
+
}
|
|
2839
|
+
return rowBlock;
|
|
2840
|
+
}
|
|
2841
|
+
function databaseArrayValues(value) {
|
|
2842
|
+
if (value instanceof Y.Array) {
|
|
2843
|
+
const entries = [];
|
|
2844
|
+
value.forEach(entry => {
|
|
2845
|
+
entries.push(entry);
|
|
2846
|
+
});
|
|
2847
|
+
return entries;
|
|
2848
|
+
}
|
|
2849
|
+
if (Array.isArray(value)) {
|
|
2850
|
+
return value;
|
|
2851
|
+
}
|
|
2852
|
+
return [];
|
|
2853
|
+
}
|
|
2531
2854
|
const SELECT_COLORS = [
|
|
2532
2855
|
"var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
|
|
2533
2856
|
"var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
|
|
2534
2857
|
"var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
|
|
2535
2858
|
];
|
|
2536
2859
|
/** Find or create a select option for a column, mutating the column's data in place */
|
|
2537
|
-
function resolveSelectOptionId(col, valueText) {
|
|
2860
|
+
function resolveSelectOptionId(col, valueText, createOption = true) {
|
|
2538
2861
|
// Try exact match first
|
|
2539
2862
|
const existing = col.options.find(o => o.value === valueText);
|
|
2540
2863
|
if (existing)
|
|
2541
2864
|
return existing.id;
|
|
2865
|
+
if (!createOption) {
|
|
2866
|
+
throw new Error(`Column "${col.name}": option "${valueText}" not found`);
|
|
2867
|
+
}
|
|
2542
2868
|
// Create new option
|
|
2543
2869
|
const newId = generateId();
|
|
2544
2870
|
const colorIdx = col.options.length % SELECT_COLORS.length;
|
|
@@ -2565,43 +2891,172 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2565
2891
|
}
|
|
2566
2892
|
return newId;
|
|
2567
2893
|
}
|
|
2894
|
+
function decodeDatabaseCellValue(col, cellEntry) {
|
|
2895
|
+
const rawValue = cellEntry instanceof Y.Map ? cellEntry.get("value") : cellEntry?.value;
|
|
2896
|
+
const base = {
|
|
2897
|
+
columnId: col.id,
|
|
2898
|
+
type: col.type,
|
|
2899
|
+
};
|
|
2900
|
+
switch (col.type) {
|
|
2901
|
+
case "rich-text":
|
|
2902
|
+
case "title":
|
|
2903
|
+
return { ...base, value: richTextValueToString(rawValue) || null };
|
|
2904
|
+
case "select": {
|
|
2905
|
+
const optionId = asStringOrNull(rawValue);
|
|
2906
|
+
const option = col.options.find(entry => entry.id === optionId) || null;
|
|
2907
|
+
return {
|
|
2908
|
+
...base,
|
|
2909
|
+
value: option?.value ?? optionId ?? null,
|
|
2910
|
+
optionId: optionId ?? null,
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
case "multi-select": {
|
|
2914
|
+
const optionIds = databaseArrayValues(rawValue).map(entry => String(entry));
|
|
2915
|
+
const values = optionIds.map(optionId => col.options.find(entry => entry.id === optionId)?.value ?? optionId);
|
|
2916
|
+
return {
|
|
2917
|
+
...base,
|
|
2918
|
+
value: values,
|
|
2919
|
+
optionIds,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
case "number": {
|
|
2923
|
+
const numericValue = typeof rawValue === "number" ? rawValue : Number(rawValue);
|
|
2924
|
+
return {
|
|
2925
|
+
...base,
|
|
2926
|
+
value: Number.isFinite(numericValue) ? numericValue : null,
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
case "checkbox":
|
|
2930
|
+
return { ...base, value: typeof rawValue === "boolean" ? rawValue : !!rawValue };
|
|
2931
|
+
case "date": {
|
|
2932
|
+
const numericValue = typeof rawValue === "number" ? rawValue : Number(rawValue);
|
|
2933
|
+
return {
|
|
2934
|
+
...base,
|
|
2935
|
+
value: Number.isFinite(numericValue) ? numericValue : null,
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
case "link":
|
|
2939
|
+
return { ...base, value: rawValue == null ? null : String(rawValue) };
|
|
2940
|
+
default:
|
|
2941
|
+
return {
|
|
2942
|
+
...base,
|
|
2943
|
+
value: typeof rawValue === "string" || rawValue instanceof Y.Text || Array.isArray(rawValue)
|
|
2944
|
+
? richTextValueToString(rawValue)
|
|
2945
|
+
: rawValue ?? null,
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function writeDatabaseCellValue(rowCells, col, value, createOption) {
|
|
2950
|
+
const cellValue = new Y.Map();
|
|
2951
|
+
cellValue.set("columnId", col.id);
|
|
2952
|
+
switch (col.type) {
|
|
2953
|
+
case "rich-text":
|
|
2954
|
+
case "title":
|
|
2955
|
+
cellValue.set("value", makeText(String(value ?? "")));
|
|
2956
|
+
break;
|
|
2957
|
+
case "number": {
|
|
2958
|
+
const num = Number(value);
|
|
2959
|
+
if (Number.isNaN(num)) {
|
|
2960
|
+
throw new Error(`Column "${col.name}": expected a number, got ${JSON.stringify(value)}`);
|
|
2961
|
+
}
|
|
2962
|
+
cellValue.set("value", num);
|
|
2963
|
+
break;
|
|
2964
|
+
}
|
|
2965
|
+
case "checkbox": {
|
|
2966
|
+
let bool;
|
|
2967
|
+
if (typeof value === "boolean") {
|
|
2968
|
+
bool = value;
|
|
2969
|
+
}
|
|
2970
|
+
else if (typeof value === "string") {
|
|
2971
|
+
const lower = value.toLowerCase().trim();
|
|
2972
|
+
bool = lower === "true" || lower === "1" || lower === "yes";
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
bool = !!value;
|
|
2976
|
+
}
|
|
2977
|
+
cellValue.set("value", bool);
|
|
2978
|
+
break;
|
|
2979
|
+
}
|
|
2980
|
+
case "select":
|
|
2981
|
+
cellValue.set("value", resolveSelectOptionId(col, String(value ?? ""), createOption));
|
|
2982
|
+
break;
|
|
2983
|
+
case "multi-select": {
|
|
2984
|
+
const labels = Array.isArray(value) ? value.map(String) : [String(value ?? "")];
|
|
2985
|
+
const optionIds = new Y.Array();
|
|
2986
|
+
optionIds.push(labels.map(label => resolveSelectOptionId(col, label, createOption)));
|
|
2987
|
+
cellValue.set("value", optionIds);
|
|
2988
|
+
break;
|
|
2989
|
+
}
|
|
2990
|
+
case "date": {
|
|
2991
|
+
const numericValue = typeof value === "number"
|
|
2992
|
+
? value
|
|
2993
|
+
: Number.isNaN(Number(value)) ? Date.parse(String(value)) : Number(value);
|
|
2994
|
+
if (!Number.isFinite(numericValue)) {
|
|
2995
|
+
throw new Error(`Column "${col.name}": expected a timestamp-compatible value, got ${JSON.stringify(value)}`);
|
|
2996
|
+
}
|
|
2997
|
+
cellValue.set("value", numericValue);
|
|
2998
|
+
break;
|
|
2999
|
+
}
|
|
3000
|
+
case "link":
|
|
3001
|
+
cellValue.set("value", String(value ?? ""));
|
|
3002
|
+
break;
|
|
3003
|
+
default:
|
|
3004
|
+
if (typeof value === "string") {
|
|
3005
|
+
cellValue.set("value", makeText(value));
|
|
3006
|
+
}
|
|
3007
|
+
else {
|
|
3008
|
+
cellValue.set("value", value);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
rowCells.set(col.id, cellValue);
|
|
3012
|
+
}
|
|
3013
|
+
async function loadDatabaseDocContext(workspaceId, docId, databaseBlockId) {
|
|
3014
|
+
const { endpoint, cookie, bearer } = await getCookieAndEndpoint();
|
|
3015
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
3016
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
3017
|
+
await joinWorkspace(socket, workspaceId);
|
|
3018
|
+
const doc = new Y.Doc();
|
|
3019
|
+
const snapshot = await loadDoc(socket, workspaceId, docId);
|
|
3020
|
+
if (!snapshot.missing) {
|
|
3021
|
+
socket.disconnect();
|
|
3022
|
+
throw new Error("Document not found");
|
|
3023
|
+
}
|
|
3024
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
3025
|
+
const prevSV = Y.encodeStateVector(doc);
|
|
3026
|
+
const blocks = doc.getMap("blocks");
|
|
3027
|
+
const dbBlock = findBlockById(blocks, databaseBlockId);
|
|
3028
|
+
if (!dbBlock) {
|
|
3029
|
+
socket.disconnect();
|
|
3030
|
+
throw new Error(`Database block '${databaseBlockId}' not found`);
|
|
3031
|
+
}
|
|
3032
|
+
const dbFlavour = dbBlock.get("sys:flavour");
|
|
3033
|
+
if (dbFlavour !== "affine:database") {
|
|
3034
|
+
socket.disconnect();
|
|
3035
|
+
throw new Error(`Block '${databaseBlockId}' is not a database (flavour: ${dbFlavour})`);
|
|
3036
|
+
}
|
|
3037
|
+
const cellsMap = dbBlock.get("prop:cells");
|
|
3038
|
+
if (!(cellsMap instanceof Y.Map)) {
|
|
3039
|
+
socket.disconnect();
|
|
3040
|
+
throw new Error("Database block has no cells map");
|
|
3041
|
+
}
|
|
3042
|
+
const lookup = buildDatabaseColumnLookup(readColumnDefs(dbBlock));
|
|
3043
|
+
return {
|
|
3044
|
+
socket,
|
|
3045
|
+
doc,
|
|
3046
|
+
prevSV,
|
|
3047
|
+
blocks,
|
|
3048
|
+
dbBlock,
|
|
3049
|
+
cellsMap,
|
|
3050
|
+
...lookup,
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
2568
3053
|
// ADD DATABASE ROW
|
|
2569
3054
|
const addDatabaseRowHandler = async (parsed) => {
|
|
2570
3055
|
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
2571
3056
|
if (!workspaceId)
|
|
2572
3057
|
throw new Error("workspaceId is required");
|
|
2573
|
-
const
|
|
2574
|
-
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
2575
|
-
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
3058
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
2576
3059
|
try {
|
|
2577
|
-
await joinWorkspace(socket, workspaceId);
|
|
2578
|
-
const doc = new Y.Doc();
|
|
2579
|
-
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
2580
|
-
if (!snapshot.missing)
|
|
2581
|
-
throw new Error("Document not found");
|
|
2582
|
-
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
2583
|
-
const prevSV = Y.encodeStateVector(doc);
|
|
2584
|
-
const blocks = doc.getMap("blocks");
|
|
2585
|
-
// Find the database block
|
|
2586
|
-
const dbBlock = findBlockById(blocks, parsed.databaseBlockId);
|
|
2587
|
-
if (!dbBlock)
|
|
2588
|
-
throw new Error(`Database block '${parsed.databaseBlockId}' not found`);
|
|
2589
|
-
const dbFlavour = dbBlock.get("sys:flavour");
|
|
2590
|
-
if (dbFlavour !== "affine:database") {
|
|
2591
|
-
throw new Error(`Block '${parsed.databaseBlockId}' is not a database (flavour: ${dbFlavour})`);
|
|
2592
|
-
}
|
|
2593
|
-
// Read column definitions with select options
|
|
2594
|
-
const columnDefs = readColumnDefs(dbBlock);
|
|
2595
|
-
// Build lookups
|
|
2596
|
-
const colByName = new Map();
|
|
2597
|
-
const colById = new Map();
|
|
2598
|
-
for (const col of columnDefs) {
|
|
2599
|
-
if (col.name)
|
|
2600
|
-
colByName.set(col.name, col);
|
|
2601
|
-
colById.set(col.id, col);
|
|
2602
|
-
}
|
|
2603
|
-
// Identify the title column (first column, or type === "title")
|
|
2604
|
-
const titleCol = columnDefs.find(c => c.type === "title") || null;
|
|
2605
3060
|
// Create a new paragraph block as the row child of the database
|
|
2606
3061
|
const rowBlockId = generateId();
|
|
2607
3062
|
const rowBlock = new Y.Map();
|
|
@@ -2609,104 +3064,26 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2609
3064
|
rowBlock.set("sys:parent", parsed.databaseBlockId);
|
|
2610
3065
|
rowBlock.set("sys:children", new Y.Array());
|
|
2611
3066
|
rowBlock.set("prop:type", "text");
|
|
2612
|
-
|
|
2613
|
-
const titleValue = titleCol ? parsed.cells[titleCol.name] ?? parsed.cells[titleCol.id] ?? "" : "";
|
|
3067
|
+
const titleValue = resolveDatabaseTitleValue(parsed.cells, ctx);
|
|
2614
3068
|
rowBlock.set("prop:text", makeText(String(titleValue)));
|
|
2615
|
-
blocks.set(rowBlockId, rowBlock);
|
|
3069
|
+
ctx.blocks.set(rowBlockId, rowBlock);
|
|
2616
3070
|
// Add row block to database's children
|
|
2617
|
-
const dbChildren = ensureChildrenArray(dbBlock);
|
|
3071
|
+
const dbChildren = ensureChildrenArray(ctx.dbBlock);
|
|
2618
3072
|
dbChildren.push([rowBlockId]);
|
|
2619
|
-
// Populate cells map on the database block
|
|
2620
|
-
const cellsMap = dbBlock.get("prop:cells");
|
|
2621
|
-
if (!(cellsMap instanceof Y.Map)) {
|
|
2622
|
-
throw new Error("Database block has no cells map");
|
|
2623
|
-
}
|
|
2624
3073
|
// Create row cell map
|
|
2625
|
-
const rowCells =
|
|
3074
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, rowBlockId);
|
|
2626
3075
|
for (const [key, value] of Object.entries(parsed.cells)) {
|
|
2627
|
-
|
|
2628
|
-
const col = colByName.get(key) || colById.get(key);
|
|
3076
|
+
const col = findDatabaseColumn(key, ctx);
|
|
2629
3077
|
if (!col) {
|
|
2630
|
-
|
|
2631
|
-
}
|
|
2632
|
-
// Skip the title column — already stored on the paragraph block
|
|
2633
|
-
if (titleCol && col.id === titleCol.id)
|
|
2634
|
-
continue;
|
|
2635
|
-
// Create cell value based on column type
|
|
2636
|
-
const cellValue = new Y.Map();
|
|
2637
|
-
cellValue.set("columnId", col.id);
|
|
2638
|
-
switch (col.type) {
|
|
2639
|
-
case "rich-text": {
|
|
2640
|
-
const yText = makeText(String(value ?? ""));
|
|
2641
|
-
cellValue.set("value", yText);
|
|
2642
|
-
break;
|
|
2643
|
-
}
|
|
2644
|
-
case "title": {
|
|
2645
|
-
// Handled above on the paragraph block; skip
|
|
3078
|
+
if (isTitleAliasKey(key)) {
|
|
2646
3079
|
continue;
|
|
2647
3080
|
}
|
|
2648
|
-
|
|
2649
|
-
const num = Number(value);
|
|
2650
|
-
if (Number.isNaN(num)) {
|
|
2651
|
-
throw new Error(`Column "${col.name}": expected a number, got ${JSON.stringify(value)}`);
|
|
2652
|
-
}
|
|
2653
|
-
cellValue.set("value", num);
|
|
2654
|
-
break;
|
|
2655
|
-
}
|
|
2656
|
-
case "checkbox": {
|
|
2657
|
-
let bool;
|
|
2658
|
-
if (typeof value === "boolean") {
|
|
2659
|
-
bool = value;
|
|
2660
|
-
}
|
|
2661
|
-
else if (typeof value === "string") {
|
|
2662
|
-
const lower = value.toLowerCase().trim();
|
|
2663
|
-
bool = lower === "true" || lower === "1" || lower === "yes";
|
|
2664
|
-
}
|
|
2665
|
-
else {
|
|
2666
|
-
bool = !!value;
|
|
2667
|
-
}
|
|
2668
|
-
cellValue.set("value", bool);
|
|
2669
|
-
break;
|
|
2670
|
-
}
|
|
2671
|
-
case "select": {
|
|
2672
|
-
// Resolve option ID by label text; auto-create if needed
|
|
2673
|
-
const optionId = resolveSelectOptionId(col, String(value ?? ""));
|
|
2674
|
-
cellValue.set("value", optionId);
|
|
2675
|
-
break;
|
|
2676
|
-
}
|
|
2677
|
-
case "multi-select": {
|
|
2678
|
-
const labels = Array.isArray(value) ? value.map(String) : [String(value ?? "")];
|
|
2679
|
-
const ids = labels.map(lbl => resolveSelectOptionId(col, lbl));
|
|
2680
|
-
cellValue.set("value", ids);
|
|
2681
|
-
break;
|
|
2682
|
-
}
|
|
2683
|
-
case "date": {
|
|
2684
|
-
const ts = Number(value);
|
|
2685
|
-
if (Number.isNaN(ts)) {
|
|
2686
|
-
throw new Error(`Column "${col.name}": expected a timestamp number, got ${JSON.stringify(value)}`);
|
|
2687
|
-
}
|
|
2688
|
-
cellValue.set("value", ts);
|
|
2689
|
-
break;
|
|
2690
|
-
}
|
|
2691
|
-
case "link": {
|
|
2692
|
-
cellValue.set("value", String(value ?? ""));
|
|
2693
|
-
break;
|
|
2694
|
-
}
|
|
2695
|
-
default: {
|
|
2696
|
-
// Fallback: store as rich-text
|
|
2697
|
-
if (typeof value === "string") {
|
|
2698
|
-
cellValue.set("value", makeText(value));
|
|
2699
|
-
}
|
|
2700
|
-
else {
|
|
2701
|
-
cellValue.set("value", value);
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
3081
|
+
throw new Error(`Column '${key}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
2704
3082
|
}
|
|
2705
|
-
rowCells
|
|
3083
|
+
writeDatabaseCellValue(rowCells, col, value, true);
|
|
2706
3084
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3085
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3086
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
2710
3087
|
return text({
|
|
2711
3088
|
added: true,
|
|
2712
3089
|
rowBlockId,
|
|
@@ -2715,7 +3092,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2715
3092
|
});
|
|
2716
3093
|
}
|
|
2717
3094
|
finally {
|
|
2718
|
-
socket.disconnect();
|
|
3095
|
+
ctx.socket.disconnect();
|
|
2719
3096
|
}
|
|
2720
3097
|
};
|
|
2721
3098
|
server.registerTool("add_database_row", {
|
|
@@ -2728,6 +3105,164 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2728
3105
|
cells: z.record(z.unknown()).describe("Map of column name (or column ID) to cell value. For select columns, pass the display label (option auto-created if new)."),
|
|
2729
3106
|
},
|
|
2730
3107
|
}, addDatabaseRowHandler);
|
|
3108
|
+
const readDatabaseCellsHandler = async (parsed) => {
|
|
3109
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3110
|
+
if (!workspaceId)
|
|
3111
|
+
throw new Error("workspaceId is required");
|
|
3112
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3113
|
+
try {
|
|
3114
|
+
const requestedRows = parsed.rowBlockIds?.length
|
|
3115
|
+
? parsed.rowBlockIds
|
|
3116
|
+
: getDatabaseRowIds(ctx.dbBlock);
|
|
3117
|
+
const requestedColumns = parsed.columns?.length
|
|
3118
|
+
? parsed.columns.map(columnKey => {
|
|
3119
|
+
const col = findDatabaseColumn(columnKey, ctx);
|
|
3120
|
+
if (!col) {
|
|
3121
|
+
throw new Error(`Column '${columnKey}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3122
|
+
}
|
|
3123
|
+
return col;
|
|
3124
|
+
})
|
|
3125
|
+
: ctx.columnDefs;
|
|
3126
|
+
const requestedColumnIds = new Set(requestedColumns.map(col => col.id));
|
|
3127
|
+
const rows = requestedRows.map(rowBlockId => {
|
|
3128
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, rowBlockId);
|
|
3129
|
+
const title = readDatabaseRowTitle(rowBlock) || null;
|
|
3130
|
+
const rowCells = ctx.cellsMap.get(rowBlockId);
|
|
3131
|
+
const cells = {};
|
|
3132
|
+
if (rowCells instanceof Y.Map) {
|
|
3133
|
+
for (const col of ctx.columnDefs) {
|
|
3134
|
+
if (ctx.titleCol && col.id === ctx.titleCol.id) {
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
if (!requestedColumnIds.has(col.id)) {
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const cellEntry = rowCells.get(col.id);
|
|
3141
|
+
if (cellEntry === undefined) {
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
cells[col.name || col.id] = decodeDatabaseCellValue(col, cellEntry);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
rowBlockId,
|
|
3149
|
+
title,
|
|
3150
|
+
cells,
|
|
3151
|
+
};
|
|
3152
|
+
});
|
|
3153
|
+
return text({ rows });
|
|
3154
|
+
}
|
|
3155
|
+
finally {
|
|
3156
|
+
ctx.socket.disconnect();
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
server.registerTool("read_database_cells", {
|
|
3160
|
+
title: "Read Database Cells",
|
|
3161
|
+
description: "Read row titles and database cell values from an AFFiNE database block.",
|
|
3162
|
+
inputSchema: {
|
|
3163
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3164
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3165
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3166
|
+
rowBlockIds: z.array(z.string().min(1)).optional().describe("Optional row block ID filter. Omit to return all rows."),
|
|
3167
|
+
columns: z.array(z.string().min(1)).optional().describe("Optional column name or ID filter."),
|
|
3168
|
+
},
|
|
3169
|
+
}, readDatabaseCellsHandler);
|
|
3170
|
+
const updateDatabaseCellHandler = async (parsed) => {
|
|
3171
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3172
|
+
if (!workspaceId)
|
|
3173
|
+
throw new Error("workspaceId is required");
|
|
3174
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3175
|
+
try {
|
|
3176
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, parsed.rowBlockId);
|
|
3177
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, parsed.rowBlockId);
|
|
3178
|
+
const col = findDatabaseColumn(parsed.column, ctx);
|
|
3179
|
+
if (!col) {
|
|
3180
|
+
if (!isTitleAliasKey(parsed.column)) {
|
|
3181
|
+
throw new Error(`Column '${parsed.column}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
else {
|
|
3185
|
+
writeDatabaseCellValue(rowCells, col, parsed.value, parsed.createOption ?? true);
|
|
3186
|
+
}
|
|
3187
|
+
if (isTitleAliasKey(parsed.column) || (col && (col.type === "title" || isTitleAliasKey(col.name)))) {
|
|
3188
|
+
rowBlock.set("prop:text", makeText(String(parsed.value ?? "")));
|
|
3189
|
+
}
|
|
3190
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3191
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3192
|
+
return text({
|
|
3193
|
+
updated: true,
|
|
3194
|
+
rowBlockId: parsed.rowBlockId,
|
|
3195
|
+
column: parsed.column,
|
|
3196
|
+
value: parsed.value ?? null,
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
finally {
|
|
3200
|
+
ctx.socket.disconnect();
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
server.registerTool("update_database_cell", {
|
|
3204
|
+
title: "Update Database Cell",
|
|
3205
|
+
description: "Update a single cell on an existing AFFiNE database row. Use `title` to update the row title shown in Kanban card headers.",
|
|
3206
|
+
inputSchema: {
|
|
3207
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3208
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3209
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3210
|
+
rowBlockId: z.string().min(1).describe("Row paragraph block ID"),
|
|
3211
|
+
column: z.string().min(1).describe("Column name or ID. Use `title` for the built-in row title."),
|
|
3212
|
+
value: z.unknown().describe("New cell value"),
|
|
3213
|
+
createOption: z.boolean().optional().describe("For select and multi-select columns, create the option label if it does not exist (default true)"),
|
|
3214
|
+
},
|
|
3215
|
+
}, updateDatabaseCellHandler);
|
|
3216
|
+
const updateDatabaseRowHandler = async (parsed) => {
|
|
3217
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3218
|
+
if (!workspaceId)
|
|
3219
|
+
throw new Error("workspaceId is required");
|
|
3220
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3221
|
+
try {
|
|
3222
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, parsed.rowBlockId);
|
|
3223
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, parsed.rowBlockId);
|
|
3224
|
+
let titleValue = null;
|
|
3225
|
+
for (const [key, value] of Object.entries(parsed.cells)) {
|
|
3226
|
+
const col = findDatabaseColumn(key, ctx);
|
|
3227
|
+
if (!col) {
|
|
3228
|
+
if (isTitleAliasKey(key)) {
|
|
3229
|
+
titleValue = String(value ?? "");
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
throw new Error(`Column '${key}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3233
|
+
}
|
|
3234
|
+
writeDatabaseCellValue(rowCells, col, value, parsed.createOption ?? true);
|
|
3235
|
+
if (col.type === "title" || isTitleAliasKey(col.name)) {
|
|
3236
|
+
titleValue = String(value ?? "");
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
if (titleValue !== null) {
|
|
3240
|
+
rowBlock.set("prop:text", makeText(titleValue));
|
|
3241
|
+
}
|
|
3242
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3243
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3244
|
+
return text({
|
|
3245
|
+
updated: true,
|
|
3246
|
+
rowBlockId: parsed.rowBlockId,
|
|
3247
|
+
cellCount: Object.keys(parsed.cells).length,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
finally {
|
|
3251
|
+
ctx.socket.disconnect();
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
server.registerTool("update_database_row", {
|
|
3255
|
+
title: "Update Database Row",
|
|
3256
|
+
description: "Batch update multiple cells on an existing AFFiNE database row. Include `title` in the cells map to update the Kanban row title.",
|
|
3257
|
+
inputSchema: {
|
|
3258
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3259
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3260
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3261
|
+
rowBlockId: z.string().min(1).describe("Row paragraph block ID"),
|
|
3262
|
+
cells: z.record(z.unknown()).describe("Map of column name (or column ID) to new cell value. Use `title` for the built-in row title."),
|
|
3263
|
+
createOption: z.boolean().optional().describe("For select and multi-select columns, create the option label if it does not exist (default true)"),
|
|
3264
|
+
},
|
|
3265
|
+
}, updateDatabaseRowHandler);
|
|
2731
3266
|
// ADD DATABASE COLUMN
|
|
2732
3267
|
const addDatabaseColumnHandler = async (parsed) => {
|
|
2733
3268
|
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "affine-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Model Context Protocol server for AFFiNE - enables AI assistants to interact with AFFiNE workspaces, documents, and collaboration features.",
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
"start": "node dist/index.js",
|
|
34
34
|
"start:http": "MCP_TRANSPORT=http node dist/index.js",
|
|
35
35
|
"test": "npm run test:tool-manifest",
|
|
36
|
+
"test:cli-version": "node tests/test-cli-version.mjs",
|
|
36
37
|
"test:tool-manifest": "node scripts/verify-tool-manifest.mjs",
|
|
37
38
|
"test:comprehensive": "node test-comprehensive.mjs",
|
|
38
39
|
"test:e2e": "bash tests/run-e2e.sh",
|
|
39
40
|
"test:db-create": "node tests/test-database-creation.mjs",
|
|
41
|
+
"test:db-cells": "node tests/test-database-cells.mjs",
|
|
40
42
|
"test:bearer": "node tests/test-bearer-auth.mjs",
|
|
43
|
+
"test:tag-visibility": "node tests/test-tag-visibility.mjs",
|
|
41
44
|
"test:playwright": "npx playwright test --config tests/playwright/playwright.config.ts",
|
|
42
45
|
"pack:check": "npm pack --dry-run",
|
|
43
46
|
"ci": "npm run build && npm run test:tool-manifest && npm run pack:check",
|