@matimo/notion 0.1.3 → 0.1.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matimo/notion",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Notion workspace tools for Matimo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
"README.md",
|
|
9
9
|
"definition.yaml"
|
|
10
10
|
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.18.0",
|
|
13
|
+
"@matimo/core": "0.1.5"
|
|
14
|
+
},
|
|
11
15
|
"peerDependencies": {
|
|
12
|
-
"matimo": "0.1.
|
|
16
|
+
"matimo": "0.1.5"
|
|
13
17
|
},
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"@matimo/core": "0.1.3"
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc"
|
|
17
20
|
}
|
|
18
21
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
3
|
+
function markdownToChildren(md) {
|
|
4
|
+
// Very small converter: split paragraphs and handle headings (#, ##, ###)
|
|
5
|
+
const parts = md.split(/\n\n+/).map((p) => p.trim()).filter(Boolean);
|
|
6
|
+
const blocks = [];
|
|
7
|
+
for (const part of parts) {
|
|
8
|
+
if (part.startsWith('# ')) {
|
|
9
|
+
blocks.push({
|
|
10
|
+
object: 'block',
|
|
11
|
+
type: 'heading_1',
|
|
12
|
+
heading_1: { rich_text: [{ type: 'text', text: { content: part.replace(/^#\s+/, '') } }] },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
else if (part.startsWith('## ')) {
|
|
16
|
+
blocks.push({
|
|
17
|
+
object: 'block',
|
|
18
|
+
type: 'heading_2',
|
|
19
|
+
heading_2: { rich_text: [{ type: 'text', text: { content: part.replace(/^##\s+/, '') } }] },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else if (part.startsWith('### ')) {
|
|
23
|
+
blocks.push({
|
|
24
|
+
object: 'block',
|
|
25
|
+
type: 'heading_3',
|
|
26
|
+
heading_3: { rich_text: [{ type: 'text', text: { content: part.replace(/^###\s+/, '') } }] },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
blocks.push({
|
|
31
|
+
object: 'block',
|
|
32
|
+
type: 'paragraph',
|
|
33
|
+
paragraph: { rich_text: [{ type: 'text', text: { content: part } }] },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return blocks;
|
|
38
|
+
}
|
|
39
|
+
export default async function createPage(params) {
|
|
40
|
+
const { parent: userProvidedParent, properties, icon, cover, children, markdown, template, position, } = params;
|
|
41
|
+
const apiKey = process.env.NOTION_API_KEY;
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new MatimoError('NOTION_API_KEY not set', ErrorCode.AUTH_FAILED, {
|
|
44
|
+
envVar: 'NOTION_API_KEY',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// If no parent provided, auto-discover a database
|
|
48
|
+
let parent = userProvidedParent;
|
|
49
|
+
if (!parent || typeof parent !== 'object') {
|
|
50
|
+
try {
|
|
51
|
+
const response = await axios.get('https://api.notion.com/v1/databases', {
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${apiKey}`,
|
|
54
|
+
'Notion-Version': '2025-09-03',
|
|
55
|
+
},
|
|
56
|
+
timeout: 15000,
|
|
57
|
+
params: { page_size: 1 },
|
|
58
|
+
});
|
|
59
|
+
const databases = response.data?.results || [];
|
|
60
|
+
if (databases.length > 0) {
|
|
61
|
+
parent = { database_id: databases[0].id };
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw new MatimoError('No databases found in workspace. Create a database first or provide `parent` parameter.', ErrorCode.VALIDATION_FAILED);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (axios.isAxiosError(err)) {
|
|
69
|
+
throw new MatimoError(`Failed to auto-discover database: ${err.response?.data?.message || err.message}`, ErrorCode.EXECUTION_FAILED);
|
|
70
|
+
}
|
|
71
|
+
throw new MatimoError('Failed to auto-discover database', ErrorCode.EXECUTION_FAILED);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Validate parameters according to tool contract:
|
|
75
|
+
// - If creating inside a database (`parent.database_id`), `properties` is required
|
|
76
|
+
// - Either `children` or `markdown` can be provided, not both (empty children array is treated as not provided)
|
|
77
|
+
// - When using `template`, `children` is not allowed
|
|
78
|
+
const isDatabaseParent = parent && typeof parent === 'object' && Object.prototype.hasOwnProperty.call(parent, 'database_id');
|
|
79
|
+
if (Array.isArray(children) && children.length > 0 && typeof markdown === 'string' && markdown.trim().length > 0) {
|
|
80
|
+
throw new MatimoError('Provide either `children` or `markdown`, not both', ErrorCode.VALIDATION_FAILED, {
|
|
81
|
+
parameters: { children: children.length, markdown: markdown.length },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (template && Array.isArray(children) && children.length > 0) {
|
|
85
|
+
throw new MatimoError('`template` cannot be used together with `children`. Omit `children` when using `template`', ErrorCode.VALIDATION_FAILED, {
|
|
86
|
+
parameters: { template: !!template, children: children.length },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Allow generating a minimal title property from `markdown` when creating in a database.
|
|
90
|
+
// If caller didn't provide `properties`, we'll attempt several common title property names
|
|
91
|
+
// (e.g., 'Name', 'Title') derived from the first markdown line. If none succeed,
|
|
92
|
+
// we return the API error to the caller.
|
|
93
|
+
const resolvedProperties = properties;
|
|
94
|
+
let titleCandidates;
|
|
95
|
+
if (isDatabaseParent) {
|
|
96
|
+
const hasProperties = resolvedProperties && typeof resolvedProperties === 'object' && Object.keys(resolvedProperties).length > 0;
|
|
97
|
+
if (!hasProperties) {
|
|
98
|
+
if (typeof markdown === 'string' && markdown.trim().length > 0) {
|
|
99
|
+
// Candidate property names to try when the database title property name is unknown
|
|
100
|
+
titleCandidates = ['Name', 'Title', 'title', 'name'].map((k) => k);
|
|
101
|
+
// We'll construct properties later per candidate when sending the request.
|
|
102
|
+
// Use resolvedProperties only if caller provided it explicitly.
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw new MatimoError('Creating a page in a database requires `properties` (at minimum a title property)', ErrorCode.VALIDATION_FAILED, {
|
|
106
|
+
parent,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Convert markdown to children if provided and children not explicitly set
|
|
112
|
+
let resolvedChildren = children;
|
|
113
|
+
if ((!resolvedChildren || (Array.isArray(resolvedChildren) && resolvedChildren.length === 0)) && markdown) {
|
|
114
|
+
resolvedChildren = markdownToChildren(markdown);
|
|
115
|
+
}
|
|
116
|
+
const baseBody = {
|
|
117
|
+
parent,
|
|
118
|
+
};
|
|
119
|
+
if (resolvedProperties)
|
|
120
|
+
baseBody.properties = resolvedProperties;
|
|
121
|
+
if (icon)
|
|
122
|
+
baseBody.icon = icon;
|
|
123
|
+
if (cover)
|
|
124
|
+
baseBody.cover = cover;
|
|
125
|
+
if (resolvedChildren)
|
|
126
|
+
baseBody.children = resolvedChildren;
|
|
127
|
+
if (template)
|
|
128
|
+
baseBody.template = template;
|
|
129
|
+
if (position)
|
|
130
|
+
baseBody.position = position;
|
|
131
|
+
// Helper to send request with a given body
|
|
132
|
+
const sendRequest = async (requestBody) => {
|
|
133
|
+
return axios.post('https://api.notion.com/v1/pages', requestBody, {
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: `Bearer ${apiKey}`,
|
|
136
|
+
'Notion-Version': '2025-09-03',
|
|
137
|
+
'Content-Type': 'application/json',
|
|
138
|
+
},
|
|
139
|
+
timeout: 15000,
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
// If we have title candidates, try each one until a request succeeds.
|
|
143
|
+
if (titleCandidates && titleCandidates.length > 0) {
|
|
144
|
+
let lastError = null;
|
|
145
|
+
const firstLine = markdown.split(/\r?\n/)[0].replace(/^#+\s*/, '').trim() || 'New Page';
|
|
146
|
+
for (const candidate of titleCandidates) {
|
|
147
|
+
const candidateProps = {
|
|
148
|
+
[candidate]: {
|
|
149
|
+
title: [
|
|
150
|
+
{
|
|
151
|
+
text: { content: firstLine },
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const tryBody = { ...baseBody, ...{ properties: candidateProps } };
|
|
157
|
+
try {
|
|
158
|
+
const resp = await sendRequest(tryBody);
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
statusCode: resp.status,
|
|
162
|
+
data: resp.data,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
lastError = err;
|
|
167
|
+
// If API returns validation error about missing property name, continue to next candidate.
|
|
168
|
+
if (axios.isAxiosError(err)) {
|
|
169
|
+
const errData = err.response?.data;
|
|
170
|
+
const message = errData?.message || '';
|
|
171
|
+
if (typeof message === 'string' && /is not a property that exists/i.test(message)) {
|
|
172
|
+
// try next candidate
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// For other errors, break and return immediately
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// All candidates failed — return last error in a structured form
|
|
181
|
+
if (axios.isAxiosError(lastError)) {
|
|
182
|
+
const errData = lastError.response?.data;
|
|
183
|
+
const statusCode = lastError.response?.status || 0;
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
statusCode,
|
|
187
|
+
error: errData || lastError.message,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
statusCode: 0,
|
|
193
|
+
error: String(lastError),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// No candidate loop — send single request using baseBody (which may include provided properties)
|
|
197
|
+
try {
|
|
198
|
+
const resp = await sendRequest(baseBody);
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
statusCode: resp.status,
|
|
202
|
+
data: resp.data,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (axios.isAxiosError(err)) {
|
|
207
|
+
const errData = err.response?.data;
|
|
208
|
+
const details = {
|
|
209
|
+
status: err.response?.status,
|
|
210
|
+
code: errData?.code,
|
|
211
|
+
request_id: errData?.request_id,
|
|
212
|
+
};
|
|
213
|
+
throw new MatimoError(errData?.message || 'Notion API error', ErrorCode.EXECUTION_FAILED, details);
|
|
214
|
+
}
|
|
215
|
+
// Non-HTTP error (network, unexpected) — wrap and throw
|
|
216
|
+
throw new MatimoError(String(err), ErrorCode.UNKNOWN_ERROR);
|
|
217
|
+
}
|
|
218
|
+
}
|