affine-mcp-server 1.7.2 → 1.9.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 +19 -78
- package/dist/index.js +7 -2
- package/dist/markdown/parse.js +13 -1
- package/dist/markdown/render.js +25 -0
- package/dist/tools/docs.js +633 -149
- package/package.json +10 -2
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: 47 focused tools with WebSocket-based document editing
|
|
20
20
|
- Status: Active
|
|
21
21
|
|
|
22
|
-
> New in v1.
|
|
22
|
+
> New in v1.9.0: Added database schema discovery, preset-backed data views, self-bootstrapping comprehensive regression, focused supporting-tools coverage, and markdown callout round-trips.
|
|
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, inspect schema, 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
|
|
|
@@ -324,9 +325,13 @@ Endpoints currently available:
|
|
|
324
325
|
- `add_tag_to_doc` – attach a tag to a document
|
|
325
326
|
- `remove_tag_from_doc` – detach a tag from a document
|
|
326
327
|
- `append_paragraph` – append a paragraph block (WebSocket)
|
|
327
|
-
- `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`
|
|
328
|
+
- `append_block` – append canonical block types (text/list/code/media/embed/database/edgeless) with strict validation and placement control (`viewMode=kanban` enables preset-backed data views; `data_view` defaults to kanban)
|
|
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_columns` – read database schema metadata including column IDs/types, select options, and table view column mappings
|
|
332
|
+
- `read_database_cells` – read row titles plus decoded database cell values with optional row / column filters
|
|
333
|
+
- `update_database_cell` – update a single database cell or the built-in row title (`createOption` defaults to `true` for select fields)
|
|
334
|
+
- `update_database_row` – batch update multiple cells on a database row (`createOption` defaults to `true` for select fields)
|
|
330
335
|
- `append_markdown` – append markdown content to an existing document
|
|
331
336
|
- `replace_doc_with_markdown` – replace the main note content with markdown content
|
|
332
337
|
- `delete_doc` – delete a document (WebSocket)
|
|
@@ -372,9 +377,10 @@ npm run pack:check
|
|
|
372
377
|
|
|
373
378
|
- `tool-manifest.json` is the source of truth for publicly exposed tool names.
|
|
374
379
|
- CI validates that `registerTool(...)` declarations match the manifest exactly.
|
|
375
|
-
- For full tool-surface verification, run `npm run test:comprehensive
|
|
380
|
+
- For full tool-surface verification, run `npm run test:comprehensive` (self-bootstraps a local Docker AFFiNE stack).
|
|
381
|
+
- For pre-provisioned environments, use `npm run test:comprehensive:raw`.
|
|
376
382
|
- 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`.
|
|
383
|
+
- Additional focused runners: `npm run test:db-create`, `npm run test:db-cells`, `npm run test:db-schema`, `npm run test:supporting-tools`, `npm run test:bearer`, `npm run test:cli-version`, `npm run test:playwright`.
|
|
378
384
|
|
|
379
385
|
## Troubleshooting
|
|
380
386
|
|
|
@@ -407,76 +413,11 @@ Workspace visibility
|
|
|
407
413
|
- Use HTTPS
|
|
408
414
|
- Store credentials in a secrets manager
|
|
409
415
|
|
|
410
|
-
##
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
-
|
|
414
|
-
-
|
|
415
|
-
- Added tag visibility regression coverage (`tests/test-tag-visibility.mjs`, `tests/playwright/verify-tag-visibility.pw.ts`)
|
|
416
|
-
- Hardened E2E credential bootstrap with configurable health retries, retry attempts, and Docker diagnostics on failure
|
|
417
|
-
- Verified CI gates (`validate`, `e2e`) for PR #46 and local `npm run ci`
|
|
418
|
-
|
|
419
|
-
### 1.7.1 (2026‑03‑03)
|
|
420
|
-
- Fixed MCP-created document structure parity with AFFiNE UI (`sys:parent` handling)
|
|
421
|
-
- Fixed callout text rendering parity in AFFiNE UI for MCP-created blocks
|
|
422
|
-
- Added regression assertions for visibility-sensitive document creation paths
|
|
423
|
-
|
|
424
|
-
### 1.7.0 (2026‑02‑27)
|
|
425
|
-
- Added Streamable HTTP MCP support on `/mcp` for remote hosting while keeping legacy SSE compatibility paths (`/sse`, `/messages`)
|
|
426
|
-
- Added HTTP deployment controls: `AFFINE_MCP_HTTP_HOST`, `AFFINE_MCP_HTTP_TOKEN`, `AFFINE_MCP_HTTP_ALLOWED_ORIGINS`, `AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS`
|
|
427
|
-
- Added `npm run start:http` for one-command HTTP mode startup
|
|
428
|
-
- Hardened HTTP request handling with explicit 50MB parser application and case-insensitive Bearer auth parsing
|
|
429
|
-
- Expanded docs with remote deployment/security presets (Docker, Render, Railway, VPS)
|
|
430
|
-
- Verified full release checks with `npm run ci`, `npm run test:e2e`, and `npm run test:comprehensive`
|
|
431
|
-
|
|
432
|
-
### 1.6.0 (2026‑02‑24)
|
|
433
|
-
- Added 11 document workflow tools: tags (`list_tags`, `list_docs_by_tag`, `create_tag`, `add_tag_to_doc`, `remove_tag_from_doc`), markdown roundtrip (`export_doc_markdown`, `create_doc_from_markdown`, `append_markdown`, `replace_doc_with_markdown`), and database operations (`add_database_column`, `add_database_row`)
|
|
434
|
-
- Added interactive CLI commands: `affine-mcp login`, `affine-mcp status`, `affine-mcp logout`
|
|
435
|
-
- Added Docker + Playwright E2E pipeline and CI workflow for auth/database regression checks
|
|
436
|
-
- Tool surface increased from 32 to 43 canonical tools
|
|
437
|
-
- Added release test commands (`test:e2e`, `test:db-create`, `test:bearer`, `test:playwright`) and package dependencies for markdown conversion + Playwright
|
|
438
|
-
|
|
439
|
-
### 1.5.0 (2026‑02‑13)
|
|
440
|
-
- Expanded `append_block` from Step1 to Step4 profiles: canonical text/list/code/divider/callout/latex/table/bookmark/media/embed plus `database`, `data_view`, `surface_ref`, `frame`, `edgeless_text`, `note` (`data_view` currently mapped to database for stability)
|
|
441
|
-
- Added strict field validation and canonical parent enforcement for page/note/surface containers
|
|
442
|
-
- Added local integration runner coverage for all 30 append_block cases against a live AFFINE server
|
|
443
|
-
|
|
444
|
-
### 1.4.0 (2026‑02‑13)
|
|
445
|
-
- Added `read_doc` for reading document block snapshot + plain text
|
|
446
|
-
- Added Cursor setup examples and troubleshooting notes for JSON-RPC method usage
|
|
447
|
-
- Added explicit local-storage workspace limitation notes
|
|
448
|
-
|
|
449
|
-
### 1.3.0 (2026‑02‑13)
|
|
450
|
-
- Added `append_block` for slash-command style editing (`heading/list/todo/code/divider/quote`)
|
|
451
|
-
- Tool surface simplified to 31 canonical tools (duplicate aliases removed)
|
|
452
|
-
- Added CI + manifest parity verification (`npm run test:tool-manifest`, `npm run ci`)
|
|
453
|
-
- Added open-source community health docs and issue/PR templates
|
|
454
|
-
|
|
455
|
-
### 1.2.2 (2025‑09‑18)
|
|
456
|
-
- CLI wrapper added to ensure Node runs ESM entry (`bin/affine-mcp`), preventing shell mis-execution
|
|
457
|
-
- Docs cleaned: use env vars via shell/app config; `.env` file no longer recommended
|
|
458
|
-
- MCP startup behavior unchanged from 1.2.1 (async login by default)
|
|
459
|
-
|
|
460
|
-
### 1.2.1 (2025‑09‑17)
|
|
461
|
-
- Default to asynchronous email/password login after MCP stdio handshake
|
|
462
|
-
- `AFFINE_LOGIN_AT_START` supports `sync` when you need blocking startup (default is non-blocking)
|
|
463
|
-
- Expanded docs for Codex/Claude using npm, npx, and local clone
|
|
464
|
-
|
|
465
|
-
### 1.2.0 (2025‑09‑16)
|
|
466
|
-
- WebSocket-based document tools: `create_doc`, `append_paragraph`, `delete_doc` (create/edit/delete now supported)
|
|
467
|
-
- Tool aliases introduced at the time (`affine_*` + non-prefixed names). They were removed later to reduce duplication.
|
|
468
|
-
- ESM resolution: NodeNext; improved build stability
|
|
469
|
-
- CLI binary: `affine-mcp` for easy `npm i -g` usage
|
|
470
|
-
|
|
471
|
-
### 1.1.0 (2025‑08‑12)
|
|
472
|
-
- Fixed workspace creation with initial documents (UI accessible)
|
|
473
|
-
- 30+ tools, simplified tool names
|
|
474
|
-
- Improved error handling and authentication
|
|
475
|
-
|
|
476
|
-
### 1.0.0 (2025‑08‑12)
|
|
477
|
-
- Initial stable release
|
|
478
|
-
- Basic workspace and document operations
|
|
479
|
-
- Full authentication support
|
|
416
|
+
## Release Notes
|
|
417
|
+
|
|
418
|
+
- Changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
419
|
+
- Release notes: [RELEASE_NOTES.md](RELEASE_NOTES.md)
|
|
420
|
+
- GitHub Releases: [Releases](https://github.com/dawncr0w/affine-mcp-server/releases)
|
|
480
421
|
|
|
481
422
|
## Contributing
|
|
482
423
|
|
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/markdown/parse.js
CHANGED
|
@@ -231,6 +231,14 @@ function collectQuoteText(tokens, start, end) {
|
|
|
231
231
|
}
|
|
232
232
|
return lines.join("\n");
|
|
233
233
|
}
|
|
234
|
+
function parseCalloutAdmonition(text) {
|
|
235
|
+
const lines = text.split("\n");
|
|
236
|
+
const marker = lines[0]?.trim() ?? "";
|
|
237
|
+
if (!/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/i.test(marker)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return lines.slice(1).join("\n").trim();
|
|
241
|
+
}
|
|
234
242
|
function parseList(tokens, start, end, defaultStyle, state, depth) {
|
|
235
243
|
const operations = [];
|
|
236
244
|
let i = start;
|
|
@@ -382,7 +390,11 @@ function parseTokens(tokens, start, end, state) {
|
|
|
382
390
|
break;
|
|
383
391
|
}
|
|
384
392
|
const quoteText = collectQuoteText(tokens, i + 1, close).trim();
|
|
385
|
-
|
|
393
|
+
const calloutText = parseCalloutAdmonition(quoteText);
|
|
394
|
+
if (calloutText !== null) {
|
|
395
|
+
state.operations.push({ type: "callout", text: calloutText });
|
|
396
|
+
}
|
|
397
|
+
else if (quoteText.length > 0) {
|
|
386
398
|
state.operations.push({ type: "quote", text: quoteText });
|
|
387
399
|
}
|
|
388
400
|
i = close + 1;
|
package/dist/markdown/render.js
CHANGED
|
@@ -8,6 +8,12 @@ function formatQuote(text) {
|
|
|
8
8
|
const lines = text.split("\n");
|
|
9
9
|
return lines.map(line => `> ${line}`);
|
|
10
10
|
}
|
|
11
|
+
function formatCallout(lines) {
|
|
12
|
+
return [
|
|
13
|
+
"> [!NOTE]",
|
|
14
|
+
...lines.map(line => line.length > 0 ? `> ${line}` : ">"),
|
|
15
|
+
];
|
|
16
|
+
}
|
|
11
17
|
function escapePipe(value) {
|
|
12
18
|
return value.replace(/\|/g, "\\|");
|
|
13
19
|
}
|
|
@@ -138,6 +144,25 @@ function renderBlock(blockId, listDepth, state) {
|
|
|
138
144
|
isList: false,
|
|
139
145
|
};
|
|
140
146
|
}
|
|
147
|
+
case "affine:callout": {
|
|
148
|
+
const contentLines = [];
|
|
149
|
+
for (const childId of children) {
|
|
150
|
+
const child = renderBlock(childId, listDepth, state);
|
|
151
|
+
if (child.lines.length > 0) {
|
|
152
|
+
if (contentLines.length > 0 && !child.isList) {
|
|
153
|
+
contentLines.push("");
|
|
154
|
+
}
|
|
155
|
+
contentLines.push(...child.lines);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (contentLines.length === 0 && text.length > 0) {
|
|
159
|
+
contentLines.push(text);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
lines: formatCallout(contentLines),
|
|
163
|
+
isList: false,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
141
166
|
case "affine:note":
|
|
142
167
|
case "affine:page":
|
|
143
168
|
case "affine:surface": {
|
package/dist/tools/docs.js
CHANGED
|
@@ -53,6 +53,8 @@ const APPEND_BLOCK_BOOKMARK_STYLE_VALUES = [
|
|
|
53
53
|
"citation",
|
|
54
54
|
];
|
|
55
55
|
const AppendBlockBookmarkStyle = z.enum(APPEND_BLOCK_BOOKMARK_STYLE_VALUES);
|
|
56
|
+
const APPEND_BLOCK_DATA_VIEW_MODE_VALUES = ["table", "kanban"];
|
|
57
|
+
const AppendBlockDataViewMode = z.enum(APPEND_BLOCK_DATA_VIEW_MODE_VALUES);
|
|
56
58
|
function blockVersion(flavour) {
|
|
57
59
|
switch (flavour) {
|
|
58
60
|
case "affine:page":
|
|
@@ -78,6 +80,11 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
78
80
|
const bearer = gql.bearer;
|
|
79
81
|
return { endpoint, cookie, bearer };
|
|
80
82
|
}
|
|
83
|
+
const SELECT_COLORS = [
|
|
84
|
+
"var(--affine-tag-blue)", "var(--affine-tag-green)", "var(--affine-tag-red)",
|
|
85
|
+
"var(--affine-tag-orange)", "var(--affine-tag-purple)", "var(--affine-tag-yellow)",
|
|
86
|
+
"var(--affine-tag-teal)", "var(--affine-tag-pink)", "var(--affine-tag-gray)",
|
|
87
|
+
];
|
|
81
88
|
function makeText(content) {
|
|
82
89
|
const yText = new Y.Text();
|
|
83
90
|
if (content.length > 0) {
|
|
@@ -717,6 +724,9 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
717
724
|
else if (raw.tableData !== undefined && normalized.strict) {
|
|
718
725
|
throw new Error("The 'tableData' field can only be used with type='table'.");
|
|
719
726
|
}
|
|
727
|
+
if (normalized.type !== "database" && normalized.type !== "data_view" && raw.viewMode !== undefined && normalized.strict) {
|
|
728
|
+
throw new Error("The 'viewMode' field can only be used with type='database' or type='data_view'.");
|
|
729
|
+
}
|
|
720
730
|
}
|
|
721
731
|
function normalizeAppendBlockInput(parsed) {
|
|
722
732
|
const strict = parsed.strict !== false;
|
|
@@ -726,6 +736,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
726
736
|
const headingLevel = Math.max(1, Math.min(6, headingLevelNumber));
|
|
727
737
|
const listStyle = typeInfo.listStyleFromAlias ?? parsed.style ?? "bulleted";
|
|
728
738
|
const bookmarkStyle = parsed.bookmarkStyle ?? "horizontal";
|
|
739
|
+
const dataViewMode = parsed.viewMode ?? (typeInfo.type === "data_view" ? "kanban" : "table");
|
|
729
740
|
const language = (parsed.language ?? "txt").trim().toLowerCase() || "txt";
|
|
730
741
|
const placement = normalizePlacement(parsed.placement);
|
|
731
742
|
const url = (parsed.url ?? "").trim();
|
|
@@ -774,6 +785,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
774
785
|
headingLevel,
|
|
775
786
|
listStyle,
|
|
776
787
|
bookmarkStyle,
|
|
788
|
+
dataViewMode,
|
|
777
789
|
checked: Boolean(parsed.checked),
|
|
778
790
|
language,
|
|
779
791
|
caption: parsed.caption,
|
|
@@ -929,6 +941,102 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
929
941
|
}
|
|
930
942
|
return { parentId, parentBlock, children, insertIndex };
|
|
931
943
|
}
|
|
944
|
+
function createDatabaseViewColumn(columnId, width = 200, hide = false) {
|
|
945
|
+
const column = new Y.Map();
|
|
946
|
+
column.set("id", columnId);
|
|
947
|
+
column.set("width", width);
|
|
948
|
+
column.set("hide", hide);
|
|
949
|
+
return column;
|
|
950
|
+
}
|
|
951
|
+
function createDatabaseColumnDefinition(input) {
|
|
952
|
+
const column = new Y.Map();
|
|
953
|
+
column.set("id", input.id);
|
|
954
|
+
column.set("name", input.name);
|
|
955
|
+
column.set("type", input.type);
|
|
956
|
+
column.set("width", input.width ?? 200);
|
|
957
|
+
if ((input.type === "select" || input.type === "multi-select") && input.options?.length) {
|
|
958
|
+
const data = new Y.Map();
|
|
959
|
+
const options = new Y.Array();
|
|
960
|
+
input.options.forEach((value, index) => {
|
|
961
|
+
const option = new Y.Map();
|
|
962
|
+
option.set("id", generateId());
|
|
963
|
+
option.set("value", value);
|
|
964
|
+
option.set("color", SELECT_COLORS[index % SELECT_COLORS.length]);
|
|
965
|
+
options.push([option]);
|
|
966
|
+
});
|
|
967
|
+
data.set("options", options);
|
|
968
|
+
column.set("data", data);
|
|
969
|
+
}
|
|
970
|
+
return column;
|
|
971
|
+
}
|
|
972
|
+
function createPresetBackedDataViewBlock(blockId, titleText, viewMode, blockType) {
|
|
973
|
+
const block = new Y.Map();
|
|
974
|
+
setSysFields(block, blockId, "affine:database");
|
|
975
|
+
block.set("sys:parent", null);
|
|
976
|
+
block.set("sys:children", new Y.Array());
|
|
977
|
+
block.set("prop:title", makeText(titleText));
|
|
978
|
+
block.set("prop:cells", new Y.Map());
|
|
979
|
+
block.set("prop:comments", undefined);
|
|
980
|
+
const titleColumnId = generateId();
|
|
981
|
+
const columns = new Y.Array();
|
|
982
|
+
columns.push([createDatabaseColumnDefinition({
|
|
983
|
+
id: titleColumnId,
|
|
984
|
+
name: "Title",
|
|
985
|
+
type: "title",
|
|
986
|
+
width: 320,
|
|
987
|
+
})]);
|
|
988
|
+
const viewColumns = new Y.Array();
|
|
989
|
+
viewColumns.push([createDatabaseViewColumn(titleColumnId, 320, false)]);
|
|
990
|
+
const header = {
|
|
991
|
+
titleColumn: titleColumnId,
|
|
992
|
+
iconColumn: "type",
|
|
993
|
+
};
|
|
994
|
+
let groupBy = null;
|
|
995
|
+
let groupProperties = null;
|
|
996
|
+
if (viewMode === "kanban") {
|
|
997
|
+
const statusColumnId = generateId();
|
|
998
|
+
columns.push([createDatabaseColumnDefinition({
|
|
999
|
+
id: statusColumnId,
|
|
1000
|
+
name: "Status",
|
|
1001
|
+
type: "select",
|
|
1002
|
+
options: ["Todo", "In Progress", "Done"],
|
|
1003
|
+
})]);
|
|
1004
|
+
viewColumns.push([createDatabaseViewColumn(statusColumnId, 200, false)]);
|
|
1005
|
+
groupBy = {
|
|
1006
|
+
columnId: statusColumnId,
|
|
1007
|
+
name: "select",
|
|
1008
|
+
type: "groupBy",
|
|
1009
|
+
};
|
|
1010
|
+
groupProperties = [];
|
|
1011
|
+
}
|
|
1012
|
+
const view = new Y.Map();
|
|
1013
|
+
view.set("id", generateId());
|
|
1014
|
+
view.set("name", viewMode === "kanban" ? "Kanban View" : "Table View");
|
|
1015
|
+
view.set("mode", viewMode);
|
|
1016
|
+
view.set("columns", viewColumns);
|
|
1017
|
+
view.set("filter", { type: "group", op: "and", conditions: [] });
|
|
1018
|
+
if (groupBy) {
|
|
1019
|
+
view.set("groupBy", groupBy);
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
view.set("groupBy", null);
|
|
1023
|
+
}
|
|
1024
|
+
if (groupProperties) {
|
|
1025
|
+
view.set("groupProperties", groupProperties);
|
|
1026
|
+
}
|
|
1027
|
+
view.set("sort", null);
|
|
1028
|
+
view.set("header", header);
|
|
1029
|
+
const views = new Y.Array();
|
|
1030
|
+
views.push([view]);
|
|
1031
|
+
block.set("prop:columns", columns);
|
|
1032
|
+
block.set("prop:views", views);
|
|
1033
|
+
return {
|
|
1034
|
+
blockId,
|
|
1035
|
+
block,
|
|
1036
|
+
flavour: "affine:database",
|
|
1037
|
+
blockType,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
932
1040
|
function createBlock(normalized) {
|
|
933
1041
|
const blockId = generateId();
|
|
934
1042
|
const block = new Y.Map();
|
|
@@ -1237,6 +1345,9 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1237
1345
|
return { blockId, block, flavour: "affine:embed-iframe" };
|
|
1238
1346
|
}
|
|
1239
1347
|
case "database": {
|
|
1348
|
+
if (normalized.dataViewMode === "kanban") {
|
|
1349
|
+
return createPresetBackedDataViewBlock(blockId, normalized.text, "kanban", "database_kanban");
|
|
1350
|
+
}
|
|
1240
1351
|
setSysFields(block, blockId, "affine:database");
|
|
1241
1352
|
block.set("sys:parent", null);
|
|
1242
1353
|
block.set("sys:children", new Y.Array());
|
|
@@ -1260,28 +1371,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1260
1371
|
return { blockId, block, flavour: "affine:database" };
|
|
1261
1372
|
}
|
|
1262
1373
|
case "data_view": {
|
|
1263
|
-
|
|
1264
|
-
// Keep API compatibility for type="data_view" by mapping it to the stable database block.
|
|
1265
|
-
setSysFields(block, blockId, "affine:database");
|
|
1266
|
-
block.set("sys:parent", null);
|
|
1267
|
-
block.set("sys:children", new Y.Array());
|
|
1268
|
-
const dvDefaultView = new Y.Map();
|
|
1269
|
-
dvDefaultView.set("id", generateId());
|
|
1270
|
-
dvDefaultView.set("name", "Table View");
|
|
1271
|
-
dvDefaultView.set("mode", "table");
|
|
1272
|
-
dvDefaultView.set("columns", new Y.Array());
|
|
1273
|
-
dvDefaultView.set("filter", { type: "group", op: "and", conditions: [] });
|
|
1274
|
-
dvDefaultView.set("groupBy", null);
|
|
1275
|
-
dvDefaultView.set("sort", null);
|
|
1276
|
-
dvDefaultView.set("header", { titleColumn: null, iconColumn: null });
|
|
1277
|
-
const dvViews = new Y.Array();
|
|
1278
|
-
dvViews.push([dvDefaultView]);
|
|
1279
|
-
block.set("prop:views", dvViews);
|
|
1280
|
-
block.set("prop:title", makeText(content));
|
|
1281
|
-
block.set("prop:cells", new Y.Map());
|
|
1282
|
-
block.set("prop:columns", new Y.Array());
|
|
1283
|
-
block.set("prop:comments", undefined);
|
|
1284
|
-
return { blockId, block, flavour: "affine:database", blockType: "data_view_fallback" };
|
|
1374
|
+
return createPresetBackedDataViewBlock(blockId, normalized.text, normalized.dataViewMode, `data_view_${normalized.dataViewMode}`);
|
|
1285
1375
|
}
|
|
1286
1376
|
case "surface_ref": {
|
|
1287
1377
|
setSysFields(block, blockId, "affine:surface-ref");
|
|
@@ -1425,6 +1515,15 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
1425
1515
|
strict,
|
|
1426
1516
|
placement,
|
|
1427
1517
|
};
|
|
1518
|
+
case "callout":
|
|
1519
|
+
return {
|
|
1520
|
+
workspaceId,
|
|
1521
|
+
docId,
|
|
1522
|
+
type: "callout",
|
|
1523
|
+
text: operation.text,
|
|
1524
|
+
strict,
|
|
1525
|
+
placement,
|
|
1526
|
+
};
|
|
1428
1527
|
case "list":
|
|
1429
1528
|
return {
|
|
1430
1529
|
workspaceId,
|
|
@@ -2439,6 +2538,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2439
2538
|
level: z.number().int().min(1).max(6).optional().describe("Heading level for type=heading"),
|
|
2440
2539
|
style: AppendBlockListStyle.optional().describe("List style for type=list"),
|
|
2441
2540
|
bookmarkStyle: AppendBlockBookmarkStyle.optional().describe("Bookmark card style"),
|
|
2541
|
+
viewMode: AppendBlockDataViewMode.optional().describe("Initial data view preset for type=database or type=data_view. Defaults: database=table, data_view=kanban"),
|
|
2442
2542
|
checked: z.boolean().optional().describe("Todo state when type is todo"),
|
|
2443
2543
|
language: z.string().optional().describe("Code language when type is code"),
|
|
2444
2544
|
caption: z.string().optional().describe("Code caption when type is code"),
|
|
@@ -2723,7 +2823,6 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2723
2823
|
description: 'Delete a document and remove from workspace list',
|
|
2724
2824
|
inputSchema: { workspaceId: z.string().optional(), docId: z.string() },
|
|
2725
2825
|
}, deleteDocHandler);
|
|
2726
|
-
// ── helpers for database select columns ──
|
|
2727
2826
|
/** Read column definitions including select options from a database block */
|
|
2728
2827
|
function readColumnDefs(dbBlock) {
|
|
2729
2828
|
const columnsRaw = dbBlock.get("prop:columns");
|
|
@@ -2761,17 +2860,158 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2761
2860
|
});
|
|
2762
2861
|
return defs;
|
|
2763
2862
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2863
|
+
function readDatabaseViewDefs(dbBlock, lookup) {
|
|
2864
|
+
const viewsRaw = dbBlock.get("prop:views");
|
|
2865
|
+
const views = [];
|
|
2866
|
+
if (!(viewsRaw instanceof Y.Array)) {
|
|
2867
|
+
return views;
|
|
2868
|
+
}
|
|
2869
|
+
viewsRaw.forEach((view) => {
|
|
2870
|
+
const id = view instanceof Y.Map ? view.get("id") : view?.id;
|
|
2871
|
+
if (!id) {
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2874
|
+
const columnsRaw = view instanceof Y.Map ? view.get("columns") : view?.columns;
|
|
2875
|
+
const headerRaw = view instanceof Y.Map ? view.get("header") : view?.header;
|
|
2876
|
+
const groupByRaw = view instanceof Y.Map ? view.get("groupBy") : view?.groupBy;
|
|
2877
|
+
const columns = databaseArrayValues(columnsRaw)
|
|
2878
|
+
.map((entry) => {
|
|
2879
|
+
const columnId = entry instanceof Y.Map ? entry.get("id") : entry?.id;
|
|
2880
|
+
if (!columnId || typeof columnId !== "string") {
|
|
2881
|
+
return null;
|
|
2882
|
+
}
|
|
2883
|
+
const columnDef = lookup.colById.get(columnId) || null;
|
|
2884
|
+
const hidden = entry instanceof Y.Map ? entry.get("hide") : entry?.hide;
|
|
2885
|
+
const width = entry instanceof Y.Map ? entry.get("width") : entry?.width;
|
|
2886
|
+
return {
|
|
2887
|
+
id: columnId,
|
|
2888
|
+
name: columnDef?.name || null,
|
|
2889
|
+
hidden: hidden === true,
|
|
2890
|
+
width: typeof width === "number" ? width : null,
|
|
2891
|
+
};
|
|
2892
|
+
})
|
|
2893
|
+
.filter((entry) => entry !== null);
|
|
2894
|
+
views.push({
|
|
2895
|
+
id: String(id),
|
|
2896
|
+
name: String((view instanceof Y.Map ? view.get("name") : view?.name) || ""),
|
|
2897
|
+
mode: String((view instanceof Y.Map ? view.get("mode") : view?.mode) || ""),
|
|
2898
|
+
columns,
|
|
2899
|
+
columnIds: columns.map(column => column.id),
|
|
2900
|
+
groupBy: groupByRaw
|
|
2901
|
+
? {
|
|
2902
|
+
columnId: typeof groupByRaw?.columnId === "string" ? groupByRaw.columnId : null,
|
|
2903
|
+
name: typeof groupByRaw?.name === "string" ? groupByRaw.name : null,
|
|
2904
|
+
type: typeof groupByRaw?.type === "string" ? groupByRaw.type : null,
|
|
2905
|
+
}
|
|
2906
|
+
: null,
|
|
2907
|
+
header: {
|
|
2908
|
+
titleColumn: typeof headerRaw?.titleColumn === "string" ? headerRaw.titleColumn : null,
|
|
2909
|
+
iconColumn: typeof headerRaw?.iconColumn === "string" ? headerRaw.iconColumn : null,
|
|
2910
|
+
},
|
|
2911
|
+
});
|
|
2912
|
+
});
|
|
2913
|
+
return views;
|
|
2914
|
+
}
|
|
2915
|
+
function isTitleAliasKey(value) {
|
|
2916
|
+
return value.trim().toLowerCase() === "title";
|
|
2917
|
+
}
|
|
2918
|
+
function buildDatabaseColumnLookup(columnDefs) {
|
|
2919
|
+
const colById = new Map();
|
|
2920
|
+
const colByName = new Map();
|
|
2921
|
+
const colByNameLower = new Map();
|
|
2922
|
+
let titleCol = null;
|
|
2923
|
+
for (const col of columnDefs) {
|
|
2924
|
+
colById.set(col.id, col);
|
|
2925
|
+
if (col.name) {
|
|
2926
|
+
colByName.set(col.name, col);
|
|
2927
|
+
colByNameLower.set(col.name.trim().toLowerCase(), col);
|
|
2928
|
+
}
|
|
2929
|
+
if (!titleCol && col.type === "title") {
|
|
2930
|
+
titleCol = col;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return { columnDefs, colById, colByName, colByNameLower, titleCol };
|
|
2934
|
+
}
|
|
2935
|
+
function findDatabaseColumn(key, lookup) {
|
|
2936
|
+
return lookup.colByName.get(key)
|
|
2937
|
+
|| lookup.colById.get(key)
|
|
2938
|
+
|| lookup.colByNameLower.get(key.trim().toLowerCase())
|
|
2939
|
+
|| null;
|
|
2940
|
+
}
|
|
2941
|
+
function availableDatabaseColumns(lookup) {
|
|
2942
|
+
return ["title", ...lookup.columnDefs.map(col => col.name || col.id)].join(", ");
|
|
2943
|
+
}
|
|
2944
|
+
function getDatabaseRowIds(dbBlock) {
|
|
2945
|
+
return childIdsFrom(dbBlock.get("sys:children"));
|
|
2946
|
+
}
|
|
2947
|
+
function readDatabaseRowTitle(rowBlock) {
|
|
2948
|
+
return asText(rowBlock.get("prop:text"));
|
|
2949
|
+
}
|
|
2950
|
+
function resolveDatabaseTitleValue(cells, lookup) {
|
|
2951
|
+
if (lookup.titleCol) {
|
|
2952
|
+
const value = cells[lookup.titleCol.name] ?? cells[lookup.titleCol.id];
|
|
2953
|
+
if (value !== undefined) {
|
|
2954
|
+
return String(value ?? "");
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
for (const [key, value] of Object.entries(cells)) {
|
|
2958
|
+
if (isTitleAliasKey(key)) {
|
|
2959
|
+
return String(value ?? "");
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
const namedTitleColumn = lookup.colByNameLower.get("title");
|
|
2963
|
+
if (namedTitleColumn) {
|
|
2964
|
+
const value = cells[namedTitleColumn.name] ?? cells[namedTitleColumn.id];
|
|
2965
|
+
if (value !== undefined) {
|
|
2966
|
+
return String(value ?? "");
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return "";
|
|
2970
|
+
}
|
|
2971
|
+
function ensureDatabaseRowCells(cellsMap, rowBlockId) {
|
|
2972
|
+
const existing = cellsMap.get(rowBlockId);
|
|
2973
|
+
if (existing instanceof Y.Map) {
|
|
2974
|
+
return existing;
|
|
2975
|
+
}
|
|
2976
|
+
const rowCells = new Y.Map();
|
|
2977
|
+
cellsMap.set(rowBlockId, rowCells);
|
|
2978
|
+
return rowCells;
|
|
2979
|
+
}
|
|
2980
|
+
function getDatabaseRowBlock(blocks, databaseBlockId, rowBlockId) {
|
|
2981
|
+
const rowBlock = findBlockById(blocks, rowBlockId);
|
|
2982
|
+
if (!rowBlock) {
|
|
2983
|
+
throw new Error(`Row block '${rowBlockId}' not found`);
|
|
2984
|
+
}
|
|
2985
|
+
if (rowBlock.get("sys:parent") !== databaseBlockId) {
|
|
2986
|
+
throw new Error(`Row block '${rowBlockId}' does not belong to database '${databaseBlockId}'`);
|
|
2987
|
+
}
|
|
2988
|
+
if (rowBlock.get("sys:flavour") !== "affine:paragraph") {
|
|
2989
|
+
throw new Error(`Row block '${rowBlockId}' is not a database row paragraph`);
|
|
2990
|
+
}
|
|
2991
|
+
return rowBlock;
|
|
2992
|
+
}
|
|
2993
|
+
function databaseArrayValues(value) {
|
|
2994
|
+
if (value instanceof Y.Array) {
|
|
2995
|
+
const entries = [];
|
|
2996
|
+
value.forEach(entry => {
|
|
2997
|
+
entries.push(entry);
|
|
2998
|
+
});
|
|
2999
|
+
return entries;
|
|
3000
|
+
}
|
|
3001
|
+
if (Array.isArray(value)) {
|
|
3002
|
+
return value;
|
|
3003
|
+
}
|
|
3004
|
+
return [];
|
|
3005
|
+
}
|
|
2769
3006
|
/** Find or create a select option for a column, mutating the column's data in place */
|
|
2770
|
-
function resolveSelectOptionId(col, valueText) {
|
|
3007
|
+
function resolveSelectOptionId(col, valueText, createOption = true) {
|
|
2771
3008
|
// Try exact match first
|
|
2772
3009
|
const existing = col.options.find(o => o.value === valueText);
|
|
2773
3010
|
if (existing)
|
|
2774
3011
|
return existing.id;
|
|
3012
|
+
if (!createOption) {
|
|
3013
|
+
throw new Error(`Column "${col.name}": option "${valueText}" not found`);
|
|
3014
|
+
}
|
|
2775
3015
|
// Create new option
|
|
2776
3016
|
const newId = generateId();
|
|
2777
3017
|
const colorIdx = col.options.length % SELECT_COLORS.length;
|
|
@@ -2798,43 +3038,172 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2798
3038
|
}
|
|
2799
3039
|
return newId;
|
|
2800
3040
|
}
|
|
3041
|
+
function decodeDatabaseCellValue(col, cellEntry) {
|
|
3042
|
+
const rawValue = cellEntry instanceof Y.Map ? cellEntry.get("value") : cellEntry?.value;
|
|
3043
|
+
const base = {
|
|
3044
|
+
columnId: col.id,
|
|
3045
|
+
type: col.type,
|
|
3046
|
+
};
|
|
3047
|
+
switch (col.type) {
|
|
3048
|
+
case "rich-text":
|
|
3049
|
+
case "title":
|
|
3050
|
+
return { ...base, value: richTextValueToString(rawValue) || null };
|
|
3051
|
+
case "select": {
|
|
3052
|
+
const optionId = asStringOrNull(rawValue);
|
|
3053
|
+
const option = col.options.find(entry => entry.id === optionId) || null;
|
|
3054
|
+
return {
|
|
3055
|
+
...base,
|
|
3056
|
+
value: option?.value ?? optionId ?? null,
|
|
3057
|
+
optionId: optionId ?? null,
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
case "multi-select": {
|
|
3061
|
+
const optionIds = databaseArrayValues(rawValue).map(entry => String(entry));
|
|
3062
|
+
const values = optionIds.map(optionId => col.options.find(entry => entry.id === optionId)?.value ?? optionId);
|
|
3063
|
+
return {
|
|
3064
|
+
...base,
|
|
3065
|
+
value: values,
|
|
3066
|
+
optionIds,
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
case "number": {
|
|
3070
|
+
const numericValue = typeof rawValue === "number" ? rawValue : Number(rawValue);
|
|
3071
|
+
return {
|
|
3072
|
+
...base,
|
|
3073
|
+
value: Number.isFinite(numericValue) ? numericValue : null,
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
case "checkbox":
|
|
3077
|
+
return { ...base, value: typeof rawValue === "boolean" ? rawValue : !!rawValue };
|
|
3078
|
+
case "date": {
|
|
3079
|
+
const numericValue = typeof rawValue === "number" ? rawValue : Number(rawValue);
|
|
3080
|
+
return {
|
|
3081
|
+
...base,
|
|
3082
|
+
value: Number.isFinite(numericValue) ? numericValue : null,
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
case "link":
|
|
3086
|
+
return { ...base, value: rawValue == null ? null : String(rawValue) };
|
|
3087
|
+
default:
|
|
3088
|
+
return {
|
|
3089
|
+
...base,
|
|
3090
|
+
value: typeof rawValue === "string" || rawValue instanceof Y.Text || Array.isArray(rawValue)
|
|
3091
|
+
? richTextValueToString(rawValue)
|
|
3092
|
+
: rawValue ?? null,
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
function writeDatabaseCellValue(rowCells, col, value, createOption) {
|
|
3097
|
+
const cellValue = new Y.Map();
|
|
3098
|
+
cellValue.set("columnId", col.id);
|
|
3099
|
+
switch (col.type) {
|
|
3100
|
+
case "rich-text":
|
|
3101
|
+
case "title":
|
|
3102
|
+
cellValue.set("value", makeText(String(value ?? "")));
|
|
3103
|
+
break;
|
|
3104
|
+
case "number": {
|
|
3105
|
+
const num = Number(value);
|
|
3106
|
+
if (Number.isNaN(num)) {
|
|
3107
|
+
throw new Error(`Column "${col.name}": expected a number, got ${JSON.stringify(value)}`);
|
|
3108
|
+
}
|
|
3109
|
+
cellValue.set("value", num);
|
|
3110
|
+
break;
|
|
3111
|
+
}
|
|
3112
|
+
case "checkbox": {
|
|
3113
|
+
let bool;
|
|
3114
|
+
if (typeof value === "boolean") {
|
|
3115
|
+
bool = value;
|
|
3116
|
+
}
|
|
3117
|
+
else if (typeof value === "string") {
|
|
3118
|
+
const lower = value.toLowerCase().trim();
|
|
3119
|
+
bool = lower === "true" || lower === "1" || lower === "yes";
|
|
3120
|
+
}
|
|
3121
|
+
else {
|
|
3122
|
+
bool = !!value;
|
|
3123
|
+
}
|
|
3124
|
+
cellValue.set("value", bool);
|
|
3125
|
+
break;
|
|
3126
|
+
}
|
|
3127
|
+
case "select":
|
|
3128
|
+
cellValue.set("value", resolveSelectOptionId(col, String(value ?? ""), createOption));
|
|
3129
|
+
break;
|
|
3130
|
+
case "multi-select": {
|
|
3131
|
+
const labels = Array.isArray(value) ? value.map(String) : [String(value ?? "")];
|
|
3132
|
+
const optionIds = new Y.Array();
|
|
3133
|
+
optionIds.push(labels.map(label => resolveSelectOptionId(col, label, createOption)));
|
|
3134
|
+
cellValue.set("value", optionIds);
|
|
3135
|
+
break;
|
|
3136
|
+
}
|
|
3137
|
+
case "date": {
|
|
3138
|
+
const numericValue = typeof value === "number"
|
|
3139
|
+
? value
|
|
3140
|
+
: Number.isNaN(Number(value)) ? Date.parse(String(value)) : Number(value);
|
|
3141
|
+
if (!Number.isFinite(numericValue)) {
|
|
3142
|
+
throw new Error(`Column "${col.name}": expected a timestamp-compatible value, got ${JSON.stringify(value)}`);
|
|
3143
|
+
}
|
|
3144
|
+
cellValue.set("value", numericValue);
|
|
3145
|
+
break;
|
|
3146
|
+
}
|
|
3147
|
+
case "link":
|
|
3148
|
+
cellValue.set("value", String(value ?? ""));
|
|
3149
|
+
break;
|
|
3150
|
+
default:
|
|
3151
|
+
if (typeof value === "string") {
|
|
3152
|
+
cellValue.set("value", makeText(value));
|
|
3153
|
+
}
|
|
3154
|
+
else {
|
|
3155
|
+
cellValue.set("value", value);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
rowCells.set(col.id, cellValue);
|
|
3159
|
+
}
|
|
3160
|
+
async function loadDatabaseDocContext(workspaceId, docId, databaseBlockId) {
|
|
3161
|
+
const { endpoint, cookie, bearer } = await getCookieAndEndpoint();
|
|
3162
|
+
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
3163
|
+
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
3164
|
+
await joinWorkspace(socket, workspaceId);
|
|
3165
|
+
const doc = new Y.Doc();
|
|
3166
|
+
const snapshot = await loadDoc(socket, workspaceId, docId);
|
|
3167
|
+
if (!snapshot.missing) {
|
|
3168
|
+
socket.disconnect();
|
|
3169
|
+
throw new Error("Document not found");
|
|
3170
|
+
}
|
|
3171
|
+
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
3172
|
+
const prevSV = Y.encodeStateVector(doc);
|
|
3173
|
+
const blocks = doc.getMap("blocks");
|
|
3174
|
+
const dbBlock = findBlockById(blocks, databaseBlockId);
|
|
3175
|
+
if (!dbBlock) {
|
|
3176
|
+
socket.disconnect();
|
|
3177
|
+
throw new Error(`Database block '${databaseBlockId}' not found`);
|
|
3178
|
+
}
|
|
3179
|
+
const dbFlavour = dbBlock.get("sys:flavour");
|
|
3180
|
+
if (dbFlavour !== "affine:database") {
|
|
3181
|
+
socket.disconnect();
|
|
3182
|
+
throw new Error(`Block '${databaseBlockId}' is not a database (flavour: ${dbFlavour})`);
|
|
3183
|
+
}
|
|
3184
|
+
const cellsMap = dbBlock.get("prop:cells");
|
|
3185
|
+
if (!(cellsMap instanceof Y.Map)) {
|
|
3186
|
+
socket.disconnect();
|
|
3187
|
+
throw new Error("Database block has no cells map");
|
|
3188
|
+
}
|
|
3189
|
+
const lookup = buildDatabaseColumnLookup(readColumnDefs(dbBlock));
|
|
3190
|
+
return {
|
|
3191
|
+
socket,
|
|
3192
|
+
doc,
|
|
3193
|
+
prevSV,
|
|
3194
|
+
blocks,
|
|
3195
|
+
dbBlock,
|
|
3196
|
+
cellsMap,
|
|
3197
|
+
...lookup,
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
2801
3200
|
// ADD DATABASE ROW
|
|
2802
3201
|
const addDatabaseRowHandler = async (parsed) => {
|
|
2803
3202
|
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
2804
3203
|
if (!workspaceId)
|
|
2805
3204
|
throw new Error("workspaceId is required");
|
|
2806
|
-
const
|
|
2807
|
-
const wsUrl = wsUrlFromGraphQLEndpoint(endpoint);
|
|
2808
|
-
const socket = await connectWorkspaceSocket(wsUrl, cookie, bearer);
|
|
3205
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
2809
3206
|
try {
|
|
2810
|
-
await joinWorkspace(socket, workspaceId);
|
|
2811
|
-
const doc = new Y.Doc();
|
|
2812
|
-
const snapshot = await loadDoc(socket, workspaceId, parsed.docId);
|
|
2813
|
-
if (!snapshot.missing)
|
|
2814
|
-
throw new Error("Document not found");
|
|
2815
|
-
Y.applyUpdate(doc, Buffer.from(snapshot.missing, "base64"));
|
|
2816
|
-
const prevSV = Y.encodeStateVector(doc);
|
|
2817
|
-
const blocks = doc.getMap("blocks");
|
|
2818
|
-
// Find the database block
|
|
2819
|
-
const dbBlock = findBlockById(blocks, parsed.databaseBlockId);
|
|
2820
|
-
if (!dbBlock)
|
|
2821
|
-
throw new Error(`Database block '${parsed.databaseBlockId}' not found`);
|
|
2822
|
-
const dbFlavour = dbBlock.get("sys:flavour");
|
|
2823
|
-
if (dbFlavour !== "affine:database") {
|
|
2824
|
-
throw new Error(`Block '${parsed.databaseBlockId}' is not a database (flavour: ${dbFlavour})`);
|
|
2825
|
-
}
|
|
2826
|
-
// Read column definitions with select options
|
|
2827
|
-
const columnDefs = readColumnDefs(dbBlock);
|
|
2828
|
-
// Build lookups
|
|
2829
|
-
const colByName = new Map();
|
|
2830
|
-
const colById = new Map();
|
|
2831
|
-
for (const col of columnDefs) {
|
|
2832
|
-
if (col.name)
|
|
2833
|
-
colByName.set(col.name, col);
|
|
2834
|
-
colById.set(col.id, col);
|
|
2835
|
-
}
|
|
2836
|
-
// Identify the title column (first column, or type === "title")
|
|
2837
|
-
const titleCol = columnDefs.find(c => c.type === "title") || null;
|
|
2838
3207
|
// Create a new paragraph block as the row child of the database
|
|
2839
3208
|
const rowBlockId = generateId();
|
|
2840
3209
|
const rowBlock = new Y.Map();
|
|
@@ -2842,104 +3211,26 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2842
3211
|
rowBlock.set("sys:parent", parsed.databaseBlockId);
|
|
2843
3212
|
rowBlock.set("sys:children", new Y.Array());
|
|
2844
3213
|
rowBlock.set("prop:type", "text");
|
|
2845
|
-
|
|
2846
|
-
const titleValue = titleCol ? parsed.cells[titleCol.name] ?? parsed.cells[titleCol.id] ?? "" : "";
|
|
3214
|
+
const titleValue = resolveDatabaseTitleValue(parsed.cells, ctx);
|
|
2847
3215
|
rowBlock.set("prop:text", makeText(String(titleValue)));
|
|
2848
|
-
blocks.set(rowBlockId, rowBlock);
|
|
3216
|
+
ctx.blocks.set(rowBlockId, rowBlock);
|
|
2849
3217
|
// Add row block to database's children
|
|
2850
|
-
const dbChildren = ensureChildrenArray(dbBlock);
|
|
3218
|
+
const dbChildren = ensureChildrenArray(ctx.dbBlock);
|
|
2851
3219
|
dbChildren.push([rowBlockId]);
|
|
2852
|
-
// Populate cells map on the database block
|
|
2853
|
-
const cellsMap = dbBlock.get("prop:cells");
|
|
2854
|
-
if (!(cellsMap instanceof Y.Map)) {
|
|
2855
|
-
throw new Error("Database block has no cells map");
|
|
2856
|
-
}
|
|
2857
3220
|
// Create row cell map
|
|
2858
|
-
const rowCells =
|
|
3221
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, rowBlockId);
|
|
2859
3222
|
for (const [key, value] of Object.entries(parsed.cells)) {
|
|
2860
|
-
|
|
2861
|
-
const col = colByName.get(key) || colById.get(key);
|
|
3223
|
+
const col = findDatabaseColumn(key, ctx);
|
|
2862
3224
|
if (!col) {
|
|
2863
|
-
|
|
2864
|
-
}
|
|
2865
|
-
// Skip the title column — already stored on the paragraph block
|
|
2866
|
-
if (titleCol && col.id === titleCol.id)
|
|
2867
|
-
continue;
|
|
2868
|
-
// Create cell value based on column type
|
|
2869
|
-
const cellValue = new Y.Map();
|
|
2870
|
-
cellValue.set("columnId", col.id);
|
|
2871
|
-
switch (col.type) {
|
|
2872
|
-
case "rich-text": {
|
|
2873
|
-
const yText = makeText(String(value ?? ""));
|
|
2874
|
-
cellValue.set("value", yText);
|
|
2875
|
-
break;
|
|
2876
|
-
}
|
|
2877
|
-
case "title": {
|
|
2878
|
-
// Handled above on the paragraph block; skip
|
|
3225
|
+
if (isTitleAliasKey(key)) {
|
|
2879
3226
|
continue;
|
|
2880
3227
|
}
|
|
2881
|
-
|
|
2882
|
-
const num = Number(value);
|
|
2883
|
-
if (Number.isNaN(num)) {
|
|
2884
|
-
throw new Error(`Column "${col.name}": expected a number, got ${JSON.stringify(value)}`);
|
|
2885
|
-
}
|
|
2886
|
-
cellValue.set("value", num);
|
|
2887
|
-
break;
|
|
2888
|
-
}
|
|
2889
|
-
case "checkbox": {
|
|
2890
|
-
let bool;
|
|
2891
|
-
if (typeof value === "boolean") {
|
|
2892
|
-
bool = value;
|
|
2893
|
-
}
|
|
2894
|
-
else if (typeof value === "string") {
|
|
2895
|
-
const lower = value.toLowerCase().trim();
|
|
2896
|
-
bool = lower === "true" || lower === "1" || lower === "yes";
|
|
2897
|
-
}
|
|
2898
|
-
else {
|
|
2899
|
-
bool = !!value;
|
|
2900
|
-
}
|
|
2901
|
-
cellValue.set("value", bool);
|
|
2902
|
-
break;
|
|
2903
|
-
}
|
|
2904
|
-
case "select": {
|
|
2905
|
-
// Resolve option ID by label text; auto-create if needed
|
|
2906
|
-
const optionId = resolveSelectOptionId(col, String(value ?? ""));
|
|
2907
|
-
cellValue.set("value", optionId);
|
|
2908
|
-
break;
|
|
2909
|
-
}
|
|
2910
|
-
case "multi-select": {
|
|
2911
|
-
const labels = Array.isArray(value) ? value.map(String) : [String(value ?? "")];
|
|
2912
|
-
const ids = labels.map(lbl => resolveSelectOptionId(col, lbl));
|
|
2913
|
-
cellValue.set("value", ids);
|
|
2914
|
-
break;
|
|
2915
|
-
}
|
|
2916
|
-
case "date": {
|
|
2917
|
-
const ts = Number(value);
|
|
2918
|
-
if (Number.isNaN(ts)) {
|
|
2919
|
-
throw new Error(`Column "${col.name}": expected a timestamp number, got ${JSON.stringify(value)}`);
|
|
2920
|
-
}
|
|
2921
|
-
cellValue.set("value", ts);
|
|
2922
|
-
break;
|
|
2923
|
-
}
|
|
2924
|
-
case "link": {
|
|
2925
|
-
cellValue.set("value", String(value ?? ""));
|
|
2926
|
-
break;
|
|
2927
|
-
}
|
|
2928
|
-
default: {
|
|
2929
|
-
// Fallback: store as rich-text
|
|
2930
|
-
if (typeof value === "string") {
|
|
2931
|
-
cellValue.set("value", makeText(value));
|
|
2932
|
-
}
|
|
2933
|
-
else {
|
|
2934
|
-
cellValue.set("value", value);
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
3228
|
+
throw new Error(`Column '${key}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
2937
3229
|
}
|
|
2938
|
-
rowCells
|
|
3230
|
+
writeDatabaseCellValue(rowCells, col, value, true);
|
|
2939
3231
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
await pushDocUpdate(socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3232
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3233
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
2943
3234
|
return text({
|
|
2944
3235
|
added: true,
|
|
2945
3236
|
rowBlockId,
|
|
@@ -2948,7 +3239,7 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2948
3239
|
});
|
|
2949
3240
|
}
|
|
2950
3241
|
finally {
|
|
2951
|
-
socket.disconnect();
|
|
3242
|
+
ctx.socket.disconnect();
|
|
2952
3243
|
}
|
|
2953
3244
|
};
|
|
2954
3245
|
server.registerTool("add_database_row", {
|
|
@@ -2961,6 +3252,199 @@ export function registerDocTools(server, gql, defaults) {
|
|
|
2961
3252
|
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)."),
|
|
2962
3253
|
},
|
|
2963
3254
|
}, addDatabaseRowHandler);
|
|
3255
|
+
const readDatabaseCellsHandler = async (parsed) => {
|
|
3256
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3257
|
+
if (!workspaceId)
|
|
3258
|
+
throw new Error("workspaceId is required");
|
|
3259
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3260
|
+
try {
|
|
3261
|
+
const requestedRows = parsed.rowBlockIds?.length
|
|
3262
|
+
? parsed.rowBlockIds
|
|
3263
|
+
: getDatabaseRowIds(ctx.dbBlock);
|
|
3264
|
+
const requestedColumns = parsed.columns?.length
|
|
3265
|
+
? parsed.columns.map(columnKey => {
|
|
3266
|
+
const col = findDatabaseColumn(columnKey, ctx);
|
|
3267
|
+
if (!col) {
|
|
3268
|
+
throw new Error(`Column '${columnKey}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3269
|
+
}
|
|
3270
|
+
return col;
|
|
3271
|
+
})
|
|
3272
|
+
: ctx.columnDefs;
|
|
3273
|
+
const requestedColumnIds = new Set(requestedColumns.map(col => col.id));
|
|
3274
|
+
const rows = requestedRows.map(rowBlockId => {
|
|
3275
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, rowBlockId);
|
|
3276
|
+
const title = readDatabaseRowTitle(rowBlock) || null;
|
|
3277
|
+
const rowCells = ctx.cellsMap.get(rowBlockId);
|
|
3278
|
+
const cells = {};
|
|
3279
|
+
if (rowCells instanceof Y.Map) {
|
|
3280
|
+
for (const col of ctx.columnDefs) {
|
|
3281
|
+
if (ctx.titleCol && col.id === ctx.titleCol.id) {
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
if (!requestedColumnIds.has(col.id)) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
const cellEntry = rowCells.get(col.id);
|
|
3288
|
+
if (cellEntry === undefined) {
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
cells[col.name || col.id] = decodeDatabaseCellValue(col, cellEntry);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
return {
|
|
3295
|
+
rowBlockId,
|
|
3296
|
+
title,
|
|
3297
|
+
cells,
|
|
3298
|
+
};
|
|
3299
|
+
});
|
|
3300
|
+
return text({ rows });
|
|
3301
|
+
}
|
|
3302
|
+
finally {
|
|
3303
|
+
ctx.socket.disconnect();
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3306
|
+
server.registerTool("read_database_cells", {
|
|
3307
|
+
title: "Read Database Cells",
|
|
3308
|
+
description: "Read row titles and database cell values from an AFFiNE database block.",
|
|
3309
|
+
inputSchema: {
|
|
3310
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3311
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3312
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3313
|
+
rowBlockIds: z.array(z.string().min(1)).optional().describe("Optional row block ID filter. Omit to return all rows."),
|
|
3314
|
+
columns: z.array(z.string().min(1)).optional().describe("Optional column name or ID filter."),
|
|
3315
|
+
},
|
|
3316
|
+
}, readDatabaseCellsHandler);
|
|
3317
|
+
const readDatabaseColumnsHandler = async (parsed) => {
|
|
3318
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3319
|
+
if (!workspaceId)
|
|
3320
|
+
throw new Error("workspaceId is required");
|
|
3321
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3322
|
+
try {
|
|
3323
|
+
const columns = ctx.columnDefs.map(col => ({
|
|
3324
|
+
id: col.id,
|
|
3325
|
+
name: col.name || null,
|
|
3326
|
+
type: col.type,
|
|
3327
|
+
options: col.options,
|
|
3328
|
+
}));
|
|
3329
|
+
return text({
|
|
3330
|
+
databaseBlockId: parsed.databaseBlockId,
|
|
3331
|
+
title: richTextValueToString(ctx.dbBlock.get("prop:title")) || null,
|
|
3332
|
+
rowCount: getDatabaseRowIds(ctx.dbBlock).length,
|
|
3333
|
+
columnCount: columns.length,
|
|
3334
|
+
titleColumnId: ctx.titleCol?.id || null,
|
|
3335
|
+
columns,
|
|
3336
|
+
views: readDatabaseViewDefs(ctx.dbBlock, ctx),
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
finally {
|
|
3340
|
+
ctx.socket.disconnect();
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
server.registerTool("read_database_columns", {
|
|
3344
|
+
title: "Read Database Columns",
|
|
3345
|
+
description: "Read schema metadata for an AFFiNE database block, including columns, select options, and view column mappings. Useful for empty databases before any rows exist.",
|
|
3346
|
+
inputSchema: {
|
|
3347
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3348
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3349
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3350
|
+
},
|
|
3351
|
+
}, readDatabaseColumnsHandler);
|
|
3352
|
+
const updateDatabaseCellHandler = async (parsed) => {
|
|
3353
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3354
|
+
if (!workspaceId)
|
|
3355
|
+
throw new Error("workspaceId is required");
|
|
3356
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3357
|
+
try {
|
|
3358
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, parsed.rowBlockId);
|
|
3359
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, parsed.rowBlockId);
|
|
3360
|
+
const col = findDatabaseColumn(parsed.column, ctx);
|
|
3361
|
+
if (!col) {
|
|
3362
|
+
if (!isTitleAliasKey(parsed.column)) {
|
|
3363
|
+
throw new Error(`Column '${parsed.column}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
else {
|
|
3367
|
+
writeDatabaseCellValue(rowCells, col, parsed.value, parsed.createOption ?? true);
|
|
3368
|
+
}
|
|
3369
|
+
if (isTitleAliasKey(parsed.column) || (col && (col.type === "title" || isTitleAliasKey(col.name)))) {
|
|
3370
|
+
rowBlock.set("prop:text", makeText(String(parsed.value ?? "")));
|
|
3371
|
+
}
|
|
3372
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3373
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3374
|
+
return text({
|
|
3375
|
+
updated: true,
|
|
3376
|
+
rowBlockId: parsed.rowBlockId,
|
|
3377
|
+
column: parsed.column,
|
|
3378
|
+
value: parsed.value ?? null,
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
finally {
|
|
3382
|
+
ctx.socket.disconnect();
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
server.registerTool("update_database_cell", {
|
|
3386
|
+
title: "Update Database Cell",
|
|
3387
|
+
description: "Update a single cell on an existing AFFiNE database row. Use `title` to update the row title shown in Kanban card headers.",
|
|
3388
|
+
inputSchema: {
|
|
3389
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3390
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3391
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3392
|
+
rowBlockId: z.string().min(1).describe("Row paragraph block ID"),
|
|
3393
|
+
column: z.string().min(1).describe("Column name or ID. Use `title` for the built-in row title."),
|
|
3394
|
+
value: z.unknown().describe("New cell value"),
|
|
3395
|
+
createOption: z.boolean().optional().describe("For select and multi-select columns, create the option label if it does not exist (default true)"),
|
|
3396
|
+
},
|
|
3397
|
+
}, updateDatabaseCellHandler);
|
|
3398
|
+
const updateDatabaseRowHandler = async (parsed) => {
|
|
3399
|
+
const workspaceId = parsed.workspaceId || defaults.workspaceId;
|
|
3400
|
+
if (!workspaceId)
|
|
3401
|
+
throw new Error("workspaceId is required");
|
|
3402
|
+
const ctx = await loadDatabaseDocContext(workspaceId, parsed.docId, parsed.databaseBlockId);
|
|
3403
|
+
try {
|
|
3404
|
+
const rowBlock = getDatabaseRowBlock(ctx.blocks, parsed.databaseBlockId, parsed.rowBlockId);
|
|
3405
|
+
const rowCells = ensureDatabaseRowCells(ctx.cellsMap, parsed.rowBlockId);
|
|
3406
|
+
let titleValue = null;
|
|
3407
|
+
for (const [key, value] of Object.entries(parsed.cells)) {
|
|
3408
|
+
const col = findDatabaseColumn(key, ctx);
|
|
3409
|
+
if (!col) {
|
|
3410
|
+
if (isTitleAliasKey(key)) {
|
|
3411
|
+
titleValue = String(value ?? "");
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
throw new Error(`Column '${key}' not found. Available columns: ${availableDatabaseColumns(ctx)}`);
|
|
3415
|
+
}
|
|
3416
|
+
writeDatabaseCellValue(rowCells, col, value, parsed.createOption ?? true);
|
|
3417
|
+
if (col.type === "title" || isTitleAliasKey(col.name)) {
|
|
3418
|
+
titleValue = String(value ?? "");
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (titleValue !== null) {
|
|
3422
|
+
rowBlock.set("prop:text", makeText(titleValue));
|
|
3423
|
+
}
|
|
3424
|
+
const delta = Y.encodeStateAsUpdate(ctx.doc, ctx.prevSV);
|
|
3425
|
+
await pushDocUpdate(ctx.socket, workspaceId, parsed.docId, Buffer.from(delta).toString("base64"));
|
|
3426
|
+
return text({
|
|
3427
|
+
updated: true,
|
|
3428
|
+
rowBlockId: parsed.rowBlockId,
|
|
3429
|
+
cellCount: Object.keys(parsed.cells).length,
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
finally {
|
|
3433
|
+
ctx.socket.disconnect();
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
server.registerTool("update_database_row", {
|
|
3437
|
+
title: "Update Database Row",
|
|
3438
|
+
description: "Batch update multiple cells on an existing AFFiNE database row. Include `title` in the cells map to update the Kanban row title.",
|
|
3439
|
+
inputSchema: {
|
|
3440
|
+
workspaceId: z.string().optional().describe("Workspace ID (optional if default set)"),
|
|
3441
|
+
docId: DocId.describe("Document ID containing the database"),
|
|
3442
|
+
databaseBlockId: z.string().min(1).describe("Block ID of the affine:database block"),
|
|
3443
|
+
rowBlockId: z.string().min(1).describe("Row paragraph block ID"),
|
|
3444
|
+
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."),
|
|
3445
|
+
createOption: z.boolean().optional().describe("For select and multi-select columns, create the option label if it does not exist (default true)"),
|
|
3446
|
+
},
|
|
3447
|
+
}, updateDatabaseRowHandler);
|
|
2964
3448
|
// ADD DATABASE COLUMN
|
|
2965
3449
|
const addDatabaseColumnHandler = async (parsed) => {
|
|
2966
3450
|
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.9.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,12 +33,20 @@
|
|
|
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
|
-
"test:comprehensive": "
|
|
38
|
+
"test:comprehensive": "bash tests/run-comprehensive.sh",
|
|
39
|
+
"test:comprehensive:raw": "node test-comprehensive.mjs",
|
|
38
40
|
"test:e2e": "bash tests/run-e2e.sh",
|
|
39
41
|
"test:db-create": "node tests/test-database-creation.mjs",
|
|
42
|
+
"test:db-cells": "node tests/test-database-cells.mjs",
|
|
43
|
+
"test:db-schema": "node tests/test-database-schema.mjs",
|
|
44
|
+
"test:data-view": "node tests/test-data-view.mjs",
|
|
45
|
+
"test:data-view-ui": "npx playwright test tests/playwright/verify-data-view.pw.ts --config tests/playwright/playwright.config.ts",
|
|
40
46
|
"test:bearer": "node tests/test-bearer-auth.mjs",
|
|
47
|
+
"test:supporting-tools": "node tests/test-supporting-tools.mjs",
|
|
41
48
|
"test:tag-visibility": "node tests/test-tag-visibility.mjs",
|
|
49
|
+
"test:markdown-roundtrip": "node tests/test-markdown-roundtrip.mjs",
|
|
42
50
|
"test:playwright": "npx playwright test --config tests/playwright/playwright.config.ts",
|
|
43
51
|
"pack:check": "npm pack --dry-run",
|
|
44
52
|
"ci": "npm run build && npm run test:tool-manifest && npm run pack:check",
|