@moxn/kb-migrate 0.4.17 → 0.4.19
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/export-notion.js
CHANGED
|
@@ -137,7 +137,12 @@ export async function runNotionExport(options) {
|
|
|
137
137
|
// Use the parent page ID as the target for database creation
|
|
138
138
|
const parentPageId = options.parentPageId;
|
|
139
139
|
const result = await exportDatabase(dbCtx, dbId, parentPageId);
|
|
140
|
-
const icon = {
|
|
140
|
+
const icon = {
|
|
141
|
+
created: '\u2713',
|
|
142
|
+
updated: '\u21bb',
|
|
143
|
+
skipped: '-',
|
|
144
|
+
failed: '\u2717',
|
|
145
|
+
}[result.status];
|
|
141
146
|
console.log(` ${icon} ${result.status}: ${result.kbDatabaseName} (${result.entriesCreated} entries)`);
|
|
142
147
|
if (result.error)
|
|
143
148
|
console.log(` Error: ${result.error}`);
|
|
@@ -165,7 +170,8 @@ export async function runNotionExport(options) {
|
|
|
165
170
|
// ============================================
|
|
166
171
|
if (!options.dryRun) {
|
|
167
172
|
// Find documents that have references and were successfully exported
|
|
168
|
-
const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') &&
|
|
173
|
+
const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') &&
|
|
174
|
+
r.externalId);
|
|
169
175
|
if (successfulExports.length > 0) {
|
|
170
176
|
console.log(`\nPass 2: Resolving cross-references for ${successfulExports.length} documents...`);
|
|
171
177
|
let totalResolved = 0;
|
package/dist/targets/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target registry and factory
|
|
3
3
|
*/
|
|
4
|
-
export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog } from './base.js';
|
|
4
|
+
export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog, } from './base.js';
|
|
5
5
|
export { NotionExportTarget, type NotionExportConfig } from './notion.js';
|
|
6
|
-
export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext } from './notion-database-export.js';
|
|
6
|
+
export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext, } from './notion-database-export.js';
|
package/dist/targets/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target registry and factory
|
|
3
3
|
*/
|
|
4
|
-
export { ExportTarget } from './base.js';
|
|
4
|
+
export { ExportTarget, } from './base.js';
|
|
5
5
|
export { NotionExportTarget } from './notion.js';
|
|
6
|
-
export { exportDatabase } from './notion-database-export.js';
|
|
6
|
+
export { exportDatabase, } from './notion-database-export.js';
|
|
@@ -28,7 +28,7 @@ const HEX_TO_NOTION_COLOR = {
|
|
|
28
28
|
function hexToNotionColor(hex) {
|
|
29
29
|
if (!hex)
|
|
30
30
|
return 'default';
|
|
31
|
-
return HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default';
|
|
31
|
+
return (HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default');
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Export a single KB database to Notion.
|
package/dist/targets/notion.js
CHANGED
|
@@ -187,22 +187,49 @@ async function uploadFileToNotion(client, url, filename, contentType) {
|
|
|
187
187
|
/**
|
|
188
188
|
* Post-process martian output to reconstruct callout blocks.
|
|
189
189
|
*
|
|
190
|
-
* Callouts
|
|
191
|
-
*
|
|
192
|
-
* `
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* Callouts may arrive as either:
|
|
191
|
+
* 1. `quote` blocks (when stored with `> ` prefix) — text in children[0].paragraph.rich_text
|
|
192
|
+
* 2. `paragraph` blocks (when stored as plain text) — text in paragraph.rich_text
|
|
193
|
+
*
|
|
194
|
+
* In both cases, if the first text segment starts with an emoji followed by a space,
|
|
195
|
+
* the block is converted to a native Notion `callout` with that emoji as the icon.
|
|
195
196
|
*/
|
|
196
197
|
function reconstructCallouts(blocks) {
|
|
197
198
|
const emojiPrefixRe = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s/u;
|
|
198
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
199
|
return blocks.map((block) => {
|
|
200
|
+
// Case 1: paragraph with emoji prefix
|
|
201
|
+
if (block.type === 'paragraph') {
|
|
202
|
+
const richText = block.paragraph?.rich_text;
|
|
203
|
+
if (!richText?.length)
|
|
204
|
+
return block;
|
|
205
|
+
const firstSegment = richText[0];
|
|
206
|
+
if (firstSegment.type !== 'text' || !firstSegment.text?.content)
|
|
207
|
+
return block;
|
|
208
|
+
const match = firstSegment.text.content.match(emojiPrefixRe);
|
|
209
|
+
if (!match)
|
|
210
|
+
return block;
|
|
211
|
+
const emoji = match[1];
|
|
212
|
+
const strippedContent = firstSegment.text.content.slice(match[0].length);
|
|
213
|
+
const calloutRichText = [
|
|
214
|
+
{ ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
|
|
215
|
+
...richText.slice(1),
|
|
216
|
+
].filter((rt) => 'text' in rt && rt.text?.content);
|
|
217
|
+
return {
|
|
218
|
+
object: 'block',
|
|
219
|
+
type: 'callout',
|
|
220
|
+
callout: {
|
|
221
|
+
rich_text: calloutRichText,
|
|
222
|
+
icon: { type: 'emoji', emoji },
|
|
223
|
+
color: 'default',
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Case 2: quote with emoji prefix in first child paragraph
|
|
200
228
|
if (block.type !== 'quote')
|
|
201
229
|
return block;
|
|
202
230
|
const children = block.quote?.children;
|
|
203
231
|
if (!children?.length)
|
|
204
232
|
return block;
|
|
205
|
-
// Find the first child paragraph with text
|
|
206
233
|
const firstChild = children[0];
|
|
207
234
|
if (firstChild.type !== 'paragraph' || !firstChild.paragraph?.rich_text?.length)
|
|
208
235
|
return block;
|
|
@@ -211,15 +238,13 @@ function reconstructCallouts(blocks) {
|
|
|
211
238
|
return block;
|
|
212
239
|
const match = firstSegment.text.content.match(emojiPrefixRe);
|
|
213
240
|
if (!match)
|
|
214
|
-
return block;
|
|
241
|
+
return block;
|
|
215
242
|
const emoji = match[1];
|
|
216
243
|
const strippedContent = firstSegment.text.content.slice(match[0].length);
|
|
217
|
-
// Build the callout's rich_text from the first child paragraph (stripped of emoji)
|
|
218
244
|
const calloutRichText = [
|
|
219
245
|
{ ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
|
|
220
246
|
...firstChild.paragraph.rich_text.slice(1),
|
|
221
247
|
].filter((rt) => rt.text?.content);
|
|
222
|
-
// Remaining children become the callout's children
|
|
223
248
|
const remainingChildren = children.slice(1);
|
|
224
249
|
return {
|
|
225
250
|
object: 'block',
|
|
@@ -285,7 +310,11 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
285
310
|
for (let i = 0; i < parts.length; i++) {
|
|
286
311
|
if (i > 0) {
|
|
287
312
|
flushText();
|
|
288
|
-
allBlocks.push({
|
|
313
|
+
allBlocks.push({
|
|
314
|
+
object: 'block',
|
|
315
|
+
type: 'divider',
|
|
316
|
+
divider: {},
|
|
317
|
+
});
|
|
289
318
|
}
|
|
290
319
|
const part = parts[i];
|
|
291
320
|
if (part.trim()) {
|
|
@@ -297,9 +326,12 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
297
326
|
else if (block.blockType === 'image' && block.url) {
|
|
298
327
|
flushText();
|
|
299
328
|
if (block.storageKey && options?.notionClient) {
|
|
300
|
-
const fname = block.filename ||
|
|
329
|
+
const fname = block.filename ||
|
|
330
|
+
deriveFilename(block.storageKey, block.mimeType || 'image/png');
|
|
301
331
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'image/png');
|
|
302
|
-
allBlocks.push(uploadId
|
|
332
|
+
allBlocks.push(uploadId
|
|
333
|
+
? notionImageUploadBlock(uploadId, block.alt)
|
|
334
|
+
: notionImageBlock(block.url, block.alt));
|
|
303
335
|
}
|
|
304
336
|
else {
|
|
305
337
|
allBlocks.push(notionImageBlock(block.url, block.alt));
|
|
@@ -308,10 +340,13 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
308
340
|
else if (block.blockType === 'document' && block.url) {
|
|
309
341
|
flushText();
|
|
310
342
|
if (block.storageKey && options?.notionClient) {
|
|
311
|
-
const fname = block.filename ||
|
|
343
|
+
const fname = block.filename ||
|
|
344
|
+
deriveFilename(block.storageKey, block.mimeType || 'application/octet-stream');
|
|
312
345
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'application/octet-stream');
|
|
313
346
|
if (uploadId) {
|
|
314
|
-
allBlocks.push(block.mimeType === 'application/pdf'
|
|
347
|
+
allBlocks.push(block.mimeType === 'application/pdf'
|
|
348
|
+
? notionPdfUploadBlock(uploadId, block.filename)
|
|
349
|
+
: notionFileUploadBlock(uploadId, block.filename));
|
|
315
350
|
}
|
|
316
351
|
else if (block.mimeType === 'application/pdf') {
|
|
317
352
|
allBlocks.push(notionPdfBlock(block.url, block.filename));
|
|
@@ -332,7 +367,9 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
332
367
|
if (block.storageKey && options?.notionClient) {
|
|
333
368
|
const fname = block.filename || deriveFilename(block.storageKey, 'text/csv');
|
|
334
369
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, 'text/csv');
|
|
335
|
-
allBlocks.push(uploadId
|
|
370
|
+
allBlocks.push(uploadId
|
|
371
|
+
? notionFileUploadBlock(uploadId, block.filename || 'data.csv')
|
|
372
|
+
: notionFileBlock(block.url, block.filename || 'data.csv'));
|
|
336
373
|
}
|
|
337
374
|
else {
|
|
338
375
|
allBlocks.push(notionFileBlock(block.url, block.filename || 'data.csv'));
|
|
@@ -427,7 +464,9 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
427
464
|
*/
|
|
428
465
|
async resolveAndAppendReferences(doc, notionPageId) {
|
|
429
466
|
// Extract references from section content (no upload needed for reference pass)
|
|
430
|
-
const { references } = await sectionsToNotionBlocks(doc.sections, {
|
|
467
|
+
const { references } = await sectionsToNotionBlocks(doc.sections, {
|
|
468
|
+
extractReferences: true,
|
|
469
|
+
});
|
|
431
470
|
if (references.length === 0) {
|
|
432
471
|
return { resolved: 0, unresolved: 0 };
|
|
433
472
|
}
|
|
@@ -597,7 +636,10 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
597
636
|
// Notion page creation / update
|
|
598
637
|
// ============================================
|
|
599
638
|
async createNotionPage(doc) {
|
|
600
|
-
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
639
|
+
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
640
|
+
extractReferences: true,
|
|
641
|
+
notionClient: this.client,
|
|
642
|
+
});
|
|
601
643
|
// First batch: up to 100 blocks as children of the new page
|
|
602
644
|
const firstBatch = blocks.slice(0, MAX_BLOCKS_PER_APPEND);
|
|
603
645
|
const remainingBlocks = blocks.slice(MAX_BLOCKS_PER_APPEND);
|
|
@@ -629,7 +671,10 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
629
671
|
},
|
|
630
672
|
});
|
|
631
673
|
await this.clearPageContent(notionPageId);
|
|
632
|
-
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
674
|
+
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
675
|
+
extractReferences: true,
|
|
676
|
+
notionClient: this.client,
|
|
677
|
+
});
|
|
633
678
|
await this.appendRemainingBlocks(notionPageId, blocks);
|
|
634
679
|
}
|
|
635
680
|
async clearPageContent(pageId) {
|
package/package.json
CHANGED