@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.
@@ -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>;
@@ -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
- const allNotionPages = await this.client.searchPages();
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,5 @@
1
+ /**
2
+ * Target registry and factory
3
+ */
4
+ export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog } from './base.js';
5
+ export { NotionExportTarget, type NotionExportConfig } from './notion.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Target registry and factory
3
+ */
4
+ export { ExportTarget } from './base.js';
5
+ export { NotionExportTarget } from './notion.js';
@@ -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
+ }