@matimo/notion 0.1.2 → 0.1.4

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.2",
3
+ "version": "0.1.4",
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.15.2",
13
+ "@matimo/core": "0.1.4"
14
+ },
11
15
  "peerDependencies": {
12
- "matimo": "0.1.2"
16
+ "matimo": "0.1.4"
13
17
  },
14
- "devDependencies": {
15
- "axios": "^1.15.2",
16
- "@matimo/core": "0.1.2"
18
+ "scripts": {
19
+ "build": "tsc"
17
20
  }
18
21
  }
@@ -47,7 +47,7 @@ parameters:
47
47
 
48
48
  execution:
49
49
  type: function
50
- code: './index.ts'
50
+ code: './index.js'
51
51
 
52
52
  authentication:
53
53
  type: bearer
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { MatimoError, ErrorCode } from '@matimo/core';
2
+ import { MatimoError, ErrorCode } from '@matimo/core/runtime';
3
3
 
4
4
  interface Params {
5
5
  parent?: Record<string, unknown>;