@jordancoin/notioncli 1.2.1 โ 1.3.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 +111 -423
- package/TECHNICAL.md +125 -0
- package/bin/notion.js +922 -857
- package/lib/config.js +68 -0
- package/lib/filters.js +213 -0
- package/lib/format.js +225 -0
- package/lib/helpers.js +9 -334
- package/lib/markdown.js +347 -0
- package/package.json +1 -1
- package/skill/SKILL.md +44 -10
- package/skill/marketplace.json +2 -2
- package/test/unit.test.js +392 -0
- package/test/debug-parent.js +0 -32
- package/test/live-relations-test.js +0 -309
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Live integration test: Relations, Rollups & Blocks CRUD
|
|
3
|
-
// Creates temp databases, links them, tests CLI output, cleans up.
|
|
4
|
-
|
|
5
|
-
const { Client } = require('@notionhq/client');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
const CLI = path.join(__dirname, '..', 'bin', 'notion.js');
|
|
10
|
-
const run = (cmd) => execSync(`node ${CLI} ${cmd}`, { encoding: 'utf-8' }).trim();
|
|
11
|
-
const runJson = (cmd) => JSON.parse(execSync(`node ${CLI} --json ${cmd}`, { encoding: 'utf-8' }));
|
|
12
|
-
|
|
13
|
-
const notion = new Client({ auth: process.env.NOTION_API_KEY || require('../lib/helpers').loadConfig(
|
|
14
|
-
require('../lib/helpers').getConfigPaths().CONFIG_PATH
|
|
15
|
-
).apiKey });
|
|
16
|
-
|
|
17
|
-
let parentPageId = null;
|
|
18
|
-
let projectsDbId = null;
|
|
19
|
-
let projectsDsId = null;
|
|
20
|
-
let tasksDbId = null;
|
|
21
|
-
let tasksDsId = null;
|
|
22
|
-
let testPageIds = [];
|
|
23
|
-
let createdDbIds = [];
|
|
24
|
-
|
|
25
|
-
async function setup() {
|
|
26
|
-
console.log('\n๐ง Setting up test databases...\n');
|
|
27
|
-
|
|
28
|
-
// Create a dedicated test page as parent (needs to be under a page the integration can access)
|
|
29
|
-
// First, find an existing page the integration has access to
|
|
30
|
-
const search = await notion.search({ filter: { value: 'page', property: 'object' }, page_size: 20 });
|
|
31
|
-
|
|
32
|
-
// Look for a top-level page (parent is workspace), or any page that's not inside a database
|
|
33
|
-
let rootPageId = null;
|
|
34
|
-
for (const p of search.results) {
|
|
35
|
-
if (p.parent?.type === 'workspace' || p.parent?.type === 'page_id') {
|
|
36
|
-
rootPageId = p.id;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!rootPageId) {
|
|
42
|
-
// No standalone page found โ create the DBs using the workspace parent directly
|
|
43
|
-
// by creating a page in the integration's space
|
|
44
|
-
console.log('No standalone page found. Will use first available page.');
|
|
45
|
-
rootPageId = search.results[0]?.id;
|
|
46
|
-
if (!rootPageId) throw new Error('No pages accessible by integration');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Create a test container page
|
|
50
|
-
const containerPage = await notion.pages.create({
|
|
51
|
-
parent: { type: 'page_id', page_id: rootPageId },
|
|
52
|
-
properties: {
|
|
53
|
-
title: { title: [{ text: { content: 'CLI Test Container (auto-delete)' } }] },
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
parentPageId = containerPage.id;
|
|
57
|
-
testPageIds.push(parentPageId);
|
|
58
|
-
console.log(`๐ Created test container page: ${parentPageId.slice(0, 8)}โฆ`);
|
|
59
|
-
|
|
60
|
-
// 1. Create "CLI Test Projects" database
|
|
61
|
-
console.log('\nCreating Projects DB...');
|
|
62
|
-
const projectsDb = await notion.databases.create({
|
|
63
|
-
parent: { type: 'page_id', page_id: parentPageId },
|
|
64
|
-
title: [{ text: { content: 'CLI Test Projects' } }],
|
|
65
|
-
properties: {
|
|
66
|
-
'Name': { title: {} },
|
|
67
|
-
'Status': { select: { options: [
|
|
68
|
-
{ name: 'Active', color: 'green' },
|
|
69
|
-
{ name: 'Done', color: 'gray' },
|
|
70
|
-
]}},
|
|
71
|
-
'Priority': { number: {} },
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
// databases.create() returns: .id = data_source_id, .database_id = database_id for page creation
|
|
75
|
-
projectsDsId = projectsDb.id;
|
|
76
|
-
projectsDbId = projectsDb.database_id || projectsDb.id;
|
|
77
|
-
createdDbIds.push(projectsDb.id);
|
|
78
|
-
console.log(`โ
Projects DB (db: ${projectsDbId.slice(0, 8)}โฆ, ds: ${projectsDsId.slice(0, 8)}โฆ)`);
|
|
79
|
-
|
|
80
|
-
// 2. Create "CLI Test Tasks" database with relation to Projects
|
|
81
|
-
console.log('Creating Tasks DB with relation...');
|
|
82
|
-
const tasksDb = await notion.databases.create({
|
|
83
|
-
parent: { type: 'page_id', page_id: parentPageId },
|
|
84
|
-
title: [{ text: { content: 'CLI Test Tasks' } }],
|
|
85
|
-
properties: {
|
|
86
|
-
'Name': { title: {} },
|
|
87
|
-
'Done': { checkbox: {} },
|
|
88
|
-
'Project': { relation: { database_id: projectsDbId } },
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
tasksDsId = tasksDb.id;
|
|
92
|
-
tasksDbId = tasksDb.database_id || tasksDb.id;
|
|
93
|
-
createdDbIds.push(tasksDb.id);
|
|
94
|
-
console.log(`โ
Tasks DB (db: ${tasksDbId.slice(0, 8)}โฆ, ds: ${tasksDsId.slice(0, 8)}โฆ)`);
|
|
95
|
-
|
|
96
|
-
// 3. Add project pages
|
|
97
|
-
console.log('\nAdding test data...');
|
|
98
|
-
const proj1 = await notion.pages.create({
|
|
99
|
-
parent: { type: 'database_id', database_id: projectsDbId },
|
|
100
|
-
properties: {
|
|
101
|
-
'Name': { title: [{ text: { content: 'Build CLI' } }] },
|
|
102
|
-
'Status': { select: { name: 'Active' } },
|
|
103
|
-
'Priority': { number: 1 },
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
testPageIds.push(proj1.id);
|
|
107
|
-
console.log(` ๐ Project: "Build CLI"`);
|
|
108
|
-
|
|
109
|
-
const proj2 = await notion.pages.create({
|
|
110
|
-
parent: { type: 'database_id', database_id: projectsDbId },
|
|
111
|
-
properties: {
|
|
112
|
-
'Name': { title: [{ text: { content: 'Write Docs' } }] },
|
|
113
|
-
'Status': { select: { name: 'Done' } },
|
|
114
|
-
'Priority': { number: 2 },
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
testPageIds.push(proj2.id);
|
|
118
|
-
console.log(` ๐ Project: "Write Docs"`);
|
|
119
|
-
|
|
120
|
-
// 4. Add task pages linked to projects
|
|
121
|
-
const task1 = await notion.pages.create({
|
|
122
|
-
parent: { type: 'database_id', database_id: tasksDbId },
|
|
123
|
-
properties: {
|
|
124
|
-
'Name': { title: [{ text: { content: 'Implement relations' } }] },
|
|
125
|
-
'Done': { checkbox: true },
|
|
126
|
-
'Project': { relation: [{ id: proj1.id }] },
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
testPageIds.push(task1.id);
|
|
130
|
-
console.log(` ๐ Task: "Implement relations" โ Build CLI`);
|
|
131
|
-
|
|
132
|
-
const task2 = await notion.pages.create({
|
|
133
|
-
parent: { type: 'database_id', database_id: tasksDbId },
|
|
134
|
-
properties: {
|
|
135
|
-
'Name': { title: [{ text: { content: 'Add tests' } }] },
|
|
136
|
-
'Done': { checkbox: false },
|
|
137
|
-
'Project': { relation: [{ id: proj1.id }] },
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
testPageIds.push(task2.id);
|
|
141
|
-
console.log(` ๐ Task: "Add tests" โ Build CLI`);
|
|
142
|
-
|
|
143
|
-
const task3 = await notion.pages.create({
|
|
144
|
-
parent: { type: 'database_id', database_id: tasksDbId },
|
|
145
|
-
properties: {
|
|
146
|
-
'Name': { title: [{ text: { content: 'Write README' } }] },
|
|
147
|
-
'Done': { checkbox: true },
|
|
148
|
-
'Project': { relation: [{ id: proj2.id }] },
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
testPageIds.push(task3.id);
|
|
152
|
-
console.log(` ๐ Task: "Write README" โ Write Docs`);
|
|
153
|
-
|
|
154
|
-
// 5. Register CLI aliases
|
|
155
|
-
run(`alias add test-projects ${projectsDsId}`);
|
|
156
|
-
run(`alias add test-tasks ${tasksDsId}`);
|
|
157
|
-
console.log(`\nโ
Aliases registered: test-projects, test-tasks\n`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function runTests() {
|
|
161
|
-
let passed = 0;
|
|
162
|
-
let failed = 0;
|
|
163
|
-
|
|
164
|
-
function check(name, condition, detail) {
|
|
165
|
-
if (condition) {
|
|
166
|
-
console.log(` โ
${name}`);
|
|
167
|
-
passed++;
|
|
168
|
-
} else {
|
|
169
|
-
console.log(` โ ${name}${detail ? ' โ ' + detail : ''}`);
|
|
170
|
-
failed++;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
console.log('๐งช Running live tests...\n');
|
|
175
|
-
|
|
176
|
-
// --- Test 1: Query tasks โ relation column formatting ---
|
|
177
|
-
console.log('--- 1. query (relation display) ---');
|
|
178
|
-
try {
|
|
179
|
-
const out = run('query test-tasks');
|
|
180
|
-
check('query shows tasks', out.includes('Implement relations'));
|
|
181
|
-
check('relation shows โ format', out.includes('โ'));
|
|
182
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
183
|
-
} catch (e) { check('query test-tasks', false, e.message); }
|
|
184
|
-
|
|
185
|
-
// --- Test 2: Get task โ resolve relation to project title ---
|
|
186
|
-
console.log('--- 2. get (relation resolution) ---');
|
|
187
|
-
try {
|
|
188
|
-
const out = run('get test-tasks --filter "Name=Implement relations"');
|
|
189
|
-
check('get resolves relation to title', out.includes('Build CLI'));
|
|
190
|
-
check('get shows URL', out.includes('notion.so'));
|
|
191
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
192
|
-
} catch (e) { check('get with relation', false, e.message); }
|
|
193
|
-
|
|
194
|
-
// --- Test 3: Relations command โ graph explorer ---
|
|
195
|
-
console.log('--- 3. relations (graph explorer) ---');
|
|
196
|
-
try {
|
|
197
|
-
const out = run('relations test-tasks --filter "Name=Implement relations"');
|
|
198
|
-
check('relations shows linked pages', out.includes('linked') || out.includes('Build CLI'));
|
|
199
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
200
|
-
} catch (e) { check('relations command', false, e.message); }
|
|
201
|
-
|
|
202
|
-
// --- Test 4: Reverse relation (project โ tasks) ---
|
|
203
|
-
console.log('--- 4. reverse relation ---');
|
|
204
|
-
try {
|
|
205
|
-
const out = run('relations test-projects --filter "Name=Build CLI"');
|
|
206
|
-
const hasLinks = out.includes('Implement') || out.includes('Add tests') || out.includes('linked');
|
|
207
|
-
check('project shows reverse relations', hasLinks);
|
|
208
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
209
|
-
} catch (e) { check('reverse relation', false, e.message); }
|
|
210
|
-
|
|
211
|
-
// --- Test 5: Blocks --ids ---
|
|
212
|
-
console.log('--- 5. blocks --ids ---');
|
|
213
|
-
try {
|
|
214
|
-
run('append test-tasks "Test block for live test" --filter "Name=Implement relations"');
|
|
215
|
-
const out = run('blocks test-tasks --filter "Name=Implement relations" --ids');
|
|
216
|
-
check('blocks --ids shows ID prefix', out.includes('['));
|
|
217
|
-
check('blocks shows appended text', out.includes('Test block'));
|
|
218
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
219
|
-
} catch (e) { check('blocks --ids', false, e.message); }
|
|
220
|
-
|
|
221
|
-
// --- Test 6: Block edit ---
|
|
222
|
-
console.log('--- 6. block-edit ---');
|
|
223
|
-
try {
|
|
224
|
-
const json = runJson('blocks test-tasks --filter "Name=Implement relations"');
|
|
225
|
-
const blockId = json.results[json.results.length - 1].id;
|
|
226
|
-
const out = run(`block-edit ${blockId} "EDITED by CLI test"`);
|
|
227
|
-
check('block-edit succeeds', out.includes('โ
'));
|
|
228
|
-
const verify = run('blocks test-tasks --filter "Name=Implement relations"');
|
|
229
|
-
check('edited content visible', verify.includes('EDITED'));
|
|
230
|
-
console.log(`\n${verify.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
231
|
-
} catch (e) { check('block-edit', false, e.message); }
|
|
232
|
-
|
|
233
|
-
// --- Test 7: Block delete ---
|
|
234
|
-
console.log('--- 7. block-delete ---');
|
|
235
|
-
try {
|
|
236
|
-
const json = runJson('blocks test-tasks --filter "Name=Implement relations"');
|
|
237
|
-
const blockId = json.results[json.results.length - 1].id;
|
|
238
|
-
const out = run(`block-delete ${blockId}`);
|
|
239
|
-
check('block-delete succeeds', out.includes('๐'));
|
|
240
|
-
const verify = run('blocks test-tasks --filter "Name=Implement relations"');
|
|
241
|
-
check('deleted content gone', !verify.includes('EDITED'));
|
|
242
|
-
} catch (e) { check('block-delete', false, e.message); }
|
|
243
|
-
|
|
244
|
-
// --- Test 8: JSON output preserves relation data ---
|
|
245
|
-
console.log('--- 8. json output ---');
|
|
246
|
-
try {
|
|
247
|
-
const json = runJson('get test-tasks --filter "Name=Implement relations"');
|
|
248
|
-
check('json has properties', !!json.properties);
|
|
249
|
-
check('json has relation property', !!json.properties.Project);
|
|
250
|
-
check('relation type correct', json.properties.Project.type === 'relation');
|
|
251
|
-
check('relation has linked IDs', json.properties.Project.relation.length > 0);
|
|
252
|
-
} catch (e) { check('json output', false, e.message); }
|
|
253
|
-
|
|
254
|
-
// --- Test 9: Query projects with rollup-like display ---
|
|
255
|
-
console.log('--- 9. query projects ---');
|
|
256
|
-
try {
|
|
257
|
-
const out = run('query test-projects');
|
|
258
|
-
check('query shows projects', out.includes('Build CLI'));
|
|
259
|
-
check('query shows both projects', out.includes('Write Docs'));
|
|
260
|
-
console.log(`\n${out.split('\n').map(l => ' ' + l).join('\n')}\n`);
|
|
261
|
-
} catch (e) { check('query projects', false, e.message); }
|
|
262
|
-
|
|
263
|
-
console.log(`\n${'โ'.repeat(50)}`);
|
|
264
|
-
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
|
|
265
|
-
console.log('โ'.repeat(50));
|
|
266
|
-
|
|
267
|
-
return failed;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async function cleanup() {
|
|
271
|
-
console.log('\n๐งน Cleaning up...');
|
|
272
|
-
|
|
273
|
-
// Archive pages first (before their parent DBs get deleted)
|
|
274
|
-
for (const id of testPageIds.filter(id => !createdDbIds.includes(id))) {
|
|
275
|
-
try { await notion.pages.update({ page_id: id, archived: true }); } catch {}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Then delete DBs and container page
|
|
279
|
-
for (const id of createdDbIds) {
|
|
280
|
-
try { await notion.blocks.delete({ block_id: id }); } catch {}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Delete container page last
|
|
284
|
-
if (parentPageId) {
|
|
285
|
-
try { await notion.blocks.delete({ block_id: parentPageId }); } catch {}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
try { run('alias remove test-projects'); } catch {}
|
|
289
|
-
try { run('alias remove test-tasks'); } catch {}
|
|
290
|
-
|
|
291
|
-
console.log('โ
Cleaned up\n');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function main() {
|
|
295
|
-
try {
|
|
296
|
-
await setup();
|
|
297
|
-
const failures = await runTests();
|
|
298
|
-
await cleanup();
|
|
299
|
-
process.exit(failures > 0 ? 1 : 0);
|
|
300
|
-
} catch (err) {
|
|
301
|
-
console.error('\n๐ฅ Test crashed:', err.message);
|
|
302
|
-
if (err.body) console.error('API body:', JSON.stringify(err.body).slice(0, 500));
|
|
303
|
-
if (err.stack) console.error(err.stack);
|
|
304
|
-
await cleanup().catch(() => {});
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
main();
|