@daf-sdk/runtime 1.0.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.
@@ -0,0 +1,1221 @@
1
+ // src/runtime/integrations/notion.ts
2
+ import { Client } from "@notionhq/client";
3
+ var NOTION_CLIENT_ID = process.env.NOTION_CLIENT_ID || "";
4
+ var NOTION_CLIENT_SECRET = process.env.NOTION_CLIENT_SECRET || "";
5
+ var NOTION_REDIRECT_URI = process.env.NOTION_REDIRECT_URI || "http://localhost:3000/api/integrations/notion/callback";
6
+ function getNotionAuthUrl(userId) {
7
+ const authUrl = new URL("https://api.notion.com/v1/oauth/authorize");
8
+ authUrl.searchParams.set("client_id", NOTION_CLIENT_ID);
9
+ authUrl.searchParams.set("response_type", "code");
10
+ authUrl.searchParams.set("owner", "user");
11
+ authUrl.searchParams.set("redirect_uri", NOTION_REDIRECT_URI);
12
+ authUrl.searchParams.set("state", userId);
13
+ return authUrl.toString();
14
+ }
15
+ async function exchangeNotionCode(code) {
16
+ const encoded = Buffer.from(`${NOTION_CLIENT_ID}:${NOTION_CLIENT_SECRET}`).toString("base64");
17
+ const response = await fetch("https://api.notion.com/v1/oauth/token", {
18
+ method: "POST",
19
+ headers: {
20
+ "Authorization": `Basic ${encoded}`,
21
+ "Content-Type": "application/json"
22
+ },
23
+ body: JSON.stringify({
24
+ grant_type: "authorization_code",
25
+ code,
26
+ redirect_uri: NOTION_REDIRECT_URI
27
+ })
28
+ });
29
+ if (!response.ok) {
30
+ const error = await response.text();
31
+ throw new Error(`Failed to exchange Notion code: ${error}`);
32
+ }
33
+ const data = await response.json();
34
+ return {
35
+ accessToken: data.access_token,
36
+ workspaceId: data.workspace_id,
37
+ workspaceName: data.workspace_name,
38
+ botId: data.bot_id
39
+ };
40
+ }
41
+ async function getNotionAccessToken(userId, adapter) {
42
+ const integration = await adapter.getNotionIntegration(userId);
43
+ if (!integration) {
44
+ throw new Error("Notion integration not found. Please connect your Notion account.");
45
+ }
46
+ return integration.accessToken;
47
+ }
48
+ function extractPageIdFromUrl(url) {
49
+ const urlWithoutQuery = url.split("?")[0];
50
+ const match = urlWithoutQuery.match(/([a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i);
51
+ if (match) {
52
+ const id = match[1].replace(/-/g, "");
53
+ return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
54
+ }
55
+ return null;
56
+ }
57
+ function extractDatabaseIdFromUrl(url) {
58
+ return extractPageIdFromUrl(url);
59
+ }
60
+ async function getNotionPageTitle(pageId, accessToken) {
61
+ try {
62
+ const notion = new Client({ auth: accessToken });
63
+ const page = await notion.pages.retrieve({ page_id: pageId });
64
+ if (page.properties) {
65
+ for (const prop of Object.values(page.properties)) {
66
+ if (prop.type === "title" && prop.title) {
67
+ const titleArray = prop.title;
68
+ if (titleArray.length > 0 && titleArray[0].plain_text) {
69
+ return titleArray[0].plain_text;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return null;
75
+ } catch (error) {
76
+ console.error("Error fetching Notion page title:", error);
77
+ return null;
78
+ }
79
+ }
80
+ async function readNotionPage(pageId, accessToken) {
81
+ const notion = new Client({ auth: accessToken });
82
+ async function getBlockContent(blockId, indentLevel = 0) {
83
+ const blocks = await notion.blocks.children.list({
84
+ block_id: blockId
85
+ });
86
+ let content = "";
87
+ const indent = " ".repeat(indentLevel);
88
+ for (const block of blocks.results) {
89
+ if ("type" in block) {
90
+ const blockType = block.type;
91
+ if (blockType === "paragraph" && "paragraph" in block) {
92
+ const paragraph = block.paragraph;
93
+ if (paragraph.rich_text && paragraph.rich_text.length > 0) {
94
+ content += indent + extractTextFromRichText(paragraph.rich_text) + "\n";
95
+ } else {
96
+ content += "\n";
97
+ }
98
+ } else if (blockType === "heading_1" && "heading_1" in block) {
99
+ const heading = block.heading_1;
100
+ if (heading.rich_text) {
101
+ content += indent + "# " + extractTextFromRichText(heading.rich_text) + "\n";
102
+ }
103
+ } else if (blockType === "heading_2" && "heading_2" in block) {
104
+ const heading = block.heading_2;
105
+ if (heading.rich_text) {
106
+ content += indent + "## " + extractTextFromRichText(heading.rich_text) + "\n";
107
+ }
108
+ } else if (blockType === "heading_3" && "heading_3" in block) {
109
+ const heading = block.heading_3;
110
+ if (heading.rich_text) {
111
+ content += indent + "### " + extractTextFromRichText(heading.rich_text) + "\n";
112
+ }
113
+ } else if (blockType === "bulleted_list_item" && "bulleted_list_item" in block) {
114
+ const item = block.bulleted_list_item;
115
+ if (item.rich_text) {
116
+ content += indent + "\u2022 " + extractTextFromRichText(item.rich_text) + "\n";
117
+ }
118
+ if (block.has_children && "id" in block) {
119
+ content += await getBlockContent(block.id, indentLevel + 1);
120
+ }
121
+ } else if (blockType === "numbered_list_item" && "numbered_list_item" in block) {
122
+ const item = block.numbered_list_item;
123
+ if (item.rich_text) {
124
+ content += indent + "1. " + extractTextFromRichText(item.rich_text) + "\n";
125
+ }
126
+ if (block.has_children && "id" in block) {
127
+ content += await getBlockContent(block.id, indentLevel + 1);
128
+ }
129
+ } else if (blockType === "to_do" && "to_do" in block) {
130
+ const todo = block.to_do;
131
+ const checkbox = todo.checked ? "[x]" : "[ ]";
132
+ if (todo.rich_text) {
133
+ content += indent + `- ${checkbox} ` + extractTextFromRichText(todo.rich_text) + "\n";
134
+ }
135
+ if (block.has_children && "id" in block) {
136
+ content += await getBlockContent(block.id, indentLevel + 1);
137
+ }
138
+ } else if (blockType === "toggle" && "toggle" in block) {
139
+ const toggle = block.toggle;
140
+ if (toggle.rich_text) {
141
+ content += indent + "\u25B8 " + extractTextFromRichText(toggle.rich_text) + "\n";
142
+ }
143
+ if (block.has_children && "id" in block) {
144
+ content += await getBlockContent(block.id, indentLevel + 1);
145
+ }
146
+ } else if (blockType === "code" && "code" in block) {
147
+ const code = block.code;
148
+ if (code.rich_text) {
149
+ content += indent + "```\n" + extractTextFromRichText(code.rich_text) + "\n```\n";
150
+ }
151
+ } else if (blockType === "quote" && "quote" in block) {
152
+ const quote = block.quote;
153
+ if (quote.rich_text) {
154
+ content += indent + "> " + extractTextFromRichText(quote.rich_text) + "\n";
155
+ }
156
+ } else if (blockType === "callout" && "callout" in block) {
157
+ const callout = block.callout;
158
+ if (callout.rich_text) {
159
+ const icon = callout.icon && "emoji" in callout.icon ? callout.icon.emoji : "\u{1F4CC}";
160
+ content += indent + `${icon} ` + extractTextFromRichText(callout.rich_text) + "\n";
161
+ }
162
+ } else if (blockType === "divider") {
163
+ content += indent + "---\n";
164
+ } else if (blockType === "child_page" && "child_page" in block && "id" in block) {
165
+ const childPage = block.child_page;
166
+ const title = childPage.title || "Untitled";
167
+ const url = `https://www.notion.so/${block.id.replace(/-/g, "")}`;
168
+ content += indent + `${title} (${url})
169
+ `;
170
+ } else if (blockType === "child_database" && "child_database" in block && "id" in block) {
171
+ const childDb = block.child_database;
172
+ const title = childDb.title || "Untitled Database";
173
+ const url = `https://www.notion.so/${block.id.replace(/-/g, "")}`;
174
+ content += indent + `[Database] ${title} (${url})
175
+ `;
176
+ }
177
+ }
178
+ }
179
+ return content;
180
+ }
181
+ return await getBlockContent(pageId);
182
+ }
183
+ function extractTextFromRichText(richText) {
184
+ return richText.map((text) => {
185
+ if (text.type === "mention" && text.mention) {
186
+ const plainText = text.plain_text;
187
+ if (text.mention.type === "page") {
188
+ const pageId = text.mention.page?.id;
189
+ if (pageId) {
190
+ const url = `https://www.notion.so/${pageId.replace(/-/g, "")}`;
191
+ return `${plainText} (${url})`;
192
+ }
193
+ }
194
+ return plainText;
195
+ }
196
+ return text.plain_text;
197
+ }).join("");
198
+ }
199
+ async function writeNotionPage(pageId, content, accessToken) {
200
+ const notion = new Client({ auth: accessToken });
201
+ const existingBlocks = await notion.blocks.children.list({
202
+ block_id: pageId
203
+ });
204
+ for (const block of existingBlocks.results) {
205
+ if ("id" in block) {
206
+ await notion.blocks.delete({
207
+ block_id: block.id
208
+ });
209
+ }
210
+ }
211
+ const lines = content.split("\n").filter((line) => line.trim());
212
+ const blocks = [];
213
+ for (const line of lines) {
214
+ if (line.startsWith("# ")) {
215
+ blocks.push({
216
+ object: "block",
217
+ type: "heading_1",
218
+ heading_1: {
219
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
220
+ }
221
+ });
222
+ } else if (line.startsWith("## ")) {
223
+ blocks.push({
224
+ object: "block",
225
+ type: "heading_2",
226
+ heading_2: {
227
+ rich_text: [{ type: "text", text: { content: line.slice(3) } }]
228
+ }
229
+ });
230
+ } else if (line.startsWith("### ")) {
231
+ blocks.push({
232
+ object: "block",
233
+ type: "heading_3",
234
+ heading_3: {
235
+ rich_text: [{ type: "text", text: { content: line.slice(4) } }]
236
+ }
237
+ });
238
+ } else if (line.startsWith("\u2022 ") || line.startsWith("- ")) {
239
+ blocks.push({
240
+ object: "block",
241
+ type: "bulleted_list_item",
242
+ bulleted_list_item: {
243
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
244
+ }
245
+ });
246
+ } else if (line.match(/^\d+\.\s/)) {
247
+ blocks.push({
248
+ object: "block",
249
+ type: "numbered_list_item",
250
+ numbered_list_item: {
251
+ rich_text: [{ type: "text", text: { content: line.replace(/^\d+\.\s/, "") } }]
252
+ }
253
+ });
254
+ } else {
255
+ blocks.push({
256
+ object: "block",
257
+ type: "paragraph",
258
+ paragraph: {
259
+ rich_text: [{ type: "text", text: { content: line } }]
260
+ }
261
+ });
262
+ }
263
+ }
264
+ const NOTION_BLOCK_BATCH_LIMIT = 100;
265
+ for (let i = 0; i < blocks.length; i += NOTION_BLOCK_BATCH_LIMIT) {
266
+ await notion.blocks.children.append({
267
+ block_id: pageId,
268
+ children: blocks.slice(i, i + NOTION_BLOCK_BATCH_LIMIT)
269
+ });
270
+ }
271
+ }
272
+ async function selectiveUpdateNotionPage(pageId, content, startBlockIndex, endBlockIndex, accessToken) {
273
+ const notion = new Client({ auth: accessToken });
274
+ const response = await notion.blocks.children.list({
275
+ block_id: pageId
276
+ });
277
+ const allBlocks = response.results;
278
+ if (startBlockIndex < 0 || endBlockIndex > allBlocks.length) {
279
+ throw new Error(`Invalid block range: startBlockIndex=${startBlockIndex}, endBlockIndex=${endBlockIndex}, total blocks=${allBlocks.length}`);
280
+ }
281
+ for (let i = startBlockIndex; i < endBlockIndex && i < allBlocks.length; i++) {
282
+ await notion.blocks.delete({
283
+ block_id: allBlocks[i].id
284
+ });
285
+ }
286
+ const lines = content.split("\n").filter((line) => line.trim());
287
+ const newBlocks = [];
288
+ for (const line of lines) {
289
+ if (line.startsWith("# ")) {
290
+ newBlocks.push({
291
+ object: "block",
292
+ type: "heading_1",
293
+ heading_1: {
294
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
295
+ }
296
+ });
297
+ } else if (line.startsWith("## ")) {
298
+ newBlocks.push({
299
+ object: "block",
300
+ type: "heading_2",
301
+ heading_2: {
302
+ rich_text: [{ type: "text", text: { content: line.slice(3) } }]
303
+ }
304
+ });
305
+ } else if (line.startsWith("### ")) {
306
+ newBlocks.push({
307
+ object: "block",
308
+ type: "heading_3",
309
+ heading_3: {
310
+ rich_text: [{ type: "text", text: { content: line.slice(4) } }]
311
+ }
312
+ });
313
+ } else if (line.match(/^[•\-]\s/)) {
314
+ newBlocks.push({
315
+ object: "block",
316
+ type: "bulleted_list_item",
317
+ bulleted_list_item: {
318
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
319
+ }
320
+ });
321
+ } else if (line.match(/^\d+\.\s/)) {
322
+ newBlocks.push({
323
+ object: "block",
324
+ type: "numbered_list_item",
325
+ numbered_list_item: {
326
+ rich_text: [{ type: "text", text: { content: line.replace(/^\d+\.\s/, "") } }]
327
+ }
328
+ });
329
+ } else {
330
+ newBlocks.push({
331
+ object: "block",
332
+ type: "paragraph",
333
+ paragraph: {
334
+ rich_text: [{ type: "text", text: { content: line } }]
335
+ }
336
+ });
337
+ }
338
+ }
339
+ if (newBlocks.length > 0) {
340
+ const blocksToMoveToEnd = allBlocks.slice(endBlockIndex);
341
+ const savedBlocks = [];
342
+ for (const block of blocksToMoveToEnd) {
343
+ savedBlocks.push(block);
344
+ await notion.blocks.delete({
345
+ block_id: block.id
346
+ });
347
+ }
348
+ await notion.blocks.children.append({
349
+ block_id: pageId,
350
+ children: newBlocks
351
+ });
352
+ if (savedBlocks.length > 0) {
353
+ const blocksToReinsert = savedBlocks.map((block) => {
354
+ const blockType = block.type;
355
+ const blockData = block[blockType];
356
+ return {
357
+ object: "block",
358
+ type: blockType,
359
+ [blockType]: blockData
360
+ };
361
+ });
362
+ await notion.blocks.children.append({
363
+ block_id: pageId,
364
+ children: blocksToReinsert
365
+ });
366
+ }
367
+ }
368
+ }
369
+ async function appendNotionPage(pageId, content, accessToken) {
370
+ const notion = new Client({ auth: accessToken });
371
+ const lines = content.split("\n").filter((line) => line.trim());
372
+ const blocks = [];
373
+ for (const line of lines) {
374
+ if (line.startsWith("# ")) {
375
+ blocks.push({
376
+ object: "block",
377
+ type: "heading_1",
378
+ heading_1: {
379
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
380
+ }
381
+ });
382
+ } else if (line.startsWith("## ")) {
383
+ blocks.push({
384
+ object: "block",
385
+ type: "heading_2",
386
+ heading_2: {
387
+ rich_text: [{ type: "text", text: { content: line.slice(3) } }]
388
+ }
389
+ });
390
+ } else if (line.startsWith("### ")) {
391
+ blocks.push({
392
+ object: "block",
393
+ type: "heading_3",
394
+ heading_3: {
395
+ rich_text: [{ type: "text", text: { content: line.slice(4) } }]
396
+ }
397
+ });
398
+ } else {
399
+ blocks.push({
400
+ object: "block",
401
+ type: "paragraph",
402
+ paragraph: {
403
+ rich_text: [{ type: "text", text: { content: line } }]
404
+ }
405
+ });
406
+ }
407
+ }
408
+ const NOTION_BLOCK_BATCH_LIMIT = 100;
409
+ for (let i = 0; i < blocks.length; i += NOTION_BLOCK_BATCH_LIMIT) {
410
+ await notion.blocks.children.append({
411
+ block_id: pageId,
412
+ children: blocks.slice(i, i + NOTION_BLOCK_BATCH_LIMIT)
413
+ });
414
+ }
415
+ }
416
+ async function deleteNotionPage(pageId, accessToken) {
417
+ const notion = new Client({ auth: accessToken });
418
+ try {
419
+ await notion.pages.update({
420
+ page_id: pageId,
421
+ archived: true
422
+ });
423
+ } catch (error) {
424
+ if (error?.message?.includes("workspace level pages")) {
425
+ throw new Error("This Notion page cannot be removed via API because it's at the workspace level. Please move it to a parent page first, or remove it manually in Notion.");
426
+ }
427
+ throw error;
428
+ }
429
+ }
430
+ async function renameNotionPage(pageId, newTitle, accessToken) {
431
+ const notion = new Client({ auth: accessToken });
432
+ await notion.pages.update({
433
+ page_id: pageId,
434
+ properties: {
435
+ title: {
436
+ title: [
437
+ {
438
+ text: {
439
+ content: newTitle
440
+ }
441
+ }
442
+ ]
443
+ }
444
+ }
445
+ });
446
+ }
447
+ async function duplicateNotionPage(pageId, newTitle, accessToken) {
448
+ const notion = new Client({ auth: accessToken });
449
+ const originalPage = await notion.pages.retrieve({ page_id: pageId });
450
+ if (!("parent" in originalPage)) {
451
+ throw new Error("Cannot access parent information for this page");
452
+ }
453
+ const blocks = await notion.blocks.children.list({
454
+ block_id: pageId
455
+ });
456
+ const parent = originalPage.parent;
457
+ let validParent;
458
+ if ("page_id" in parent) {
459
+ validParent = { page_id: parent.page_id };
460
+ } else if ("database_id" in parent) {
461
+ validParent = { database_id: parent.database_id };
462
+ } else if ("workspace" in parent) {
463
+ validParent = { workspace: true };
464
+ } else {
465
+ throw new Error("Cannot duplicate page: parent type not supported");
466
+ }
467
+ const newPage = await notion.pages.create({
468
+ parent: validParent,
469
+ properties: {
470
+ title: {
471
+ title: [
472
+ {
473
+ text: {
474
+ content: newTitle
475
+ }
476
+ }
477
+ ]
478
+ }
479
+ }
480
+ });
481
+ if (blocks.results.length > 0) {
482
+ const blockChildren = blocks.results.map((block) => {
483
+ const blockCopy = {
484
+ type: block.type
485
+ };
486
+ if (block.type && block[block.type]) {
487
+ blockCopy[block.type] = block[block.type];
488
+ }
489
+ return blockCopy;
490
+ });
491
+ try {
492
+ await notion.blocks.children.append({
493
+ block_id: newPage.id,
494
+ children: blockChildren
495
+ });
496
+ } catch (error) {
497
+ console.error("Error copying blocks to new page:", error);
498
+ }
499
+ }
500
+ return {
501
+ pageId: newPage.id,
502
+ url: "url" in newPage ? newPage.url : `https://www.notion.so/${newPage.id.replace(/-/g, "")}`
503
+ };
504
+ }
505
+ async function createNotionSubpage(parentPageId, pageTitle, accessToken, initialContent) {
506
+ const notion = new Client({ auth: accessToken });
507
+ const pageData = {
508
+ properties: {
509
+ title: {
510
+ title: [
511
+ {
512
+ text: {
513
+ content: pageTitle
514
+ }
515
+ }
516
+ ]
517
+ }
518
+ }
519
+ };
520
+ if (parentPageId) {
521
+ pageData.parent = {
522
+ page_id: parentPageId
523
+ };
524
+ } else {
525
+ pageData.parent = {
526
+ workspace: true
527
+ };
528
+ }
529
+ const newPage = await notion.pages.create(pageData);
530
+ if (initialContent) {
531
+ const lines = initialContent.split("\n").filter((line) => line.trim());
532
+ const blocks = [];
533
+ const NOTION_TEXT_LIMIT = 2e3;
534
+ const chunkText = (text) => {
535
+ const chunks = [];
536
+ for (let i = 0; i < text.length; i += NOTION_TEXT_LIMIT) {
537
+ chunks.push(text.slice(i, i + NOTION_TEXT_LIMIT));
538
+ }
539
+ return chunks.length > 0 ? chunks : [""];
540
+ };
541
+ for (const line of lines) {
542
+ if (line.startsWith("# ")) {
543
+ blocks.push({
544
+ object: "block",
545
+ type: "heading_1",
546
+ heading_1: {
547
+ rich_text: [{ type: "text", text: { content: line.slice(2).slice(0, NOTION_TEXT_LIMIT) } }]
548
+ }
549
+ });
550
+ } else if (line.startsWith("## ")) {
551
+ blocks.push({
552
+ object: "block",
553
+ type: "heading_2",
554
+ heading_2: {
555
+ rich_text: [{ type: "text", text: { content: line.slice(3).slice(0, NOTION_TEXT_LIMIT) } }]
556
+ }
557
+ });
558
+ } else if (line.startsWith("### ")) {
559
+ blocks.push({
560
+ object: "block",
561
+ type: "heading_3",
562
+ heading_3: {
563
+ rich_text: [{ type: "text", text: { content: line.slice(4).slice(0, NOTION_TEXT_LIMIT) } }]
564
+ }
565
+ });
566
+ } else if (line.startsWith("\u2022 ") || line.startsWith("- ")) {
567
+ blocks.push({
568
+ object: "block",
569
+ type: "bulleted_list_item",
570
+ bulleted_list_item: {
571
+ rich_text: [{ type: "text", text: { content: line.slice(2).slice(0, NOTION_TEXT_LIMIT) } }]
572
+ }
573
+ });
574
+ } else {
575
+ for (const chunk of chunkText(line)) {
576
+ blocks.push({
577
+ object: "block",
578
+ type: "paragraph",
579
+ paragraph: {
580
+ rich_text: [{ type: "text", text: { content: chunk } }]
581
+ }
582
+ });
583
+ }
584
+ }
585
+ }
586
+ const NOTION_BLOCK_BATCH_LIMIT = 100;
587
+ for (let i = 0; i < blocks.length; i += NOTION_BLOCK_BATCH_LIMIT) {
588
+ await notion.blocks.children.append({
589
+ block_id: newPage.id,
590
+ children: blocks.slice(i, i + NOTION_BLOCK_BATCH_LIMIT)
591
+ });
592
+ }
593
+ }
594
+ return {
595
+ pageId: newPage.id,
596
+ url: "url" in newPage ? newPage.url : `https://www.notion.so/${newPage.id.replace(/-/g, "")}`
597
+ };
598
+ }
599
+ async function listNotionPageChildren(pageId, accessToken) {
600
+ const notion = new Client({ auth: accessToken });
601
+ const blocks = await notion.blocks.children.list({
602
+ block_id: pageId
603
+ });
604
+ const children = [];
605
+ for (const block of blocks.results) {
606
+ if (!("type" in block)) continue;
607
+ if (block.type === "child_page" && "child_page" in block) {
608
+ const childPage = block.child_page;
609
+ children.push({
610
+ id: block.id,
611
+ title: childPage.title || "Untitled",
612
+ type: "page",
613
+ url: `https://www.notion.so/${block.id.replace(/-/g, "")}`
614
+ });
615
+ } else if (block.type === "child_database" && "child_database" in block) {
616
+ const childDb = block.child_database;
617
+ children.push({
618
+ id: block.id,
619
+ title: childDb.title || "Untitled Database",
620
+ type: "database",
621
+ url: `https://www.notion.so/${block.id.replace(/-/g, "")}`
622
+ });
623
+ }
624
+ }
625
+ return children;
626
+ }
627
+ async function getNotionDatabase(databaseId, accessToken) {
628
+ const notion = new Client({ auth: accessToken });
629
+ const database = await notion.databases.retrieve({ database_id: databaseId });
630
+ let title = "Untitled Database";
631
+ if (database.title && database.title.length > 0) {
632
+ title = database.title[0].plain_text || "Untitled Database";
633
+ }
634
+ return {
635
+ title,
636
+ properties: database.properties || {}
637
+ };
638
+ }
639
+ async function createNotionDatabasePage(databaseId, properties, accessToken, content) {
640
+ const notion = new Client({ auth: accessToken });
641
+ const database = await notion.databases.retrieve({ database_id: databaseId });
642
+ let dbProperties;
643
+ if (database.data_sources && database.data_sources.length > 0) {
644
+ const dataSourceId = database.data_sources[0].id;
645
+ console.log(`Fetching data source properties for data source ID: ${dataSourceId}`);
646
+ try {
647
+ const dataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId });
648
+ dbProperties = dataSource.properties;
649
+ } catch (error) {
650
+ console.error("Error fetching data source:", error);
651
+ throw new Error(`Failed to fetch data source properties: ${error instanceof Error ? error.message : "Unknown error"}`);
652
+ }
653
+ } else if (database.properties) {
654
+ dbProperties = database.properties;
655
+ }
656
+ if (!dbProperties || Object.keys(dbProperties).length === 0) {
657
+ console.error("Database retrieval result:", JSON.stringify(database, null, 2));
658
+ throw new Error(`Database has no properties defined. Database ID: ${databaseId}`);
659
+ }
660
+ const formattedProperties = {};
661
+ for (const [propName, propValue] of Object.entries(properties)) {
662
+ const propSchema = dbProperties[propName];
663
+ if (!propSchema) {
664
+ console.warn(`Property "${propName}" not found in database schema, skipping`);
665
+ continue;
666
+ }
667
+ const propType = propSchema.type;
668
+ switch (propType) {
669
+ case "title":
670
+ formattedProperties[propName] = {
671
+ title: [{ text: { content: String(propValue) } }]
672
+ };
673
+ break;
674
+ case "rich_text":
675
+ formattedProperties[propName] = {
676
+ rich_text: [{ text: { content: String(propValue) } }]
677
+ };
678
+ break;
679
+ case "number":
680
+ formattedProperties[propName] = {
681
+ number: Number(propValue)
682
+ };
683
+ break;
684
+ case "select":
685
+ formattedProperties[propName] = {
686
+ select: { name: String(propValue) }
687
+ };
688
+ break;
689
+ case "multi_select":
690
+ const values = Array.isArray(propValue) ? propValue : [propValue];
691
+ formattedProperties[propName] = {
692
+ multi_select: values.map((v) => ({ name: String(v) }))
693
+ };
694
+ break;
695
+ case "date":
696
+ formattedProperties[propName] = {
697
+ date: { start: String(propValue) }
698
+ };
699
+ break;
700
+ case "checkbox":
701
+ formattedProperties[propName] = {
702
+ checkbox: Boolean(propValue)
703
+ };
704
+ break;
705
+ case "url":
706
+ formattedProperties[propName] = {
707
+ url: String(propValue)
708
+ };
709
+ break;
710
+ case "email":
711
+ formattedProperties[propName] = {
712
+ email: String(propValue)
713
+ };
714
+ break;
715
+ case "phone_number":
716
+ formattedProperties[propName] = {
717
+ phone_number: String(propValue)
718
+ };
719
+ break;
720
+ default:
721
+ console.warn(`Property type "${propType}" not supported for property "${propName}", skipping`);
722
+ }
723
+ }
724
+ if (Object.keys(formattedProperties).length === 0) {
725
+ const availableProps = Object.keys(dbProperties).join(", ");
726
+ const requestedProps = Object.keys(properties).join(", ");
727
+ throw new Error(
728
+ `No valid properties found. Requested properties: ${requestedProps}. Available properties in database: ${availableProps}`
729
+ );
730
+ }
731
+ const newPage = await notion.pages.create({
732
+ parent: { database_id: databaseId },
733
+ properties: formattedProperties
734
+ });
735
+ if (content) {
736
+ const lines = content.split("\n").filter((line) => line.trim());
737
+ const blocks = [];
738
+ for (const line of lines) {
739
+ if (line.startsWith("# ")) {
740
+ blocks.push({
741
+ object: "block",
742
+ type: "heading_1",
743
+ heading_1: {
744
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
745
+ }
746
+ });
747
+ } else if (line.startsWith("## ")) {
748
+ blocks.push({
749
+ object: "block",
750
+ type: "heading_2",
751
+ heading_2: {
752
+ rich_text: [{ type: "text", text: { content: line.slice(3) } }]
753
+ }
754
+ });
755
+ } else if (line.startsWith("### ")) {
756
+ blocks.push({
757
+ object: "block",
758
+ type: "heading_3",
759
+ heading_3: {
760
+ rich_text: [{ type: "text", text: { content: line.slice(4) } }]
761
+ }
762
+ });
763
+ } else if (line.startsWith("\u2022 ") || line.startsWith("- ")) {
764
+ blocks.push({
765
+ object: "block",
766
+ type: "bulleted_list_item",
767
+ bulleted_list_item: {
768
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
769
+ }
770
+ });
771
+ } else {
772
+ blocks.push({
773
+ object: "block",
774
+ type: "paragraph",
775
+ paragraph: {
776
+ rich_text: [{ type: "text", text: { content: line } }]
777
+ }
778
+ });
779
+ }
780
+ }
781
+ const NOTION_BLOCK_BATCH_LIMIT = 100;
782
+ for (let i = 0; i < blocks.length; i += NOTION_BLOCK_BATCH_LIMIT) {
783
+ await notion.blocks.children.append({
784
+ block_id: newPage.id,
785
+ children: blocks.slice(i, i + NOTION_BLOCK_BATCH_LIMIT)
786
+ });
787
+ }
788
+ }
789
+ return {
790
+ pageId: newPage.id,
791
+ url: "url" in newPage ? newPage.url : `https://www.notion.so/${newPage.id.replace(/-/g, "")}`
792
+ };
793
+ }
794
+ async function queryNotionDatabase(databaseId, accessToken, filter, sorts) {
795
+ const notion = new Client({ auth: accessToken });
796
+ const response = await notion.databases.query({
797
+ database_id: databaseId,
798
+ filter,
799
+ sorts
800
+ });
801
+ return response.results;
802
+ }
803
+ async function createNotionDatabase(parentPageId, title, properties, accessToken) {
804
+ const notion = new Client({ auth: accessToken });
805
+ const parent = parentPageId ? {
806
+ type: "page_id",
807
+ page_id: parentPageId
808
+ } : {
809
+ type: "workspace",
810
+ workspace: true
811
+ };
812
+ const database = await notion.databases.create({
813
+ parent,
814
+ title: [
815
+ {
816
+ type: "text",
817
+ text: {
818
+ content: title
819
+ }
820
+ }
821
+ ],
822
+ properties
823
+ });
824
+ return {
825
+ databaseId: database.id,
826
+ url: "url" in database ? database.url : `https://www.notion.so/${database.id.replace(/-/g, "")}`
827
+ };
828
+ }
829
+ async function readNotionDatabase(databaseId, accessToken) {
830
+ const notion = new Client({ auth: accessToken });
831
+ const database = await notion.databases.retrieve({ database_id: databaseId });
832
+ let title = "Untitled Database";
833
+ if (database.title && database.title.length > 0) {
834
+ title = database.title[0].plain_text || "Untitled Database";
835
+ }
836
+ let dbProperties = {};
837
+ if (database.data_sources && database.data_sources.length > 0) {
838
+ const dataSourceId = database.data_sources[0].id;
839
+ try {
840
+ const dataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId });
841
+ dbProperties = dataSource.properties || {};
842
+ } catch (error) {
843
+ console.error("Error fetching data source:", error);
844
+ }
845
+ } else if (database.properties) {
846
+ dbProperties = database.properties;
847
+ }
848
+ return {
849
+ title,
850
+ properties: dbProperties,
851
+ url: database.url || `https://www.notion.so/${databaseId.replace(/-/g, "")}`
852
+ };
853
+ }
854
+ async function updateNotionDatabase(databaseId, accessToken, title, properties) {
855
+ const notion = new Client({ auth: accessToken });
856
+ if (title) {
857
+ await notion.databases.update({
858
+ database_id: databaseId,
859
+ title: [
860
+ {
861
+ type: "text",
862
+ text: {
863
+ content: title
864
+ }
865
+ }
866
+ ]
867
+ });
868
+ }
869
+ if (properties && Object.keys(properties).length > 0) {
870
+ const database = await notion.databases.retrieve({ database_id: databaseId });
871
+ if (database.data_sources && database.data_sources.length > 0) {
872
+ const dataSourceId = database.data_sources[0].id;
873
+ await notion.dataSources.update({
874
+ data_source_id: dataSourceId,
875
+ properties
876
+ });
877
+ } else {
878
+ await notion.databases.update({
879
+ database_id: databaseId,
880
+ properties
881
+ });
882
+ }
883
+ }
884
+ }
885
+ async function deleteNotionDatabase(databaseId, accessToken) {
886
+ const notion = new Client({ auth: accessToken });
887
+ try {
888
+ const result = await notion.databases.update({
889
+ database_id: databaseId,
890
+ in_trash: true
891
+ // Databases use in_trash instead of archived
892
+ });
893
+ console.log("[deleteNotionDatabase] Database moved to trash successfully:", {
894
+ id: result.id,
895
+ in_trash: result.in_trash
896
+ });
897
+ } catch (error) {
898
+ if (error.message && error.message.includes("workspace level")) {
899
+ throw new Error("Cannot archive workspace-level databases via API. Only databases inside pages can be archived. Please archive this database manually in Notion or move it inside a page first.");
900
+ }
901
+ throw new Error(`Failed to move database to trash: ${error instanceof Error ? error.message : "Unknown error"}`);
902
+ }
903
+ }
904
+ async function duplicateNotionDatabase(databaseId, accessToken, newDatabaseName) {
905
+ const notion = new Client({ auth: accessToken });
906
+ const originalDatabase = await notion.databases.retrieve({ database_id: databaseId });
907
+ let originalTitle = "Untitled";
908
+ if (originalDatabase.title && Array.isArray(originalDatabase.title) && originalDatabase.title.length > 0) {
909
+ originalTitle = originalDatabase.title[0].plain_text || "Untitled";
910
+ }
911
+ const newTitle = newDatabaseName || `${originalTitle} (Copy)`;
912
+ let rawProperties = {};
913
+ if (originalDatabase.data_sources && originalDatabase.data_sources.length > 0) {
914
+ const dataSourceId = originalDatabase.data_sources[0].id;
915
+ const dataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId });
916
+ rawProperties = dataSource.properties;
917
+ } else if (originalDatabase.properties) {
918
+ rawProperties = originalDatabase.properties;
919
+ }
920
+ console.log("[duplicateDatabase] Raw properties retrieved:", JSON.stringify(rawProperties, null, 2));
921
+ const properties = {};
922
+ for (const [propName, propSchema] of Object.entries(rawProperties)) {
923
+ const schema = propSchema;
924
+ const propType = schema.type;
925
+ if (propType === "title") {
926
+ properties[propName] = { title: {} };
927
+ } else if (propType === "rich_text") {
928
+ properties[propName] = { rich_text: {} };
929
+ } else if (propType === "number") {
930
+ properties[propName] = { number: schema.number || {} };
931
+ } else if (propType === "select") {
932
+ properties[propName] = { select: schema.select || {} };
933
+ } else if (propType === "multi_select") {
934
+ properties[propName] = { multi_select: schema.multi_select || {} };
935
+ } else if (propType === "date") {
936
+ properties[propName] = { date: {} };
937
+ } else if (propType === "checkbox") {
938
+ properties[propName] = { checkbox: {} };
939
+ } else if (propType === "url") {
940
+ properties[propName] = { url: {} };
941
+ } else if (propType === "email") {
942
+ properties[propName] = { email: {} };
943
+ } else if (propType === "phone_number") {
944
+ properties[propName] = { phone_number: {} };
945
+ } else if (propType === "people") {
946
+ properties[propName] = { people: {} };
947
+ } else if (propType === "files") {
948
+ properties[propName] = { files: {} };
949
+ } else if (propType === "relation") {
950
+ properties[propName] = { relation: schema.relation || {} };
951
+ } else if (propType === "rollup") {
952
+ properties[propName] = { rollup: schema.rollup || {} };
953
+ } else if (propType === "formula") {
954
+ properties[propName] = { formula: schema.formula || {} };
955
+ } else if (propType === "status") {
956
+ properties[propName] = { status: schema.status || {} };
957
+ } else if (propType === "created_time") {
958
+ properties[propName] = { created_time: {} };
959
+ } else if (propType === "created_by") {
960
+ properties[propName] = { created_by: {} };
961
+ } else if (propType === "last_edited_time") {
962
+ properties[propName] = { last_edited_time: {} };
963
+ } else if (propType === "last_edited_by") {
964
+ properties[propName] = { last_edited_by: {} };
965
+ } else {
966
+ console.warn(`[duplicateDatabase] Unknown property type "${propType}" for property "${propName}", copying as-is`);
967
+ properties[propName] = { [propType]: schema[propType] || {} };
968
+ }
969
+ }
970
+ console.log("[duplicateDatabase] Cleaned properties for create:", JSON.stringify(properties, null, 2));
971
+ const parent = originalDatabase.parent;
972
+ console.log("[duplicateDatabase] Creating database with parent:", JSON.stringify(parent, null, 2));
973
+ console.log("[duplicateDatabase] Number of properties:", Object.keys(properties).length);
974
+ try {
975
+ const titleProperty = Object.entries(properties).find(([_, schema]) => schema.title);
976
+ const titlePropName = titleProperty ? titleProperty[0] : "Name";
977
+ const minimalProperties = titleProperty ? { [titlePropName]: titleProperty[1] } : { "Name": { "title": {} } };
978
+ console.log("[duplicateDatabase] Step 1: Creating with title property only");
979
+ const newDatabase = await notion.databases.create({
980
+ parent,
981
+ title: [{ type: "text", text: { content: newTitle } }],
982
+ properties: minimalProperties
983
+ });
984
+ console.log("[duplicateDatabase] Database created:", newDatabase.id);
985
+ const remainingProperties = Object.fromEntries(
986
+ Object.entries(properties).filter(([name, _]) => name !== titlePropName)
987
+ );
988
+ if (Object.keys(remainingProperties).length > 0) {
989
+ console.log("[duplicateDatabase] Step 2: Adding", Object.keys(remainingProperties).length, "remaining properties");
990
+ const createdDb = await notion.databases.retrieve({ database_id: newDatabase.id });
991
+ if (createdDb.data_sources && createdDb.data_sources.length > 0) {
992
+ const dataSourceId = createdDb.data_sources[0].id;
993
+ console.log("[duplicateDatabase] Using data source API to add properties to data source:", dataSourceId);
994
+ await notion.dataSources.update({
995
+ data_source_id: dataSourceId,
996
+ properties: remainingProperties
997
+ });
998
+ console.log("[duplicateDatabase] All properties added successfully via data source API");
999
+ } else {
1000
+ console.log("[duplicateDatabase] No data sources found, using legacy database update API");
1001
+ await notion.databases.update({
1002
+ database_id: newDatabase.id,
1003
+ properties: remainingProperties
1004
+ });
1005
+ }
1006
+ }
1007
+ const verifyDatabase = await notion.databases.retrieve({ database_id: newDatabase.id });
1008
+ let finalPropertyCount = 0;
1009
+ if (verifyDatabase.data_sources && verifyDatabase.data_sources.length > 0) {
1010
+ const dataSourceId = verifyDatabase.data_sources[0].id;
1011
+ const verifyDataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId });
1012
+ finalPropertyCount = Object.keys(verifyDataSource.properties || {}).length;
1013
+ console.log("[duplicateDatabase] Final verification - properties:", finalPropertyCount, "/", Object.keys(properties).length);
1014
+ } else if (verifyDatabase.properties) {
1015
+ finalPropertyCount = Object.keys(verifyDatabase.properties).length;
1016
+ console.log("[duplicateDatabase] Final verification - properties:", finalPropertyCount, "/", Object.keys(properties).length);
1017
+ }
1018
+ return {
1019
+ databaseId: newDatabase.id,
1020
+ title: newTitle,
1021
+ url: "url" in newDatabase ? newDatabase.url : `https://www.notion.so/${newDatabase.id.replace(/-/g, "")}`
1022
+ };
1023
+ } catch (error) {
1024
+ console.error("[duplicateDatabase] Error:", error);
1025
+ throw error;
1026
+ }
1027
+ }
1028
+ async function updateNotionDatabasePage(pageId, accessToken, properties, content) {
1029
+ const notion = new Client({ auth: accessToken });
1030
+ if (properties && Object.keys(properties).length > 0) {
1031
+ const page = await notion.pages.retrieve({ page_id: pageId });
1032
+ if (!("parent" in page) || !("database_id" in page.parent)) {
1033
+ throw new Error("This page is not a database page");
1034
+ }
1035
+ const databaseId = page.parent.database_id;
1036
+ const database = await notion.databases.retrieve({ database_id: databaseId });
1037
+ let dbProperties;
1038
+ if (database.data_sources && database.data_sources.length > 0) {
1039
+ const dataSourceId = database.data_sources[0].id;
1040
+ console.log(`Fetching data source properties for data source ID: ${dataSourceId}`);
1041
+ try {
1042
+ const dataSource = await notion.dataSources.retrieve({ data_source_id: dataSourceId });
1043
+ dbProperties = dataSource.properties;
1044
+ } catch (error) {
1045
+ console.error("Error fetching data source:", error);
1046
+ throw new Error(`Failed to fetch data source properties: ${error instanceof Error ? error.message : "Unknown error"}`);
1047
+ }
1048
+ } else if (database.properties) {
1049
+ dbProperties = database.properties;
1050
+ }
1051
+ if (!dbProperties || Object.keys(dbProperties).length === 0) {
1052
+ throw new Error("Database has no properties defined");
1053
+ }
1054
+ const formattedProperties = {};
1055
+ for (const [propName, propValue] of Object.entries(properties)) {
1056
+ const propSchema = dbProperties[propName];
1057
+ if (!propSchema) {
1058
+ console.warn(`Property "${propName}" not found in database schema, skipping`);
1059
+ continue;
1060
+ }
1061
+ const propType = propSchema.type;
1062
+ switch (propType) {
1063
+ case "title":
1064
+ formattedProperties[propName] = {
1065
+ title: [{ text: { content: String(propValue) } }]
1066
+ };
1067
+ break;
1068
+ case "rich_text":
1069
+ formattedProperties[propName] = {
1070
+ rich_text: [{ text: { content: String(propValue) } }]
1071
+ };
1072
+ break;
1073
+ case "number":
1074
+ formattedProperties[propName] = {
1075
+ number: Number(propValue)
1076
+ };
1077
+ break;
1078
+ case "select":
1079
+ formattedProperties[propName] = {
1080
+ select: propValue === null ? null : { name: String(propValue) }
1081
+ };
1082
+ break;
1083
+ case "multi_select":
1084
+ const values = Array.isArray(propValue) ? propValue : [propValue];
1085
+ formattedProperties[propName] = {
1086
+ multi_select: values.map((v) => ({ name: String(v) }))
1087
+ };
1088
+ break;
1089
+ case "date":
1090
+ formattedProperties[propName] = {
1091
+ date: propValue === null ? null : { start: String(propValue) }
1092
+ };
1093
+ break;
1094
+ case "checkbox":
1095
+ formattedProperties[propName] = {
1096
+ checkbox: Boolean(propValue)
1097
+ };
1098
+ break;
1099
+ case "url":
1100
+ formattedProperties[propName] = {
1101
+ url: propValue === null ? null : String(propValue)
1102
+ };
1103
+ break;
1104
+ case "email":
1105
+ formattedProperties[propName] = {
1106
+ email: propValue === null ? null : String(propValue)
1107
+ };
1108
+ break;
1109
+ case "phone_number":
1110
+ formattedProperties[propName] = {
1111
+ phone_number: propValue === null ? null : String(propValue)
1112
+ };
1113
+ break;
1114
+ default:
1115
+ console.warn(`Property type "${propType}" not supported for property "${propName}", skipping`);
1116
+ }
1117
+ }
1118
+ if (Object.keys(formattedProperties).length === 0) {
1119
+ const availableProps = Object.keys(dbProperties).join(", ");
1120
+ const requestedProps = Object.keys(properties).join(", ");
1121
+ throw new Error(
1122
+ `No valid properties found to update. Requested properties: ${requestedProps}. Available properties in database: ${availableProps}`
1123
+ );
1124
+ }
1125
+ await notion.pages.update({
1126
+ page_id: pageId,
1127
+ properties: formattedProperties
1128
+ });
1129
+ }
1130
+ if (content !== void 0) {
1131
+ const existingBlocks = await notion.blocks.children.list({
1132
+ block_id: pageId
1133
+ });
1134
+ for (const block of existingBlocks.results) {
1135
+ if ("id" in block) {
1136
+ await notion.blocks.delete({
1137
+ block_id: block.id
1138
+ });
1139
+ }
1140
+ }
1141
+ const lines = content.split("\n").filter((line) => line.trim());
1142
+ const blocks = [];
1143
+ for (const line of lines) {
1144
+ if (line.startsWith("# ")) {
1145
+ blocks.push({
1146
+ object: "block",
1147
+ type: "heading_1",
1148
+ heading_1: {
1149
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
1150
+ }
1151
+ });
1152
+ } else if (line.startsWith("## ")) {
1153
+ blocks.push({
1154
+ object: "block",
1155
+ type: "heading_2",
1156
+ heading_2: {
1157
+ rich_text: [{ type: "text", text: { content: line.slice(3) } }]
1158
+ }
1159
+ });
1160
+ } else if (line.startsWith("### ")) {
1161
+ blocks.push({
1162
+ object: "block",
1163
+ type: "heading_3",
1164
+ heading_3: {
1165
+ rich_text: [{ type: "text", text: { content: line.slice(4) } }]
1166
+ }
1167
+ });
1168
+ } else if (line.startsWith("\u2022 ") || line.startsWith("- ")) {
1169
+ blocks.push({
1170
+ object: "block",
1171
+ type: "bulleted_list_item",
1172
+ bulleted_list_item: {
1173
+ rich_text: [{ type: "text", text: { content: line.slice(2) } }]
1174
+ }
1175
+ });
1176
+ } else {
1177
+ blocks.push({
1178
+ object: "block",
1179
+ type: "paragraph",
1180
+ paragraph: {
1181
+ rich_text: [{ type: "text", text: { content: line } }]
1182
+ }
1183
+ });
1184
+ }
1185
+ }
1186
+ const NOTION_BLOCK_BATCH_LIMIT = 100;
1187
+ for (let i = 0; i < blocks.length; i += NOTION_BLOCK_BATCH_LIMIT) {
1188
+ await notion.blocks.children.append({
1189
+ block_id: pageId,
1190
+ children: blocks.slice(i, i + NOTION_BLOCK_BATCH_LIMIT)
1191
+ });
1192
+ }
1193
+ }
1194
+ }
1195
+
1196
+ export {
1197
+ getNotionAuthUrl,
1198
+ exchangeNotionCode,
1199
+ getNotionAccessToken,
1200
+ extractPageIdFromUrl,
1201
+ extractDatabaseIdFromUrl,
1202
+ getNotionPageTitle,
1203
+ readNotionPage,
1204
+ writeNotionPage,
1205
+ selectiveUpdateNotionPage,
1206
+ appendNotionPage,
1207
+ deleteNotionPage,
1208
+ renameNotionPage,
1209
+ duplicateNotionPage,
1210
+ createNotionSubpage,
1211
+ listNotionPageChildren,
1212
+ getNotionDatabase,
1213
+ createNotionDatabasePage,
1214
+ queryNotionDatabase,
1215
+ createNotionDatabase,
1216
+ readNotionDatabase,
1217
+ updateNotionDatabase,
1218
+ deleteNotionDatabase,
1219
+ duplicateNotionDatabase,
1220
+ updateNotionDatabasePage
1221
+ };