@moxn/kb-migrate 0.4.21 → 0.4.22

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.
@@ -10,6 +10,7 @@
10
10
  import type { ExtractedDocument } from '../types.js';
11
11
  import { MigrationSource, type SourceConfig } from './base.js';
12
12
  import type { NotionPage } from './notion-api.js';
13
+ import { type PagePathMap } from './notion-blocks.js';
13
14
  import { type ParsedDatabaseSchema } from './notion-databases.js';
14
15
  export interface NotionSourceConfig extends SourceConfig {
15
16
  token: string;
@@ -18,6 +19,15 @@ export interface NotionSourceConfig extends SourceConfig {
18
19
  /** Date filter for source pages */
19
20
  dateFilter?: import('../date-filter.js').DateFilter;
20
21
  }
22
+ export interface PageTreeNode {
23
+ page: NotionPage;
24
+ title: string;
25
+ slug: string;
26
+ children: PageTreeNode[];
27
+ kbPath: string;
28
+ isDatabaseEntry: boolean;
29
+ parentDatabaseId?: string;
30
+ }
21
31
  /** Info yielded to the migration runner for database import (post-document pass). */
22
32
  export interface NotionDatabaseImportInfo {
23
33
  notionDatabaseId: string;
@@ -62,14 +72,35 @@ export declare class NotionSource extends MigrationSource<NotionSourceConfig> {
62
72
  extract(): AsyncGenerator<ExtractedDocument, void, unknown>;
63
73
  /** Clean up temp files after migration completes. */
64
74
  cleanup(): Promise<void>;
65
- private buildPageTree;
66
- private getParentPageId;
75
+ private buildPageTreeInternal;
67
76
  private processDatabases;
68
77
  private getDatabaseParentId;
69
78
  private extractPage;
70
79
  private extractDatabaseEntry;
71
80
  private downloadSectionMedia;
72
81
  }
82
+ /** Options for building the page tree. */
83
+ export interface BuildPageTreeOptions {
84
+ rootPageId?: string;
85
+ maxDepth?: number;
86
+ }
87
+ /** Result of building the page tree. */
88
+ export interface PageTreeResult {
89
+ tree: PageTreeNode[];
90
+ allPages: PageTreeNode[];
91
+ pagePathMap: PagePathMap;
92
+ databaseEntryPageIds: Set<string>;
93
+ }
94
+ /**
95
+ * Build a hierarchical page tree from a flat list of Notion pages.
96
+ *
97
+ * Computes kbPath for each page based on parent-child relationships,
98
+ * deduplicates sibling slugs, and identifies database entries.
99
+ *
100
+ * Exported so the import-orchestrator can build hierarchical paths
101
+ * without instantiating a full NotionSource.
102
+ */
103
+ export declare function buildPageTree(pages: NotionPage[], options?: BuildPageTreeOptions): PageTreeResult;
73
104
  /**
74
105
  * Slugify a page title for use as a KB path segment.
75
106
  * Lowercase, spaces→hyphens, strip special chars, ltree-compatible.
@@ -85,7 +85,7 @@ export class NotionSource extends MigrationSource {
85
85
  const allNotionDatabases = await this.client.searchDatabases();
86
86
  console.log(` Found ${allNotionDatabases.length} databases.`);
87
87
  // 4. Build page tree
88
- this.buildPageTree(allNotionPages);
88
+ this.buildPageTreeInternal(allNotionPages);
89
89
  // 5. Process databases — identify entries and build schemas
90
90
  await this.processDatabases(allNotionDatabases);
91
91
  // 6. Count and validate
@@ -169,123 +169,17 @@ export class NotionSource extends MigrationSource {
169
169
  await this.mediaDownloader.cleanup();
170
170
  }
171
171
  // ============================================
172
- // Tree building
172
+ // Tree building (delegates to standalone function)
173
173
  // ============================================
174
- buildPageTree(pages) {
175
- // Build lookup maps
176
- const pageById = new Map();
177
- const childrenByParent = new Map();
178
- for (const page of pages) {
179
- const nid = normalizeId(page.id);
180
- pageById.set(nid, page);
181
- const parentId = this.getParentPageId(page);
182
- if (parentId) {
183
- const npid = normalizeId(parentId);
184
- if (!childrenByParent.has(npid)) {
185
- childrenByParent.set(npid, []);
186
- }
187
- childrenByParent.get(npid).push(page);
188
- }
189
- }
190
- // Identify database entries
191
- for (const page of pages) {
192
- if (page.parent.type === 'database_id' && page.parent.database_id) {
193
- this.databaseEntryPageIds.add(normalizeId(page.id));
194
- }
195
- }
196
- // Find root pages
197
- const rootPages = [];
198
- if (this.config.rootPageId) {
199
- // Subtree mode: start from specified page
200
- const rootId = normalizeId(this.config.rootPageId);
201
- const rootPage = pageById.get(rootId);
202
- if (rootPage) {
203
- rootPages.push(rootPage);
204
- }
205
- else {
206
- // Root page itself wasn't in search results — search for its children
207
- const children = childrenByParent.get(rootId) ?? [];
208
- rootPages.push(...children);
209
- }
210
- }
211
- else {
212
- // Full workspace mode: pages with workspace parent or no parent in our set
213
- for (const page of pages) {
214
- if (page.parent.type === 'workspace') {
215
- rootPages.push(page);
216
- }
217
- else {
218
- const parentId = this.getParentPageId(page);
219
- if (parentId && !pageById.has(normalizeId(parentId))) {
220
- // Parent is not in our page set — treat as root
221
- if (page.parent.type !== 'database_id') {
222
- rootPages.push(page);
223
- }
224
- }
225
- }
226
- }
227
- }
228
- // Build tree recursively
229
- const buildNode = (page, parentPath, depth, siblingSlugCounts) => {
230
- if (this.config.maxDepth !== undefined && depth > this.config.maxDepth) {
231
- return null;
232
- }
233
- const title = getPageTitle(page);
234
- let slug = slugify(title);
235
- // Deduplicate sibling slugs
236
- const existing = siblingSlugCounts.get(slug) ?? 0;
237
- siblingSlugCounts.set(slug, existing + 1);
238
- if (existing > 0) {
239
- slug = `${slug}-${existing + 1}`;
240
- }
241
- const kbPath = parentPath ? `${parentPath}/${slug}` : slug;
242
- const nid = normalizeId(page.id);
243
- const isDatabaseEntry = this.databaseEntryPageIds.has(nid);
244
- const node = {
245
- page,
246
- title,
247
- slug,
248
- kbPath,
249
- isDatabaseEntry,
250
- parentDatabaseId: page.parent.type === 'database_id' ? page.parent.database_id : undefined,
251
- children: [],
252
- };
253
- // Register in path map
254
- this.pagePathMap.set(nid, kbPath);
255
- // Process children
256
- const childPages = childrenByParent.get(nid) ?? [];
257
- const childSlugCounts = new Map();
258
- for (const childPage of childPages) {
259
- const childNode = buildNode(childPage, kbPath, depth + 1, childSlugCounts);
260
- if (childNode) {
261
- node.children.push(childNode);
262
- }
263
- }
264
- return node;
265
- };
266
- // Build roots
267
- const rootSlugCounts = new Map();
268
- for (const rootPage of rootPages) {
269
- const node = buildNode(rootPage, '', 0, rootSlugCounts);
270
- if (node) {
271
- this.pageTree.push(node);
272
- }
273
- }
274
- // Flatten tree to depth-first list
275
- const flatten = (nodes) => {
276
- for (const node of nodes) {
277
- this.allPages.push(node);
278
- flatten(node.children);
279
- }
280
- };
281
- flatten(this.pageTree);
282
- }
283
- getParentPageId(page) {
284
- if (page.parent.type === 'page_id')
285
- return page.parent.page_id ?? null;
286
- if (page.parent.type === 'block_id')
287
- return page.parent.block_id ?? null;
288
- return null;
174
+ buildPageTreeInternal(pages) {
175
+ const result = buildPageTree(pages, {
176
+ rootPageId: this.config.rootPageId,
177
+ maxDepth: this.config.maxDepth,
178
+ });
179
+ this.pageTree = result.tree;
180
+ this.allPages = result.allPages;
181
+ this.pagePathMap = result.pagePathMap;
182
+ this.databaseEntryPageIds = result.databaseEntryPageIds;
289
183
  }
290
184
  // ============================================
291
185
  // Database processing
@@ -421,6 +315,133 @@ export class NotionSource extends MigrationSource {
421
315
  return result;
422
316
  }
423
317
  }
318
+ /**
319
+ * Build a hierarchical page tree from a flat list of Notion pages.
320
+ *
321
+ * Computes kbPath for each page based on parent-child relationships,
322
+ * deduplicates sibling slugs, and identifies database entries.
323
+ *
324
+ * Exported so the import-orchestrator can build hierarchical paths
325
+ * without instantiating a full NotionSource.
326
+ */
327
+ export function buildPageTree(pages, options) {
328
+ const pagePathMap = new Map();
329
+ const databaseEntryPageIds = new Set();
330
+ const tree = [];
331
+ const allPages = [];
332
+ // Build lookup maps
333
+ const pageById = new Map();
334
+ const childrenByParent = new Map();
335
+ for (const page of pages) {
336
+ const nid = normalizeId(page.id);
337
+ pageById.set(nid, page);
338
+ const parentId = getParentPageId(page);
339
+ if (parentId) {
340
+ const npid = normalizeId(parentId);
341
+ if (!childrenByParent.has(npid)) {
342
+ childrenByParent.set(npid, []);
343
+ }
344
+ childrenByParent.get(npid).push(page);
345
+ }
346
+ }
347
+ // Identify database entries
348
+ for (const page of pages) {
349
+ if (page.parent.type === 'database_id' && page.parent.database_id) {
350
+ databaseEntryPageIds.add(normalizeId(page.id));
351
+ }
352
+ }
353
+ // Find root pages
354
+ const rootPages = [];
355
+ if (options?.rootPageId) {
356
+ const rootId = normalizeId(options.rootPageId);
357
+ const rootPage = pageById.get(rootId);
358
+ if (rootPage) {
359
+ rootPages.push(rootPage);
360
+ }
361
+ else {
362
+ const children = childrenByParent.get(rootId) ?? [];
363
+ rootPages.push(...children);
364
+ }
365
+ }
366
+ else {
367
+ for (const page of pages) {
368
+ if (page.parent.type === 'workspace') {
369
+ rootPages.push(page);
370
+ }
371
+ else {
372
+ const parentId = getParentPageId(page);
373
+ if (parentId && !pageById.has(normalizeId(parentId))) {
374
+ if (page.parent.type !== 'database_id') {
375
+ rootPages.push(page);
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ // Build tree recursively
382
+ const buildNode = (page, parentPath, depth, siblingSlugCounts) => {
383
+ if (options?.maxDepth !== undefined && depth > options.maxDepth) {
384
+ return null;
385
+ }
386
+ const title = getPageTitle(page);
387
+ let slug = slugify(title);
388
+ // Deduplicate sibling slugs
389
+ const existing = siblingSlugCounts.get(slug) ?? 0;
390
+ siblingSlugCounts.set(slug, existing + 1);
391
+ if (existing > 0) {
392
+ slug = `${slug}-${existing + 1}`;
393
+ }
394
+ const kbPath = parentPath ? `${parentPath}/${slug}` : slug;
395
+ const nid = normalizeId(page.id);
396
+ const isDatabaseEntry = databaseEntryPageIds.has(nid);
397
+ const node = {
398
+ page,
399
+ title,
400
+ slug,
401
+ kbPath,
402
+ isDatabaseEntry,
403
+ parentDatabaseId: page.parent.type === 'database_id' ? page.parent.database_id : undefined,
404
+ children: [],
405
+ };
406
+ // Register in path map
407
+ pagePathMap.set(nid, kbPath);
408
+ // Process children
409
+ const childPages = childrenByParent.get(nid) ?? [];
410
+ const childSlugCounts = new Map();
411
+ for (const childPage of childPages) {
412
+ const childNode = buildNode(childPage, kbPath, depth + 1, childSlugCounts);
413
+ if (childNode) {
414
+ node.children.push(childNode);
415
+ }
416
+ }
417
+ return node;
418
+ };
419
+ // Build roots
420
+ const rootSlugCounts = new Map();
421
+ for (const rootPage of rootPages) {
422
+ const node = buildNode(rootPage, '', 0, rootSlugCounts);
423
+ if (node) {
424
+ tree.push(node);
425
+ }
426
+ }
427
+ // Flatten tree to depth-first list
428
+ const flatten = (nodes) => {
429
+ for (const node of nodes) {
430
+ allPages.push(node);
431
+ flatten(node.children);
432
+ }
433
+ };
434
+ flatten(tree);
435
+ return { tree, allPages, pagePathMap, databaseEntryPageIds };
436
+ }
437
+ /** Extract the parent page ID from a Notion page's parent field. */
438
+ function getParentPageId(page) {
439
+ if (page.parent.type === 'page_id')
440
+ return page.parent.page_id ?? null;
441
+ if (page.parent.type === 'block_id')
442
+ return page.parent.block_id ?? null;
443
+ return null;
444
+ }
424
445
  // ============================================
425
446
  // Path helpers
426
447
  // ============================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.21",
3
+ "version": "0.4.22",
4
4
  "description": "Migration tool for importing documents into Moxn Knowledge Base from local files, Notion, Google Docs, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",