@moxn/kb-migrate 0.4.6 → 0.4.7
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.d.ts +14 -1
- package/dist/client.js +27 -1
- package/dist/date-filter.d.ts +31 -0
- package/dist/date-filter.js +83 -0
- package/dist/export-notion.d.ts +22 -0
- package/dist/export-notion.js +188 -0
- package/dist/export.js +14 -1
- package/dist/index.js +218 -18
- package/dist/sources/local.d.ts +3 -0
- package/dist/sources/local.js +31 -1
- package/dist/sources/notion-blocks.d.ts +2 -0
- package/dist/sources/notion-blocks.js +52 -24
- package/dist/sources/notion-databases.js +4 -0
- package/dist/sources/notion-media.js +1 -1
- package/dist/sources/notion.d.ts +11 -0
- package/dist/sources/notion.js +44 -3
- package/dist/targets/base.d.ts +77 -0
- package/dist/targets/base.js +21 -0
- package/dist/targets/index.d.ts +5 -0
- package/dist/targets/index.js +5 -0
- package/dist/targets/notion.d.ts +93 -0
- package/dist/targets/notion.js +478 -0
- package/dist/types.d.ts +18 -2
- package/package.json +23 -1
package/dist/sources/notion.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface NotionSourceConfig extends SourceConfig {
|
|
|
15
15
|
token: string;
|
|
16
16
|
rootPageId?: string;
|
|
17
17
|
maxDepth?: number;
|
|
18
|
+
/** Date filter for source pages */
|
|
19
|
+
dateFilter?: import('../date-filter.js').DateFilter;
|
|
18
20
|
}
|
|
19
21
|
/** Info yielded to the migration runner for database import (post-document pass). */
|
|
20
22
|
export interface NotionDatabaseImportInfo {
|
|
@@ -38,7 +40,16 @@ export declare class NotionSource extends MigrationSource<NotionSourceConfig> {
|
|
|
38
40
|
private databases;
|
|
39
41
|
private databaseEntryPageIds;
|
|
40
42
|
private _documentCount;
|
|
43
|
+
private _validated;
|
|
44
|
+
/** Map of Notion database ID → KB database ID, set before extraction. */
|
|
45
|
+
private _databaseIdMap;
|
|
41
46
|
constructor(config: NotionSourceConfig);
|
|
47
|
+
/**
|
|
48
|
+
* Set the database ID map (Notion DB ID → KB DB ID).
|
|
49
|
+
* Must be called after validate() and before extract() so that
|
|
50
|
+
* child_database blocks can be converted to database_embed blocks.
|
|
51
|
+
*/
|
|
52
|
+
setDatabaseIdMap(map: Map<string, string>): void;
|
|
42
53
|
get sourceType(): string;
|
|
43
54
|
get sourceLocation(): string;
|
|
44
55
|
validate(): Promise<void>;
|
package/dist/sources/notion.js
CHANGED
|
@@ -27,11 +27,22 @@ export class NotionSource extends MigrationSource {
|
|
|
27
27
|
databases = [];
|
|
28
28
|
databaseEntryPageIds = new Set();
|
|
29
29
|
_documentCount = 0;
|
|
30
|
+
_validated = false;
|
|
31
|
+
/** Map of Notion database ID → KB database ID, set before extraction. */
|
|
32
|
+
_databaseIdMap = new Map();
|
|
30
33
|
constructor(config) {
|
|
31
34
|
super(config);
|
|
32
35
|
this.client = new NotionApiClient(config.token);
|
|
33
36
|
this.mediaDownloader = new NotionMediaDownloader();
|
|
34
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Set the database ID map (Notion DB ID → KB DB ID).
|
|
40
|
+
* Must be called after validate() and before extract() so that
|
|
41
|
+
* child_database blocks can be converted to database_embed blocks.
|
|
42
|
+
*/
|
|
43
|
+
setDatabaseIdMap(map) {
|
|
44
|
+
this._databaseIdMap = map;
|
|
45
|
+
}
|
|
35
46
|
get sourceType() {
|
|
36
47
|
return 'notion';
|
|
37
48
|
}
|
|
@@ -44,14 +55,31 @@ export class NotionSource extends MigrationSource {
|
|
|
44
55
|
// Pass 1: Discovery (validate)
|
|
45
56
|
// ============================================
|
|
46
57
|
async validate() {
|
|
58
|
+
// Idempotent — safe to call multiple times (e.g. once explicitly for
|
|
59
|
+
// pre-creating databases, then again inside runMigration).
|
|
60
|
+
if (this._validated)
|
|
61
|
+
return;
|
|
47
62
|
// 1. Test token
|
|
48
63
|
console.log('Validating Notion API token...');
|
|
49
64
|
await this.client.validateToken();
|
|
50
65
|
console.log(' Token valid.');
|
|
51
66
|
// 2. Discover all pages
|
|
52
67
|
console.log('Discovering pages...');
|
|
53
|
-
|
|
68
|
+
let allNotionPages = await this.client.searchPages();
|
|
54
69
|
console.log(` Found ${allNotionPages.length} pages in workspace.`);
|
|
70
|
+
// 2b. Apply date filter if configured
|
|
71
|
+
if (this.config.dateFilter) {
|
|
72
|
+
const { matchesDateFilter } = await import('../date-filter.js');
|
|
73
|
+
const beforeCount = allNotionPages.length;
|
|
74
|
+
allNotionPages = allNotionPages.filter((page) => matchesDateFilter(this.config.dateFilter, {
|
|
75
|
+
createdAt: page.created_time,
|
|
76
|
+
modifiedAt: page.last_edited_time,
|
|
77
|
+
}));
|
|
78
|
+
const skipped = beforeCount - allNotionPages.length;
|
|
79
|
+
if (skipped > 0) {
|
|
80
|
+
console.log(` Filtered ${skipped} pages by date criteria`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
55
83
|
// 3. Discover all databases
|
|
56
84
|
console.log('Discovering databases...');
|
|
57
85
|
const allNotionDatabases = await this.client.searchDatabases();
|
|
@@ -78,6 +106,7 @@ export class NotionSource extends MigrationSource {
|
|
|
78
106
|
console.log(` ${this.allPages.length} pages + ${this.databases.length} databases ready for import.`);
|
|
79
107
|
// 7. Initialize media downloader
|
|
80
108
|
await this.mediaDownloader.init();
|
|
109
|
+
this._validated = true;
|
|
81
110
|
}
|
|
82
111
|
async getDocumentCount() {
|
|
83
112
|
return this._documentCount;
|
|
@@ -300,7 +329,9 @@ export class NotionSource extends MigrationSource {
|
|
|
300
329
|
console.log(` Skipping empty page: ${node.title}`);
|
|
301
330
|
return null;
|
|
302
331
|
}
|
|
303
|
-
let sections = await blocksToSections(blocks, this.client, this.pagePathMap
|
|
332
|
+
let sections = await blocksToSections(blocks, this.client, this.pagePathMap, {
|
|
333
|
+
databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
|
|
334
|
+
});
|
|
304
335
|
// Download Notion-hosted media files
|
|
305
336
|
sections = await this.downloadSectionMedia(sections);
|
|
306
337
|
// Resolve cross-references
|
|
@@ -316,6 +347,10 @@ export class NotionSource extends MigrationSource {
|
|
|
316
347
|
sections,
|
|
317
348
|
sourcePath: `notion://${node.page.id}`,
|
|
318
349
|
references: references.length > 0 ? references : undefined,
|
|
350
|
+
metadata: {
|
|
351
|
+
notionPageId: normalizeId(node.page.id),
|
|
352
|
+
notionTitle: node.title,
|
|
353
|
+
},
|
|
319
354
|
};
|
|
320
355
|
}
|
|
321
356
|
catch (error) {
|
|
@@ -338,7 +373,9 @@ export class NotionSource extends MigrationSource {
|
|
|
338
373
|
// Get page blocks
|
|
339
374
|
const blocks = await this.client.getBlockChildren(entry.id);
|
|
340
375
|
if (blocks.length > 0) {
|
|
341
|
-
const contentSections = await blocksToSections(blocks, this.client, this.pagePathMap
|
|
376
|
+
const contentSections = await blocksToSections(blocks, this.client, this.pagePathMap, {
|
|
377
|
+
databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
|
|
378
|
+
});
|
|
342
379
|
sections.push(...contentSections);
|
|
343
380
|
}
|
|
344
381
|
// Download media
|
|
@@ -361,6 +398,10 @@ export class NotionSource extends MigrationSource {
|
|
|
361
398
|
],
|
|
362
399
|
sourcePath: `notion://${entry.id}`,
|
|
363
400
|
references: references.length > 0 ? references : undefined,
|
|
401
|
+
metadata: {
|
|
402
|
+
notionPageId: normalizeId(entry.id),
|
|
403
|
+
notionTitle: title,
|
|
404
|
+
},
|
|
364
405
|
};
|
|
365
406
|
}
|
|
366
407
|
catch (error) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base interface for export targets
|
|
3
|
+
*
|
|
4
|
+
* Each target (Notion, Google Docs, etc.) implements this interface
|
|
5
|
+
* to provide a consistent way to export KB documents.
|
|
6
|
+
*/
|
|
7
|
+
import type { DocumentDetail, DocumentListItem } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Result of exporting a single document to an external target
|
|
10
|
+
*/
|
|
11
|
+
export interface ExportTargetResult {
|
|
12
|
+
documentId: string;
|
|
13
|
+
documentPath: string;
|
|
14
|
+
status: 'created' | 'updated' | 'skipped' | 'failed';
|
|
15
|
+
externalId?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
duration: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Summary log for an export-to-target operation
|
|
21
|
+
*/
|
|
22
|
+
export interface ExportTargetLog {
|
|
23
|
+
timestamp: string;
|
|
24
|
+
sourceApi: string;
|
|
25
|
+
target: {
|
|
26
|
+
type: string;
|
|
27
|
+
location: string;
|
|
28
|
+
};
|
|
29
|
+
basePath: string;
|
|
30
|
+
options: {
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
conflictStrategy: 'skip' | 'update';
|
|
33
|
+
};
|
|
34
|
+
results: ExportTargetResult[];
|
|
35
|
+
summary: {
|
|
36
|
+
total: number;
|
|
37
|
+
created: number;
|
|
38
|
+
updated: number;
|
|
39
|
+
skipped: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
duration: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for export targets
|
|
46
|
+
*/
|
|
47
|
+
export interface ExportTargetConfig {
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Abstract base class for export targets
|
|
52
|
+
*/
|
|
53
|
+
export declare abstract class ExportTarget<TConfig extends ExportTargetConfig = ExportTargetConfig> {
|
|
54
|
+
protected config: TConfig;
|
|
55
|
+
constructor(config: TConfig);
|
|
56
|
+
/**
|
|
57
|
+
* Human-readable name of the target type
|
|
58
|
+
*/
|
|
59
|
+
abstract get targetType(): string;
|
|
60
|
+
/**
|
|
61
|
+
* Human-readable location identifier for logging
|
|
62
|
+
*/
|
|
63
|
+
abstract get targetLocation(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Validate the target configuration and connectivity
|
|
66
|
+
* @throws Error if configuration is invalid or target is not accessible
|
|
67
|
+
*/
|
|
68
|
+
abstract validate(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Export a single document to the target
|
|
71
|
+
*/
|
|
72
|
+
abstract exportDocument(doc: DocumentDetail, listItem: DocumentListItem, dryRun: boolean): Promise<ExportTargetResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Clean up resources after export completes
|
|
75
|
+
*/
|
|
76
|
+
cleanup(): Promise<void>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base interface for export targets
|
|
3
|
+
*
|
|
4
|
+
* Each target (Notion, Google Docs, etc.) implements this interface
|
|
5
|
+
* to provide a consistent way to export KB documents.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Abstract base class for export targets
|
|
9
|
+
*/
|
|
10
|
+
export class ExportTarget {
|
|
11
|
+
config;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Clean up resources after export completes
|
|
17
|
+
*/
|
|
18
|
+
async cleanup() {
|
|
19
|
+
// Default no-op, override if needed
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotionExportTarget — exports KB documents to Notion pages.
|
|
3
|
+
*
|
|
4
|
+
* Converts KB section content (markdown) to Notion blocks using @tryfabric/martian,
|
|
5
|
+
* then creates or updates Notion pages via the Notion API.
|
|
6
|
+
*
|
|
7
|
+
* Supports a two-pass export algorithm:
|
|
8
|
+
* - Pass 1: Create all pages (without cross-references)
|
|
9
|
+
* - Pass 2: Append reference blocks using the KB doc ID -> Notion page ID mapping
|
|
10
|
+
*
|
|
11
|
+
* Conflict detection uses the MoxnClient's notion-mapping API endpoints
|
|
12
|
+
* (when available) to find existing Notion pages for a KB document.
|
|
13
|
+
*/
|
|
14
|
+
import { ExportTarget, type ExportTargetConfig, type ExportTargetResult } from './base.js';
|
|
15
|
+
import type { DocumentDetail, DocumentListItem } from '../types.js';
|
|
16
|
+
export interface NotionExportConfig extends ExportTargetConfig {
|
|
17
|
+
/** Notion integration token */
|
|
18
|
+
notionToken: string;
|
|
19
|
+
/** Parent page ID under which to create pages */
|
|
20
|
+
parentPageId: string;
|
|
21
|
+
/** How to handle pages that already exist in Notion */
|
|
22
|
+
conflictStrategy: 'skip' | 'update';
|
|
23
|
+
/** Moxn API URL (for notion-mapping lookups) */
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
/** Moxn API key */
|
|
26
|
+
apiKey: string;
|
|
27
|
+
}
|
|
28
|
+
/** A cross-reference extracted from section content */
|
|
29
|
+
export interface ExtractedKBReference {
|
|
30
|
+
/** The KB document ID of the source document */
|
|
31
|
+
sourceDocumentId: string;
|
|
32
|
+
/** Target KB path found in markdown links */
|
|
33
|
+
targetKbPath: string;
|
|
34
|
+
/** Display text for the reference */
|
|
35
|
+
displayText: string;
|
|
36
|
+
}
|
|
37
|
+
export declare class NotionExportTarget extends ExportTarget<NotionExportConfig> {
|
|
38
|
+
private client;
|
|
39
|
+
/** Cache: kbDocumentId -> notionPageId (from mapping API) */
|
|
40
|
+
private mappingCache;
|
|
41
|
+
/** Cache: notionPageId -> true (pages we've verified exist) */
|
|
42
|
+
private verifiedPages;
|
|
43
|
+
/** Map from KB path prefix -> Notion page ID (for nested page creation) */
|
|
44
|
+
private pathToNotionId;
|
|
45
|
+
/** KB path -> KB document ID mapping (built during two-pass export) */
|
|
46
|
+
private kbPathToDocId;
|
|
47
|
+
/** KB doc ID -> Notion page ID mapping (built during pass 1) */
|
|
48
|
+
private docIdToNotionPageId;
|
|
49
|
+
constructor(config: NotionExportConfig);
|
|
50
|
+
get targetType(): string;
|
|
51
|
+
get targetLocation(): string;
|
|
52
|
+
validate(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Register a KB document in the path -> ID index.
|
|
55
|
+
* Called before pass 1 so references can be resolved in pass 2.
|
|
56
|
+
*/
|
|
57
|
+
registerDocument(doc: DocumentDetail): void;
|
|
58
|
+
/**
|
|
59
|
+
* After pass 1, record a successful export mapping.
|
|
60
|
+
* Called by the runner when a doc is created/updated successfully.
|
|
61
|
+
*/
|
|
62
|
+
registerExportedPage(kbDocumentId: string, notionPageId: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Pass 2: Resolve references for a document and append link blocks to its Notion page.
|
|
65
|
+
*
|
|
66
|
+
* Finds KB path links in the document content, resolves them to Notion page IDs
|
|
67
|
+
* (from the current export batch or pre-existing mappings), and appends
|
|
68
|
+
* mention/link blocks to the Notion page.
|
|
69
|
+
*
|
|
70
|
+
* Returns the number of references resolved.
|
|
71
|
+
*/
|
|
72
|
+
resolveAndAppendReferences(doc: DocumentDetail, notionPageId: string): Promise<{
|
|
73
|
+
resolved: number;
|
|
74
|
+
unresolved: number;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a KB path to a Notion page ID.
|
|
78
|
+
*
|
|
79
|
+
* Checks three sources in order:
|
|
80
|
+
* 1. Current export batch (docIdToNotionPageId from pass 1)
|
|
81
|
+
* 2. Local mapping cache
|
|
82
|
+
* 3. Remote notion-mapping API
|
|
83
|
+
*/
|
|
84
|
+
private resolveKBPathToNotionPage;
|
|
85
|
+
exportDocument(doc: DocumentDetail, listItem: DocumentListItem, dryRun: boolean): Promise<ExportTargetResult>;
|
|
86
|
+
cleanup(): Promise<void>;
|
|
87
|
+
private createNotionPage;
|
|
88
|
+
private updateNotionPage;
|
|
89
|
+
private clearPageContent;
|
|
90
|
+
private appendRemainingBlocks;
|
|
91
|
+
private findExistingNotionPage;
|
|
92
|
+
private recordMapping;
|
|
93
|
+
}
|