@m2ai-mcp/notion-mcp 0.1.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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +374 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/blocks.d.ts +43 -0
- package/dist/tools/blocks.d.ts.map +1 -0
- package/dist/tools/blocks.js +124 -0
- package/dist/tools/blocks.js.map +1 -0
- package/dist/tools/databases.d.ts +71 -0
- package/dist/tools/databases.d.ts.map +1 -0
- package/dist/tools/databases.js +121 -0
- package/dist/tools/databases.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/pages.d.ts +72 -0
- package/dist/tools/pages.d.ts.map +1 -0
- package/dist/tools/pages.js +153 -0
- package/dist/tools/pages.js.map +1 -0
- package/dist/tools/search.d.ts +28 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +62 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/users.d.ts +33 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +51 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/utils/markdown-converter.d.ts +31 -0
- package/dist/utils/markdown-converter.d.ts.map +1 -0
- package/dist/utils/markdown-converter.js +355 -0
- package/dist/utils/markdown-converter.js.map +1 -0
- package/dist/utils/notion-client.d.ts +32 -0
- package/dist/utils/notion-client.d.ts.map +1 -0
- package/dist/utils/notion-client.js +111 -0
- package/dist/utils/notion-client.js.map +1 -0
- package/dist/utils/types.d.ts +212 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +18 -0
- package/dist/utils/types.js.map +1 -0
- package/jest.config.cjs +33 -0
- package/package.json +53 -0
- package/server.json +92 -0
- package/src/index.ts +435 -0
- package/src/tools/blocks.ts +184 -0
- package/src/tools/databases.ts +216 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/pages.ts +253 -0
- package/src/tools/search.ts +96 -0
- package/src/tools/users.ts +93 -0
- package/src/utils/markdown-converter.ts +408 -0
- package/src/utils/notion-client.ts +159 -0
- package/src/utils/types.ts +237 -0
- package/tests/markdown-converter.test.ts +252 -0
- package/tests/notion-client.test.ts +67 -0
- package/tests/tools.test.ts +448 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion API Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Common types
|
|
6
|
+
export interface NotionUser {
|
|
7
|
+
object: 'user';
|
|
8
|
+
id: string;
|
|
9
|
+
type?: 'person' | 'bot';
|
|
10
|
+
name?: string;
|
|
11
|
+
avatar_url?: string | null;
|
|
12
|
+
person?: {
|
|
13
|
+
email?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NotionParent {
|
|
18
|
+
type: 'database_id' | 'page_id' | 'workspace';
|
|
19
|
+
database_id?: string;
|
|
20
|
+
page_id?: string;
|
|
21
|
+
workspace?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface NotionRichTextItem {
|
|
25
|
+
type: 'text' | 'mention' | 'equation';
|
|
26
|
+
text?: {
|
|
27
|
+
content: string;
|
|
28
|
+
link?: { url: string } | null;
|
|
29
|
+
};
|
|
30
|
+
annotations?: {
|
|
31
|
+
bold: boolean;
|
|
32
|
+
italic: boolean;
|
|
33
|
+
strikethrough: boolean;
|
|
34
|
+
underline: boolean;
|
|
35
|
+
code: boolean;
|
|
36
|
+
color: string;
|
|
37
|
+
};
|
|
38
|
+
plain_text?: string;
|
|
39
|
+
href?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Page types
|
|
43
|
+
export interface NotionPage {
|
|
44
|
+
object: 'page';
|
|
45
|
+
id: string;
|
|
46
|
+
created_time: string;
|
|
47
|
+
last_edited_time: string;
|
|
48
|
+
created_by: NotionUser;
|
|
49
|
+
last_edited_by: NotionUser;
|
|
50
|
+
parent: NotionParent;
|
|
51
|
+
archived: boolean;
|
|
52
|
+
properties: Record<string, NotionProperty>;
|
|
53
|
+
url: string;
|
|
54
|
+
icon?: NotionIcon | null;
|
|
55
|
+
cover?: NotionFile | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NotionIcon {
|
|
59
|
+
type: 'emoji' | 'external' | 'file';
|
|
60
|
+
emoji?: string;
|
|
61
|
+
external?: { url: string };
|
|
62
|
+
file?: { url: string };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NotionFile {
|
|
66
|
+
type: 'external' | 'file';
|
|
67
|
+
external?: { url: string };
|
|
68
|
+
file?: { url: string; expiry_time: string };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Property types
|
|
72
|
+
export interface NotionProperty {
|
|
73
|
+
id: string;
|
|
74
|
+
type: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TitleProperty extends NotionProperty {
|
|
79
|
+
type: 'title';
|
|
80
|
+
title: NotionRichTextItem[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RichTextProperty extends NotionProperty {
|
|
84
|
+
type: 'rich_text';
|
|
85
|
+
rich_text: NotionRichTextItem[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface NumberProperty extends NotionProperty {
|
|
89
|
+
type: 'number';
|
|
90
|
+
number: number | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SelectProperty extends NotionProperty {
|
|
94
|
+
type: 'select';
|
|
95
|
+
select: { id: string; name: string; color: string } | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface MultiSelectProperty extends NotionProperty {
|
|
99
|
+
type: 'multi_select';
|
|
100
|
+
multi_select: { id: string; name: string; color: string }[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface DateProperty extends NotionProperty {
|
|
104
|
+
type: 'date';
|
|
105
|
+
date: { start: string; end?: string | null; time_zone?: string | null } | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CheckboxProperty extends NotionProperty {
|
|
109
|
+
type: 'checkbox';
|
|
110
|
+
checkbox: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface UrlProperty extends NotionProperty {
|
|
114
|
+
type: 'url';
|
|
115
|
+
url: string | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface EmailProperty extends NotionProperty {
|
|
119
|
+
type: 'email';
|
|
120
|
+
email: string | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PhoneProperty extends NotionProperty {
|
|
124
|
+
type: 'phone_number';
|
|
125
|
+
phone_number: string | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface PeopleProperty extends NotionProperty {
|
|
129
|
+
type: 'people';
|
|
130
|
+
people: NotionUser[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Database types
|
|
134
|
+
export interface NotionDatabase {
|
|
135
|
+
object: 'database';
|
|
136
|
+
id: string;
|
|
137
|
+
created_time: string;
|
|
138
|
+
last_edited_time: string;
|
|
139
|
+
title: NotionRichTextItem[];
|
|
140
|
+
description: NotionRichTextItem[];
|
|
141
|
+
properties: Record<string, DatabaseProperty>;
|
|
142
|
+
parent: NotionParent;
|
|
143
|
+
url: string;
|
|
144
|
+
archived: boolean;
|
|
145
|
+
is_inline: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface DatabaseProperty {
|
|
149
|
+
id: string;
|
|
150
|
+
name: string;
|
|
151
|
+
type: string;
|
|
152
|
+
[key: string]: unknown;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Block types
|
|
156
|
+
export interface NotionBlock {
|
|
157
|
+
object: 'block';
|
|
158
|
+
id: string;
|
|
159
|
+
parent: NotionParent;
|
|
160
|
+
type: string;
|
|
161
|
+
created_time: string;
|
|
162
|
+
last_edited_time: string;
|
|
163
|
+
created_by: NotionUser;
|
|
164
|
+
last_edited_by: NotionUser;
|
|
165
|
+
has_children: boolean;
|
|
166
|
+
archived: boolean;
|
|
167
|
+
[key: string]: unknown;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Search types
|
|
171
|
+
export interface SearchRequest {
|
|
172
|
+
query?: string;
|
|
173
|
+
filter?: {
|
|
174
|
+
value: 'page' | 'database';
|
|
175
|
+
property: 'object';
|
|
176
|
+
};
|
|
177
|
+
sort?: {
|
|
178
|
+
direction: 'ascending' | 'descending';
|
|
179
|
+
timestamp: 'last_edited_time';
|
|
180
|
+
};
|
|
181
|
+
start_cursor?: string;
|
|
182
|
+
page_size?: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SearchResponse {
|
|
186
|
+
object: 'list';
|
|
187
|
+
results: (NotionPage | NotionDatabase)[];
|
|
188
|
+
next_cursor: string | null;
|
|
189
|
+
has_more: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Query types
|
|
193
|
+
export interface DatabaseQueryRequest {
|
|
194
|
+
filter?: NotionFilter;
|
|
195
|
+
sorts?: NotionSort[];
|
|
196
|
+
start_cursor?: string;
|
|
197
|
+
page_size?: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface NotionFilter {
|
|
201
|
+
and?: NotionFilter[];
|
|
202
|
+
or?: NotionFilter[];
|
|
203
|
+
property?: string;
|
|
204
|
+
[key: string]: unknown;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface NotionSort {
|
|
208
|
+
property?: string;
|
|
209
|
+
timestamp?: 'created_time' | 'last_edited_time';
|
|
210
|
+
direction: 'ascending' | 'descending';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface PaginatedResponse<T> {
|
|
214
|
+
object: 'list';
|
|
215
|
+
results: T[];
|
|
216
|
+
next_cursor: string | null;
|
|
217
|
+
has_more: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// User list response
|
|
221
|
+
export interface UsersListResponse extends PaginatedResponse<NotionUser> {}
|
|
222
|
+
|
|
223
|
+
// Helper to extract title from page
|
|
224
|
+
export function getPageTitle(page: NotionPage): string {
|
|
225
|
+
for (const [, prop] of Object.entries(page.properties)) {
|
|
226
|
+
if (prop.type === 'title') {
|
|
227
|
+
const titleProp = prop as TitleProperty;
|
|
228
|
+
return titleProp.title.map(t => t.plain_text || t.text?.content || '').join('');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return 'Untitled';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Helper to extract title from database
|
|
235
|
+
export function getDatabaseTitle(database: NotionDatabase): string {
|
|
236
|
+
return database.title.map(t => t.plain_text || t.text?.content || '').join('') || 'Untitled';
|
|
237
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Markdown Converter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseInlineFormatting,
|
|
7
|
+
markdownToBlocks,
|
|
8
|
+
blocksToMarkdown,
|
|
9
|
+
blocksToPlainText
|
|
10
|
+
} from '../src/utils/markdown-converter';
|
|
11
|
+
|
|
12
|
+
describe('parseInlineFormatting', () => {
|
|
13
|
+
it('should handle plain text', () => {
|
|
14
|
+
const result = parseInlineFormatting('Hello world');
|
|
15
|
+
expect(result).toHaveLength(1);
|
|
16
|
+
expect(result[0].text.content).toBe('Hello world');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle bold text', () => {
|
|
20
|
+
const result = parseInlineFormatting('This is **bold** text');
|
|
21
|
+
expect(result).toHaveLength(3);
|
|
22
|
+
expect(result[0].text.content).toBe('This is ');
|
|
23
|
+
expect(result[1].text.content).toBe('bold');
|
|
24
|
+
expect(result[1].annotations?.bold).toBe(true);
|
|
25
|
+
expect(result[2].text.content).toBe(' text');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle italic text', () => {
|
|
29
|
+
const result = parseInlineFormatting('This is *italic* text');
|
|
30
|
+
expect(result).toHaveLength(3);
|
|
31
|
+
expect(result[1].text.content).toBe('italic');
|
|
32
|
+
expect(result[1].annotations?.italic).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle inline code', () => {
|
|
36
|
+
const result = parseInlineFormatting('Use `code` here');
|
|
37
|
+
expect(result).toHaveLength(3);
|
|
38
|
+
expect(result[1].text.content).toBe('code');
|
|
39
|
+
expect(result[1].annotations?.code).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle strikethrough', () => {
|
|
43
|
+
const result = parseInlineFormatting('This is ~~deleted~~ text');
|
|
44
|
+
expect(result).toHaveLength(3);
|
|
45
|
+
expect(result[1].text.content).toBe('deleted');
|
|
46
|
+
expect(result[1].annotations?.strikethrough).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should handle links', () => {
|
|
50
|
+
const result = parseInlineFormatting('Click [here](https://example.com) now');
|
|
51
|
+
expect(result).toHaveLength(3);
|
|
52
|
+
expect(result[1].text.content).toBe('here');
|
|
53
|
+
expect(result[1].text.link?.url).toBe('https://example.com');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle multiple formatting in sequence', () => {
|
|
57
|
+
const result = parseInlineFormatting('**bold** and *italic*');
|
|
58
|
+
expect(result.length).toBeGreaterThan(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('markdownToBlocks', () => {
|
|
63
|
+
it('should convert heading 1', () => {
|
|
64
|
+
const blocks = markdownToBlocks('# Heading 1');
|
|
65
|
+
expect(blocks).toHaveLength(1);
|
|
66
|
+
expect(blocks[0].type).toBe('heading_1');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should convert heading 2', () => {
|
|
70
|
+
const blocks = markdownToBlocks('## Heading 2');
|
|
71
|
+
expect(blocks).toHaveLength(1);
|
|
72
|
+
expect(blocks[0].type).toBe('heading_2');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should convert heading 3', () => {
|
|
76
|
+
const blocks = markdownToBlocks('### Heading 3');
|
|
77
|
+
expect(blocks).toHaveLength(1);
|
|
78
|
+
expect(blocks[0].type).toBe('heading_3');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should convert bullet list', () => {
|
|
82
|
+
const blocks = markdownToBlocks('- Item 1\n- Item 2');
|
|
83
|
+
expect(blocks).toHaveLength(2);
|
|
84
|
+
expect(blocks[0].type).toBe('bulleted_list_item');
|
|
85
|
+
expect(blocks[1].type).toBe('bulleted_list_item');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should convert numbered list', () => {
|
|
89
|
+
const blocks = markdownToBlocks('1. First\n2. Second');
|
|
90
|
+
expect(blocks).toHaveLength(2);
|
|
91
|
+
expect(blocks[0].type).toBe('numbered_list_item');
|
|
92
|
+
expect(blocks[1].type).toBe('numbered_list_item');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should convert checkbox unchecked', () => {
|
|
96
|
+
const blocks = markdownToBlocks('- [ ] Todo item');
|
|
97
|
+
expect(blocks).toHaveLength(1);
|
|
98
|
+
expect(blocks[0].type).toBe('to_do');
|
|
99
|
+
expect((blocks[0].to_do as { checked: boolean }).checked).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should convert checkbox checked', () => {
|
|
103
|
+
const blocks = markdownToBlocks('- [x] Done item');
|
|
104
|
+
expect(blocks).toHaveLength(1);
|
|
105
|
+
expect(blocks[0].type).toBe('to_do');
|
|
106
|
+
expect((blocks[0].to_do as { checked: boolean }).checked).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should convert blockquote', () => {
|
|
110
|
+
const blocks = markdownToBlocks('> This is a quote');
|
|
111
|
+
expect(blocks).toHaveLength(1);
|
|
112
|
+
expect(blocks[0].type).toBe('quote');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should convert divider', () => {
|
|
116
|
+
const blocks = markdownToBlocks('---');
|
|
117
|
+
expect(blocks).toHaveLength(1);
|
|
118
|
+
expect(blocks[0].type).toBe('divider');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should convert paragraph', () => {
|
|
122
|
+
const blocks = markdownToBlocks('This is a paragraph');
|
|
123
|
+
expect(blocks).toHaveLength(1);
|
|
124
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should convert code block', () => {
|
|
128
|
+
const blocks = markdownToBlocks('```javascript\nconst x = 1;\n```');
|
|
129
|
+
expect(blocks).toHaveLength(1);
|
|
130
|
+
expect(blocks[0].type).toBe('code');
|
|
131
|
+
expect((blocks[0].code as { language: string }).language).toBe('javascript');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle multiple elements', () => {
|
|
135
|
+
const markdown = `# Title
|
|
136
|
+
|
|
137
|
+
This is a paragraph.
|
|
138
|
+
|
|
139
|
+
- List item 1
|
|
140
|
+
- List item 2
|
|
141
|
+
|
|
142
|
+
> A quote`;
|
|
143
|
+
|
|
144
|
+
const blocks = markdownToBlocks(markdown);
|
|
145
|
+
expect(blocks.length).toBeGreaterThan(4);
|
|
146
|
+
expect(blocks[0].type).toBe('heading_1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle empty input', () => {
|
|
150
|
+
const blocks = markdownToBlocks('');
|
|
151
|
+
expect(blocks).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('blocksToMarkdown', () => {
|
|
156
|
+
it('should convert paragraph block to markdown', () => {
|
|
157
|
+
const blocks = [{
|
|
158
|
+
type: 'paragraph',
|
|
159
|
+
paragraph: {
|
|
160
|
+
rich_text: [{ type: 'text', text: { content: 'Hello' } }]
|
|
161
|
+
}
|
|
162
|
+
}];
|
|
163
|
+
const result = blocksToMarkdown(blocks);
|
|
164
|
+
expect(result).toBe('Hello');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should convert heading blocks to markdown', () => {
|
|
168
|
+
const blocks = [
|
|
169
|
+
{
|
|
170
|
+
type: 'heading_1',
|
|
171
|
+
heading_1: {
|
|
172
|
+
rich_text: [{ type: 'text', text: { content: 'Title' } }]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'heading_2',
|
|
177
|
+
heading_2: {
|
|
178
|
+
rich_text: [{ type: 'text', text: { content: 'Subtitle' } }]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
const result = blocksToMarkdown(blocks);
|
|
183
|
+
expect(result).toContain('# Title');
|
|
184
|
+
expect(result).toContain('## Subtitle');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should convert list items to markdown', () => {
|
|
188
|
+
const blocks = [
|
|
189
|
+
{
|
|
190
|
+
type: 'bulleted_list_item',
|
|
191
|
+
bulleted_list_item: {
|
|
192
|
+
rich_text: [{ type: 'text', text: { content: 'Item' } }]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
const result = blocksToMarkdown(blocks);
|
|
197
|
+
expect(result).toContain('- Item');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should convert todo items to markdown', () => {
|
|
201
|
+
const blocks = [
|
|
202
|
+
{
|
|
203
|
+
type: 'to_do',
|
|
204
|
+
to_do: {
|
|
205
|
+
rich_text: [{ type: 'text', text: { content: 'Task' } }],
|
|
206
|
+
checked: true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
];
|
|
210
|
+
const result = blocksToMarkdown(blocks);
|
|
211
|
+
expect(result).toContain('- [x] Task');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should handle formatted text', () => {
|
|
215
|
+
const blocks = [{
|
|
216
|
+
type: 'paragraph',
|
|
217
|
+
paragraph: {
|
|
218
|
+
rich_text: [
|
|
219
|
+
{ type: 'text', text: { content: 'bold' }, annotations: { bold: true } }
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
}];
|
|
223
|
+
const result = blocksToMarkdown(blocks);
|
|
224
|
+
expect(result).toBe('**bold**');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('blocksToPlainText', () => {
|
|
229
|
+
it('should extract plain text from blocks', () => {
|
|
230
|
+
const blocks = [
|
|
231
|
+
{
|
|
232
|
+
type: 'paragraph',
|
|
233
|
+
paragraph: {
|
|
234
|
+
rich_text: [{ type: 'text', text: { content: 'Hello' } }]
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
type: 'paragraph',
|
|
239
|
+
paragraph: {
|
|
240
|
+
rich_text: [{ type: 'text', text: { content: 'World' } }]
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
const result = blocksToPlainText(blocks);
|
|
245
|
+
expect(result).toBe('Hello\nWorld');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should handle empty blocks', () => {
|
|
249
|
+
const result = blocksToPlainText([]);
|
|
250
|
+
expect(result).toBe('');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Notion Client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { extractNotionId } from '../src/utils/notion-client';
|
|
6
|
+
|
|
7
|
+
describe('extractNotionId', () => {
|
|
8
|
+
describe('UUID format handling', () => {
|
|
9
|
+
it('should return normalized UUID with dashes when given 32 char hex', () => {
|
|
10
|
+
const input = '1234567890abcdef1234567890abcdef';
|
|
11
|
+
const result = extractNotionId(input);
|
|
12
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should keep UUID format with dashes unchanged', () => {
|
|
16
|
+
const input = '12345678-90ab-cdef-1234-567890abcdef';
|
|
17
|
+
const result = extractNotionId(input);
|
|
18
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should normalize mixed case UUID', () => {
|
|
22
|
+
const input = 'ABCDEF12-3456-7890-ABCD-EF1234567890';
|
|
23
|
+
const result = extractNotionId(input);
|
|
24
|
+
expect(result).toBe('abcdef12-3456-7890-abcd-ef1234567890');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('URL extraction', () => {
|
|
29
|
+
it('should extract ID from notion.so page URL', () => {
|
|
30
|
+
const input = 'https://www.notion.so/myworkspace/Page-Title-1234567890abcdef1234567890abcdef';
|
|
31
|
+
const result = extractNotionId(input);
|
|
32
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should extract ID from notion.site URL', () => {
|
|
36
|
+
const input = 'https://example.notion.site/Page-1234567890abcdef1234567890abcdef';
|
|
37
|
+
const result = extractNotionId(input);
|
|
38
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should extract ID from URL with query parameters', () => {
|
|
42
|
+
const input = 'https://www.notion.so/Page-1234567890abcdef1234567890abcdef?v=abc123';
|
|
43
|
+
const result = extractNotionId(input);
|
|
44
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should extract ID from URL with dashes in the ID', () => {
|
|
48
|
+
const input = 'https://www.notion.so/workspace/12345678-90ab-cdef-1234-567890abcdef';
|
|
49
|
+
const result = extractNotionId(input);
|
|
50
|
+
expect(result).toBe('12345678-90ab-cdef-1234-567890abcdef');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('edge cases', () => {
|
|
55
|
+
it('should return input as-is if no pattern matches', () => {
|
|
56
|
+
const input = 'invalid-id';
|
|
57
|
+
const result = extractNotionId(input);
|
|
58
|
+
expect(result).toBe('invalid-id');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle empty string', () => {
|
|
62
|
+
const input = '';
|
|
63
|
+
const result = extractNotionId(input);
|
|
64
|
+
expect(result).toBe('');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|