@moxn/kb-migrate 0.4.23 → 0.4.24
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/dist/client.js +12 -2
- package/dist/index.js +18 -4
- package/dist/sources/notion-blocks.d.ts +4 -0
- package/dist/sources/notion-blocks.js +25 -21
- package/dist/sources/notion.d.ts +5 -0
- package/dist/sources/notion.js +69 -7
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -299,7 +299,12 @@ export class MoxnClient {
|
|
|
299
299
|
error.branchId = body.branchId;
|
|
300
300
|
throw error;
|
|
301
301
|
}
|
|
302
|
-
|
|
302
|
+
const message = body.details
|
|
303
|
+
? `API error ${response.status}: ${body.details}`
|
|
304
|
+
: body.error
|
|
305
|
+
? `API error ${response.status}: ${body.error}`
|
|
306
|
+
: `API error: ${response.status}`;
|
|
307
|
+
throw new Error(message);
|
|
303
308
|
}
|
|
304
309
|
return response.json();
|
|
305
310
|
}
|
|
@@ -314,7 +319,12 @@ export class MoxnClient {
|
|
|
314
319
|
});
|
|
315
320
|
if (!response.ok) {
|
|
316
321
|
const body = await response.json().catch(() => ({}));
|
|
317
|
-
|
|
322
|
+
const message = body.details
|
|
323
|
+
? `API error ${response.status}: ${body.details}`
|
|
324
|
+
: body.error
|
|
325
|
+
? `API error ${response.status}: ${body.error}`
|
|
326
|
+
: `API error: ${response.status}`;
|
|
327
|
+
throw new Error(message);
|
|
318
328
|
}
|
|
319
329
|
return response.json();
|
|
320
330
|
}
|
package/dist/index.js
CHANGED
|
@@ -27,21 +27,28 @@ async function runMigration(source, options) {
|
|
|
27
27
|
const results = [];
|
|
28
28
|
// Validate source
|
|
29
29
|
await source.validate();
|
|
30
|
-
// Get document count for progress
|
|
30
|
+
// Get document count for progress (this is an upper bound — some pages
|
|
31
|
+
// may be skipped during extraction if they are empty or have errors)
|
|
31
32
|
const totalCount = await source.getDocumentCount();
|
|
32
|
-
if (totalCount !== undefined) {
|
|
33
|
-
console.log(`Found ${totalCount} documents to migrate`);
|
|
34
|
-
}
|
|
35
33
|
// Create client
|
|
36
34
|
const client = new MoxnClient(options);
|
|
37
35
|
// Process documents
|
|
38
36
|
let processed = 0;
|
|
37
|
+
let consecutiveFailures = 0;
|
|
38
|
+
const MAX_CONSECUTIVE_FAILURES = 10;
|
|
39
39
|
for await (const doc of source.extract()) {
|
|
40
40
|
processed++;
|
|
41
41
|
const progress = totalCount ? ` (${processed}/${totalCount})` : '';
|
|
42
42
|
console.log(`Processing: ${doc.sourcePath}${progress}`);
|
|
43
43
|
const result = await client.migrateDocument(doc, options.basePath, options.onConflict, options.dryRun);
|
|
44
44
|
results.push(result);
|
|
45
|
+
// Track consecutive failures for circuit breaker
|
|
46
|
+
if (result.status === 'failed') {
|
|
47
|
+
consecutiveFailures++;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
consecutiveFailures = 0;
|
|
51
|
+
}
|
|
45
52
|
// Log result
|
|
46
53
|
const statusIcon = {
|
|
47
54
|
created: '\u2713',
|
|
@@ -53,6 +60,13 @@ async function runMigration(source, options) {
|
|
|
53
60
|
if (result.error) {
|
|
54
61
|
console.log(` Error: ${result.error}`);
|
|
55
62
|
}
|
|
63
|
+
// Circuit breaker: abort after too many consecutive failures
|
|
64
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
65
|
+
console.error(`\nAborting: ${MAX_CONSECUTIVE_FAILURES} consecutive failures. ` +
|
|
66
|
+
'Last error: ' + (result.error || 'unknown') + '\n' +
|
|
67
|
+
'Fix the underlying issue and retry. Remaining documents will be skipped.');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
// Build summary
|
|
58
72
|
const summary = {
|
|
@@ -8,6 +8,8 @@ import type { SectionInput } from '../types.js';
|
|
|
8
8
|
import type { NotionBlock, NotionRichText, NotionApiClient, NotionPage } from './notion-api.js';
|
|
9
9
|
/** Map of page IDs to their KB paths (for cross-link resolution). */
|
|
10
10
|
export type PagePathMap = Map<string, string>;
|
|
11
|
+
/** Tracks unsupported Notion block types encountered during conversion. */
|
|
12
|
+
export type UnsupportedBlockTracker = Map<string, number>;
|
|
11
13
|
/**
|
|
12
14
|
* Convert a page's blocks into Moxn sections.
|
|
13
15
|
*
|
|
@@ -19,6 +21,8 @@ export declare function blocksToSections(blocks: NotionBlock[], client: NotionAp
|
|
|
19
21
|
visitedSyncedBlocks?: Set<string>;
|
|
20
22
|
/** Map of Notion database IDs to KB database IDs (for child_database blocks). */
|
|
21
23
|
databaseIdMap?: Map<string, string>;
|
|
24
|
+
/** Accumulates counts of unsupported block types (caller-owned). */
|
|
25
|
+
unsupportedBlocks?: UnsupportedBlockTracker;
|
|
22
26
|
}): Promise<SectionInput[]>;
|
|
23
27
|
/** Convert rich text array to markdown string. */
|
|
24
28
|
export declare function richTextToMarkdown(richText: NotionRichText[]): string;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
export async function blocksToSections(blocks, client, pagePathMap, options) {
|
|
17
17
|
const visitedSyncedBlocks = options?.visitedSyncedBlocks ?? new Set();
|
|
18
18
|
const databaseIdMap = options?.databaseIdMap;
|
|
19
|
+
const unsupportedBlocks = options?.unsupportedBlocks;
|
|
19
20
|
const sections = [];
|
|
20
21
|
let currentSectionName = 'Introduction';
|
|
21
22
|
let currentBlocks = [];
|
|
@@ -35,7 +36,7 @@ export async function blocksToSections(blocks, client, pagePathMap, options) {
|
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
37
38
|
// Convert block to content blocks
|
|
38
|
-
const converted = await convertBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap);
|
|
39
|
+
const converted = await convertBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks);
|
|
39
40
|
currentBlocks.push(...converted);
|
|
40
41
|
}
|
|
41
42
|
// Flush last section
|
|
@@ -51,7 +52,7 @@ export async function blocksToSections(blocks, client, pagePathMap, options) {
|
|
|
51
52
|
// ============================================
|
|
52
53
|
// Block conversion
|
|
53
54
|
// ============================================
|
|
54
|
-
async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
55
|
+
async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
55
56
|
const results = [];
|
|
56
57
|
switch (block.type) {
|
|
57
58
|
case 'paragraph':
|
|
@@ -81,10 +82,10 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
81
82
|
results.push(...convertToDo(block));
|
|
82
83
|
break;
|
|
83
84
|
case 'quote':
|
|
84
|
-
results.push(...(await convertQuote(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap)));
|
|
85
|
+
results.push(...(await convertQuote(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks)));
|
|
85
86
|
break;
|
|
86
87
|
case 'callout':
|
|
87
|
-
results.push(...(await convertCallout(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap)));
|
|
88
|
+
results.push(...(await convertCallout(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks)));
|
|
88
89
|
break;
|
|
89
90
|
case 'divider':
|
|
90
91
|
results.push(textBlock('---'));
|
|
@@ -93,7 +94,7 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
93
94
|
results.push(...(await convertTable(block, client)));
|
|
94
95
|
break;
|
|
95
96
|
case 'toggle':
|
|
96
|
-
results.push(...(await convertToggle(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap)));
|
|
97
|
+
results.push(...(await convertToggle(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks)));
|
|
97
98
|
break;
|
|
98
99
|
case 'bookmark':
|
|
99
100
|
results.push(...convertBookmark(block));
|
|
@@ -122,10 +123,10 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
122
123
|
results.push(...convertChildDatabase(block, databaseIdMap));
|
|
123
124
|
break;
|
|
124
125
|
case 'synced_block':
|
|
125
|
-
results.push(...(await convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap)));
|
|
126
|
+
results.push(...(await convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks)));
|
|
126
127
|
break;
|
|
127
128
|
case 'column_list':
|
|
128
|
-
results.push(...(await convertColumnList(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap)));
|
|
129
|
+
results.push(...(await convertColumnList(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks)));
|
|
129
130
|
break;
|
|
130
131
|
case 'column':
|
|
131
132
|
// Columns are handled by column_list
|
|
@@ -146,7 +147,10 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
146
147
|
results.push(...convertLinkPreview(block));
|
|
147
148
|
break;
|
|
148
149
|
default:
|
|
149
|
-
// Unknown block type —
|
|
150
|
+
// Unknown block type — track and skip
|
|
151
|
+
if (unsupportedBlocks) {
|
|
152
|
+
unsupportedBlocks.set(block.type, (unsupportedBlocks.get(block.type) ?? 0) + 1);
|
|
153
|
+
}
|
|
150
154
|
console.warn(` Skipping unsupported Notion block type: ${block.type}`);
|
|
151
155
|
break;
|
|
152
156
|
}
|
|
@@ -165,7 +169,7 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
165
169
|
? ' '
|
|
166
170
|
: undefined;
|
|
167
171
|
const children = await client.getBlockChildren(block.id);
|
|
168
|
-
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent, databaseIdMap);
|
|
172
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent, databaseIdMap, unsupportedBlocks);
|
|
169
173
|
results.push(...childBlocks);
|
|
170
174
|
}
|
|
171
175
|
return results;
|
|
@@ -211,7 +215,7 @@ function convertToDo(block) {
|
|
|
211
215
|
const text = richTextToMarkdown(td.to_do.rich_text);
|
|
212
216
|
return [textBlock(`${checkbox} ${text}`)];
|
|
213
217
|
}
|
|
214
|
-
async function convertQuote(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
218
|
+
async function convertQuote(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
215
219
|
const q = block;
|
|
216
220
|
const text = richTextToMarkdown(q.quote.rich_text);
|
|
217
221
|
if (!text && !block.has_children)
|
|
@@ -225,12 +229,12 @@ async function convertQuote(block, client, pagePathMap, visitedSyncedBlocks, dat
|
|
|
225
229
|
const results = quoted ? [textBlock(quoted)] : [];
|
|
226
230
|
if (block.has_children) {
|
|
227
231
|
const children = await client.getBlockChildren(block.id);
|
|
228
|
-
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap);
|
|
232
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap, unsupportedBlocks);
|
|
229
233
|
results.push(...childBlocks);
|
|
230
234
|
}
|
|
231
235
|
return results;
|
|
232
236
|
}
|
|
233
|
-
async function convertCallout(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
237
|
+
async function convertCallout(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
234
238
|
const c = block;
|
|
235
239
|
const text = richTextToMarkdown(c.callout.rich_text);
|
|
236
240
|
const emoji = c.callout.icon?.emoji ?? '';
|
|
@@ -242,7 +246,7 @@ async function convertCallout(block, client, pagePathMap, visitedSyncedBlocks, d
|
|
|
242
246
|
const results = [textBlock(quoted)];
|
|
243
247
|
if (block.has_children) {
|
|
244
248
|
const children = await client.getBlockChildren(block.id);
|
|
245
|
-
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap);
|
|
249
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap, unsupportedBlocks);
|
|
246
250
|
results.push(...childBlocks);
|
|
247
251
|
}
|
|
248
252
|
return results;
|
|
@@ -271,13 +275,13 @@ async function convertTable(block, client) {
|
|
|
271
275
|
}
|
|
272
276
|
return [textBlock(lines.join('\n'))];
|
|
273
277
|
}
|
|
274
|
-
async function convertToggle(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
278
|
+
async function convertToggle(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
275
279
|
const t = block;
|
|
276
280
|
const header = richTextToMarkdown(t.toggle.rich_text);
|
|
277
281
|
const results = [textBlock(`**${header}**`)];
|
|
278
282
|
if (block.has_children) {
|
|
279
283
|
const children = await client.getBlockChildren(block.id);
|
|
280
|
-
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap);
|
|
284
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ', databaseIdMap, unsupportedBlocks);
|
|
281
285
|
results.push(...childBlocks);
|
|
282
286
|
}
|
|
283
287
|
return results;
|
|
@@ -402,7 +406,7 @@ function convertLinkToPage(block, pagePathMap) {
|
|
|
402
406
|
// Target not in import set
|
|
403
407
|
return [textBlock(`*(Link to Notion page: ${targetId})*`)];
|
|
404
408
|
}
|
|
405
|
-
async function convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
409
|
+
async function convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
406
410
|
const sb = block;
|
|
407
411
|
// Get the source block ID (either this block or the original)
|
|
408
412
|
const sourceId = sb.synced_block.synced_from?.block_id ?? block.id;
|
|
@@ -413,19 +417,19 @@ async function convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlock
|
|
|
413
417
|
visitedSyncedBlocks.add(sourceId);
|
|
414
418
|
try {
|
|
415
419
|
const children = await client.getBlockChildren(sourceId);
|
|
416
|
-
return await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, undefined, databaseIdMap);
|
|
420
|
+
return await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, undefined, databaseIdMap, unsupportedBlocks);
|
|
417
421
|
}
|
|
418
422
|
finally {
|
|
419
423
|
visitedSyncedBlocks.delete(sourceId);
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
|
-
async function convertColumnList(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap) {
|
|
426
|
+
async function convertColumnList(block, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks) {
|
|
423
427
|
const children = await client.getBlockChildren(block.id);
|
|
424
428
|
const results = [];
|
|
425
429
|
for (const column of children) {
|
|
426
430
|
if (column.type === 'column' && column.has_children) {
|
|
427
431
|
const columnChildren = await client.getBlockChildren(column.id);
|
|
428
|
-
const converted = await convertAndMergeChildren(columnChildren, client, pagePathMap, visitedSyncedBlocks, undefined, databaseIdMap);
|
|
432
|
+
const converted = await convertAndMergeChildren(columnChildren, client, pagePathMap, visitedSyncedBlocks, undefined, databaseIdMap, unsupportedBlocks);
|
|
429
433
|
results.push(...converted);
|
|
430
434
|
}
|
|
431
435
|
}
|
|
@@ -574,10 +578,10 @@ function mergeConsecutiveListBlocks(blocks) {
|
|
|
574
578
|
/**
|
|
575
579
|
* Convert children blocks, merge consecutive list items, and optionally indent.
|
|
576
580
|
*/
|
|
577
|
-
async function convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent, databaseIdMap) {
|
|
581
|
+
async function convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent, databaseIdMap, unsupportedBlocks) {
|
|
578
582
|
const blocks = [];
|
|
579
583
|
for (const child of children) {
|
|
580
|
-
const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks, databaseIdMap);
|
|
584
|
+
const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks, databaseIdMap, unsupportedBlocks);
|
|
581
585
|
blocks.push(...converted);
|
|
582
586
|
}
|
|
583
587
|
const merged = mergeConsecutiveListBlocks(blocks);
|
package/dist/sources/notion.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export declare class NotionSource extends MigrationSource<NotionSourceConfig> {
|
|
|
52
52
|
private databaseEntryPageIds;
|
|
53
53
|
private _documentCount;
|
|
54
54
|
private _validated;
|
|
55
|
+
private _skippedEmpty;
|
|
56
|
+
private _skippedNoContent;
|
|
57
|
+
private _extractionErrors;
|
|
58
|
+
private _syntheticDatabaseNodes;
|
|
59
|
+
private _unsupportedBlocks;
|
|
55
60
|
/** Map of Notion database ID → KB database ID, set before extraction. */
|
|
56
61
|
private _databaseIdMap;
|
|
57
62
|
constructor(config: NotionSourceConfig);
|
package/dist/sources/notion.js
CHANGED
|
@@ -28,6 +28,12 @@ export class NotionSource extends MigrationSource {
|
|
|
28
28
|
databaseEntryPageIds = new Set();
|
|
29
29
|
_documentCount = 0;
|
|
30
30
|
_validated = false;
|
|
31
|
+
// Tracking for extraction-time skips (populated during extract())
|
|
32
|
+
_skippedEmpty = [];
|
|
33
|
+
_skippedNoContent = [];
|
|
34
|
+
_extractionErrors = [];
|
|
35
|
+
_syntheticDatabaseNodes = 0;
|
|
36
|
+
_unsupportedBlocks = new Map();
|
|
31
37
|
/** Map of Notion database ID → KB database ID, set before extraction. */
|
|
32
38
|
_databaseIdMap = new Map();
|
|
33
39
|
constructor(config) {
|
|
@@ -89,13 +95,18 @@ export class NotionSource extends MigrationSource {
|
|
|
89
95
|
// 5. Process databases — identify entries and build schemas
|
|
90
96
|
await this.processDatabases(allNotionDatabases);
|
|
91
97
|
// 6. Count and validate (exclude synthetic database container nodes)
|
|
92
|
-
this.
|
|
98
|
+
this._syntheticDatabaseNodes = this.allPages.filter((n) => n.isDatabaseNode).length;
|
|
99
|
+
const pagesInTree = this.allPages.filter((n) => !n.isDatabaseNode).length;
|
|
100
|
+
const databaseEntriesInTree = this.allPages.filter((n) => n.isDatabaseEntry && !n.isDatabaseNode).length;
|
|
101
|
+
this._documentCount = pagesInTree;
|
|
93
102
|
// Add database-only entries (pages that are in databases but not in page tree)
|
|
103
|
+
let databaseEntriesOutsideTree = 0;
|
|
94
104
|
for (const dbInfo of this.databases) {
|
|
95
105
|
for (const entry of dbInfo.entries) {
|
|
96
106
|
const nid = normalizeId(entry.id);
|
|
97
107
|
if (!this.pagePathMap.has(nid)) {
|
|
98
108
|
this._documentCount++;
|
|
109
|
+
databaseEntriesOutsideTree++;
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
}
|
|
@@ -103,7 +114,12 @@ export class NotionSource extends MigrationSource {
|
|
|
103
114
|
throw new Error(`Workspace has ${this._documentCount} documents, exceeding the ${MAX_DOCUMENT_COUNT} limit. ` +
|
|
104
115
|
'Use --root-page-id to import a subtree.');
|
|
105
116
|
}
|
|
106
|
-
|
|
117
|
+
// Log clear breakdown of what was discovered
|
|
118
|
+
const standalonePages = pagesInTree - databaseEntriesInTree;
|
|
119
|
+
const totalDbEntries = databaseEntriesInTree + databaseEntriesOutsideTree;
|
|
120
|
+
console.log(` ${this._documentCount} documents to import: ` +
|
|
121
|
+
`${standalonePages} pages + ${totalDbEntries} database entries ` +
|
|
122
|
+
`(across ${this.databases.length} databases)`);
|
|
107
123
|
// 7. Initialize media downloader
|
|
108
124
|
await this.mediaDownloader.init();
|
|
109
125
|
this._validated = true;
|
|
@@ -135,6 +151,12 @@ export class NotionSource extends MigrationSource {
|
|
|
135
151
|
// Pass 2: Extraction
|
|
136
152
|
// ============================================
|
|
137
153
|
async *extract() {
|
|
154
|
+
// Reset extraction tracking
|
|
155
|
+
this._skippedEmpty = [];
|
|
156
|
+
this._skippedNoContent = [];
|
|
157
|
+
this._extractionErrors = [];
|
|
158
|
+
this._unsupportedBlocks = new Map();
|
|
159
|
+
let yielded = 0;
|
|
138
160
|
// Walk pages depth-first
|
|
139
161
|
for (const node of this.allPages) {
|
|
140
162
|
// Skip database entries that will be created during database import
|
|
@@ -145,8 +167,10 @@ export class NotionSource extends MigrationSource {
|
|
|
145
167
|
if (node.isDatabaseNode)
|
|
146
168
|
continue;
|
|
147
169
|
const doc = await this.extractPage(node);
|
|
148
|
-
if (doc)
|
|
170
|
+
if (doc) {
|
|
171
|
+
yielded++;
|
|
149
172
|
yield doc;
|
|
173
|
+
}
|
|
150
174
|
}
|
|
151
175
|
// Extract database-only entries (pages not in page tree)
|
|
152
176
|
for (const dbInfo of this.databases) {
|
|
@@ -162,9 +186,42 @@ export class NotionSource extends MigrationSource {
|
|
|
162
186
|
}
|
|
163
187
|
}
|
|
164
188
|
const doc = await this.extractDatabaseEntry(entry, dbInfo);
|
|
165
|
-
if (doc)
|
|
189
|
+
if (doc) {
|
|
190
|
+
yielded++;
|
|
166
191
|
yield doc;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Log extraction summary so user can reconcile counts
|
|
196
|
+
const totalSkipped = this._skippedEmpty.length +
|
|
197
|
+
this._skippedNoContent.length +
|
|
198
|
+
this._extractionErrors.length;
|
|
199
|
+
console.log(`\nExtraction complete: ${yielded} of ${this._documentCount} documents extracted`);
|
|
200
|
+
if (totalSkipped > 0) {
|
|
201
|
+
console.log(` ${totalSkipped} pages skipped:`);
|
|
202
|
+
if (this._skippedEmpty.length > 0) {
|
|
203
|
+
console.log(` Empty pages (no blocks): ${this._skippedEmpty.map((t) => `"${t}"`).join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
if (this._skippedNoContent.length > 0) {
|
|
206
|
+
console.log(` No extractable content: ${this._skippedNoContent.map((t) => `"${t}"`).join(', ')}`);
|
|
167
207
|
}
|
|
208
|
+
if (this._extractionErrors.length > 0) {
|
|
209
|
+
console.log(` Extraction errors: ${this._extractionErrors.map((t) => `"${t}"`).join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Report unsupported block types
|
|
213
|
+
if (this._unsupportedBlocks.size > 0) {
|
|
214
|
+
const sorted = [...this._unsupportedBlocks.entries()].sort((a, b) => b[1] - a[1]);
|
|
215
|
+
const totalUnsupported = sorted.reduce((sum, [, count]) => sum + count, 0);
|
|
216
|
+
console.log(` ${totalUnsupported} unsupported block(s) across ${sorted.length} type(s):`);
|
|
217
|
+
for (const [type, count] of sorted) {
|
|
218
|
+
console.log(` ${type}: ${count}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Warn if counts don't reconcile (helps catch counting bugs)
|
|
222
|
+
const expectedYield = this._documentCount - totalSkipped;
|
|
223
|
+
if (yielded !== expectedYield) {
|
|
224
|
+
console.log(` ⚠ Count mismatch: expected ${expectedYield} (${this._documentCount} - ${totalSkipped} skipped) but extracted ${yielded}`);
|
|
168
225
|
}
|
|
169
226
|
}
|
|
170
227
|
/** Clean up temp files after migration completes. */
|
|
@@ -224,11 +281,12 @@ export class NotionSource extends MigrationSource {
|
|
|
224
281
|
try {
|
|
225
282
|
const blocks = await this.client.getBlockChildren(node.page.id);
|
|
226
283
|
if (blocks.length === 0) {
|
|
227
|
-
|
|
284
|
+
this._skippedEmpty.push(node.title);
|
|
228
285
|
return null;
|
|
229
286
|
}
|
|
230
287
|
let sections = await blocksToSections(blocks, this.client, this.pagePathMap, {
|
|
231
288
|
databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
|
|
289
|
+
unsupportedBlocks: this._unsupportedBlocks,
|
|
232
290
|
});
|
|
233
291
|
// Download Notion-hosted media files
|
|
234
292
|
sections = await this.downloadSectionMedia(sections);
|
|
@@ -236,7 +294,7 @@ export class NotionSource extends MigrationSource {
|
|
|
236
294
|
const { sections: resolvedSections, references } = resolveNotionReferences(sections, { notionIdToKbPath: this.pagePathMap });
|
|
237
295
|
sections = resolvedSections;
|
|
238
296
|
if (sections.length === 0) {
|
|
239
|
-
|
|
297
|
+
this._skippedNoContent.push(node.title);
|
|
240
298
|
return null;
|
|
241
299
|
}
|
|
242
300
|
return {
|
|
@@ -252,6 +310,7 @@ export class NotionSource extends MigrationSource {
|
|
|
252
310
|
};
|
|
253
311
|
}
|
|
254
312
|
catch (error) {
|
|
313
|
+
this._extractionErrors.push(node.title);
|
|
255
314
|
console.error(` Error extracting page "${node.title}": ${error instanceof Error ? error.message : error}`);
|
|
256
315
|
return null;
|
|
257
316
|
}
|
|
@@ -273,6 +332,7 @@ export class NotionSource extends MigrationSource {
|
|
|
273
332
|
if (blocks.length > 0) {
|
|
274
333
|
const contentSections = await blocksToSections(blocks, this.client, this.pagePathMap, {
|
|
275
334
|
databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
|
|
335
|
+
unsupportedBlocks: this._unsupportedBlocks,
|
|
276
336
|
});
|
|
277
337
|
sections.push(...contentSections);
|
|
278
338
|
}
|
|
@@ -303,7 +363,9 @@ export class NotionSource extends MigrationSource {
|
|
|
303
363
|
};
|
|
304
364
|
}
|
|
305
365
|
catch (error) {
|
|
306
|
-
|
|
366
|
+
const entryTitle = getPageTitle(entry) || `DB entry ${entry.id}`;
|
|
367
|
+
this._extractionErrors.push(entryTitle);
|
|
368
|
+
console.error(` Error extracting database entry "${entryTitle}": ${error instanceof Error ? error.message : error}`);
|
|
307
369
|
return null;
|
|
308
370
|
}
|
|
309
371
|
}
|
package/package.json
CHANGED