@moxn/kb-migrate 0.4.24 → 0.4.25
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 +1 -1
- package/dist/index.js +21 -1
- package/dist/sources/notion-databases.d.ts +3 -3
- package/dist/sources/notion-databases.js +26 -3
- package/dist/sources/notion-references.d.ts +1 -0
- package/dist/sources/notion-references.js +9 -6
- package/dist/sources/notion.d.ts +2 -0
- package/dist/sources/notion.js +13 -6
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export declare class MoxnClient {
|
|
|
53
53
|
*/
|
|
54
54
|
addDatabaseColumn(databaseId: string, input: {
|
|
55
55
|
name: string;
|
|
56
|
-
type: 'select' | 'multi_select' | 'status' | 'checkbox' | 'number' | 'date' | 'text' | 'url' | 'email';
|
|
56
|
+
type: 'select' | 'multi_select' | 'status' | 'checkbox' | 'number' | 'date' | 'text' | 'url' | 'email' | 'page_ref';
|
|
57
57
|
optionTagIds?: string[];
|
|
58
58
|
newOptionParentPath?: string;
|
|
59
59
|
config?: Record<string, unknown>;
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,9 @@ async function runMigration(source, options) {
|
|
|
63
63
|
// Circuit breaker: abort after too many consecutive failures
|
|
64
64
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
65
65
|
console.error(`\nAborting: ${MAX_CONSECUTIVE_FAILURES} consecutive failures. ` +
|
|
66
|
-
'Last error: ' +
|
|
66
|
+
'Last error: ' +
|
|
67
|
+
(result.error || 'unknown') +
|
|
68
|
+
'\n' +
|
|
67
69
|
'Fix the underlying issue and retry. Remaining documents will be skipped.');
|
|
68
70
|
break;
|
|
69
71
|
}
|
|
@@ -688,6 +690,8 @@ async function importNotionDatabase(client, dbImport, log, options, preCreatedDb
|
|
|
688
690
|
// 3. Link entries and assign tags
|
|
689
691
|
// Build a map of KB path → { documentId, branchId } from migration results
|
|
690
692
|
const docByPath = new Map();
|
|
693
|
+
// Build a map of Notion page ID → KB document ID for page_ref resolution
|
|
694
|
+
const notionIdToDocId = new Map();
|
|
691
695
|
for (const result of log.results) {
|
|
692
696
|
if (result.documentId && result.branchId) {
|
|
693
697
|
docByPath.set(result.documentPath, {
|
|
@@ -695,6 +699,9 @@ async function importNotionDatabase(client, dbImport, log, options, preCreatedDb
|
|
|
695
699
|
branchId: result.branchId,
|
|
696
700
|
});
|
|
697
701
|
}
|
|
702
|
+
if (result.sourcePageId && result.documentId) {
|
|
703
|
+
notionIdToDocId.set(result.sourcePageId, result.documentId);
|
|
704
|
+
}
|
|
698
705
|
}
|
|
699
706
|
let linkedCount = 0;
|
|
700
707
|
for (const entry of entries) {
|
|
@@ -749,6 +756,19 @@ async function importNotionDatabase(client, dbImport, log, options, preCreatedDb
|
|
|
749
756
|
case 'email':
|
|
750
757
|
value = scalarVal.textValue ?? null;
|
|
751
758
|
break;
|
|
759
|
+
case 'page_ref': {
|
|
760
|
+
// Resolve notion:XXX markers to KB document IDs
|
|
761
|
+
const refs = JSON.parse(scalarVal.textValue ?? '[]');
|
|
762
|
+
const resolvedRefs = refs.map((ref) => {
|
|
763
|
+
if (ref.startsWith('notion:')) {
|
|
764
|
+
const notionId = ref.substring(7);
|
|
765
|
+
return notionIdToDocId.get(notionId) ?? ref;
|
|
766
|
+
}
|
|
767
|
+
return ref;
|
|
768
|
+
});
|
|
769
|
+
value = JSON.stringify(resolvedRefs);
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
752
772
|
}
|
|
753
773
|
if (value != null) {
|
|
754
774
|
try {
|
|
@@ -30,8 +30,8 @@ export interface MappedColumn {
|
|
|
30
30
|
export interface ScalarColumn {
|
|
31
31
|
notionPropertyId: string;
|
|
32
32
|
notionPropertyName: string;
|
|
33
|
-
notionType: 'number' | 'date' | 'url' | 'email' | 'rich_text' | 'checkbox';
|
|
34
|
-
moxnType: 'number' | 'date' | 'url' | 'email' | 'text';
|
|
33
|
+
notionType: 'number' | 'date' | 'url' | 'email' | 'rich_text' | 'checkbox' | 'relation';
|
|
34
|
+
moxnType: 'number' | 'date' | 'url' | 'email' | 'text' | 'page_ref';
|
|
35
35
|
}
|
|
36
36
|
/** Parsed Notion column that will be rendered as text in a Properties section. */
|
|
37
37
|
export interface UnmappedColumn {
|
|
@@ -50,7 +50,7 @@ export interface ParsedDatabaseSchema {
|
|
|
50
50
|
/** Scalar value for a single database entry property. */
|
|
51
51
|
export interface ScalarValue {
|
|
52
52
|
columnName: string;
|
|
53
|
-
moxnType: 'number' | 'date' | 'url' | 'email' | 'text';
|
|
53
|
+
moxnType: 'number' | 'date' | 'url' | 'email' | 'text' | 'page_ref';
|
|
54
54
|
textValue?: string | null;
|
|
55
55
|
numberValue?: number | null;
|
|
56
56
|
dateValue?: string | null;
|
|
@@ -123,6 +123,14 @@ export function parseDatabaseSchema(db) {
|
|
|
123
123
|
moxnType: 'text',
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
|
+
else if (prop.type === 'relation') {
|
|
127
|
+
scalarColumns.push({
|
|
128
|
+
notionPropertyId: prop.id,
|
|
129
|
+
notionPropertyName: propName,
|
|
130
|
+
notionType: 'relation',
|
|
131
|
+
moxnType: 'page_ref',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
126
134
|
else {
|
|
127
135
|
// Warn on property types that have no meaningful static value
|
|
128
136
|
if (prop.type === 'button' || prop.type === 'ai_text') {
|
|
@@ -175,9 +183,24 @@ export function parseEntryValues(page, schema) {
|
|
|
175
183
|
// Check if this is a scalar column
|
|
176
184
|
const scalar = scalarByName.get(propName);
|
|
177
185
|
if (scalar) {
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
186
|
+
// page_ref columns extract relation page IDs (handled differently from standard scalars)
|
|
187
|
+
if (scalar.moxnType === 'page_ref') {
|
|
188
|
+
const refs = (propValue.relation ?? [])
|
|
189
|
+
.map((r) => normalizeId(r.id))
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
if (refs.length > 0) {
|
|
192
|
+
scalarValues.set(propName, {
|
|
193
|
+
columnName: propName,
|
|
194
|
+
moxnType: 'page_ref',
|
|
195
|
+
textValue: JSON.stringify(refs.map((id) => `notion:${id}`)),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const extracted = extractScalarValue(propValue, scalar);
|
|
201
|
+
if (extracted) {
|
|
202
|
+
scalarValues.set(propName, extracted);
|
|
203
|
+
}
|
|
181
204
|
}
|
|
182
205
|
continue;
|
|
183
206
|
}
|
|
@@ -125,17 +125,19 @@ export function resolveNotionReferences(sections, mapping) {
|
|
|
125
125
|
text = text.replace(RELATION_REF_RE, (_match, rawId) => {
|
|
126
126
|
const nid = normalizeId(rawId);
|
|
127
127
|
const kbPath = mapping.notionIdToKbPath.get(nid);
|
|
128
|
+
const title = mapping.notionIdToTitle?.get(nid);
|
|
128
129
|
if (kbPath) {
|
|
130
|
+
const displayText = title || kbPath;
|
|
129
131
|
references.push({
|
|
130
132
|
sectionIndex,
|
|
131
133
|
targetNotionId: nid,
|
|
132
134
|
targetKbPath: kbPath,
|
|
133
|
-
displayText
|
|
135
|
+
displayText,
|
|
134
136
|
});
|
|
135
|
-
return `[${
|
|
137
|
+
return `[${displayText}](${kbPath})`;
|
|
136
138
|
}
|
|
137
|
-
// Unresolved — keep the ID
|
|
138
|
-
return nid;
|
|
139
|
+
// Unresolved — show title if available, otherwise keep the ID
|
|
140
|
+
return title || nid;
|
|
139
141
|
});
|
|
140
142
|
return { blockType: 'text', text };
|
|
141
143
|
});
|
|
@@ -151,9 +153,10 @@ export function resolveRelationIds(value, mapping) {
|
|
|
151
153
|
return value.replace(RELATION_REF_RE, (_match, rawId) => {
|
|
152
154
|
const nid = normalizeId(rawId);
|
|
153
155
|
const kbPath = mapping.notionIdToKbPath.get(nid);
|
|
156
|
+
const title = mapping.notionIdToTitle?.get(nid);
|
|
154
157
|
if (kbPath) {
|
|
155
|
-
return `[${kbPath}](${kbPath})`;
|
|
158
|
+
return `[${title || kbPath}](${kbPath})`;
|
|
156
159
|
}
|
|
157
|
-
return nid;
|
|
160
|
+
return title || nid;
|
|
158
161
|
});
|
|
159
162
|
}
|
package/dist/sources/notion.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare class NotionSource extends MigrationSource<NotionSourceConfig> {
|
|
|
48
48
|
private pageTree;
|
|
49
49
|
private allPages;
|
|
50
50
|
private pagePathMap;
|
|
51
|
+
private pageTitleMap;
|
|
51
52
|
private databases;
|
|
52
53
|
private databaseEntryPageIds;
|
|
53
54
|
private _documentCount;
|
|
@@ -96,6 +97,7 @@ export interface PageTreeResult {
|
|
|
96
97
|
tree: PageTreeNode[];
|
|
97
98
|
allPages: PageTreeNode[];
|
|
98
99
|
pagePathMap: PagePathMap;
|
|
100
|
+
pageTitleMap: Map<string, string>;
|
|
99
101
|
databaseEntryPageIds: Set<string>;
|
|
100
102
|
}
|
|
101
103
|
/**
|
package/dist/sources/notion.js
CHANGED
|
@@ -24,6 +24,7 @@ export class NotionSource extends MigrationSource {
|
|
|
24
24
|
pageTree = [];
|
|
25
25
|
allPages = []; // flat list, depth-first order
|
|
26
26
|
pagePathMap = new Map();
|
|
27
|
+
pageTitleMap = new Map();
|
|
27
28
|
databases = [];
|
|
28
29
|
databaseEntryPageIds = new Set();
|
|
29
30
|
_documentCount = 0;
|
|
@@ -240,6 +241,7 @@ export class NotionSource extends MigrationSource {
|
|
|
240
241
|
this.pageTree = result.tree;
|
|
241
242
|
this.allPages = result.allPages;
|
|
242
243
|
this.pagePathMap = result.pagePathMap;
|
|
244
|
+
this.pageTitleMap = result.pageTitleMap;
|
|
243
245
|
this.databaseEntryPageIds = result.databaseEntryPageIds;
|
|
244
246
|
}
|
|
245
247
|
// ============================================
|
|
@@ -291,7 +293,7 @@ export class NotionSource extends MigrationSource {
|
|
|
291
293
|
// Download Notion-hosted media files
|
|
292
294
|
sections = await this.downloadSectionMedia(sections);
|
|
293
295
|
// Resolve cross-references
|
|
294
|
-
const { sections: resolvedSections, references } = resolveNotionReferences(sections, { notionIdToKbPath: this.pagePathMap });
|
|
296
|
+
const { sections: resolvedSections, references } = resolveNotionReferences(sections, { notionIdToKbPath: this.pagePathMap, notionIdToTitle: this.pageTitleMap });
|
|
295
297
|
sections = resolvedSections;
|
|
296
298
|
if (sections.length === 0) {
|
|
297
299
|
this._skippedNoContent.push(node.title);
|
|
@@ -339,7 +341,7 @@ export class NotionSource extends MigrationSource {
|
|
|
339
341
|
// Download media
|
|
340
342
|
let processedSections = await this.downloadSectionMedia(sections);
|
|
341
343
|
// Resolve cross-references
|
|
342
|
-
const { sections: resolvedSections, references } = resolveNotionReferences(processedSections, { notionIdToKbPath: this.pagePathMap });
|
|
344
|
+
const { sections: resolvedSections, references } = resolveNotionReferences(processedSections, { notionIdToKbPath: this.pagePathMap, notionIdToTitle: this.pageTitleMap });
|
|
343
345
|
processedSections = resolvedSections;
|
|
344
346
|
const nid = normalizeId(entry.id);
|
|
345
347
|
const kbPath = this.pagePathMap.get(nid) ?? slug;
|
|
@@ -392,6 +394,7 @@ export class NotionSource extends MigrationSource {
|
|
|
392
394
|
*/
|
|
393
395
|
export function buildPageTree(pages, options) {
|
|
394
396
|
const pagePathMap = new Map();
|
|
397
|
+
const pageTitleMap = new Map();
|
|
395
398
|
const databaseEntryPageIds = new Set();
|
|
396
399
|
const tree = [];
|
|
397
400
|
const allPages = [];
|
|
@@ -493,7 +496,11 @@ export function buildPageTree(pages, options) {
|
|
|
493
496
|
}
|
|
494
497
|
// Build tree recursively
|
|
495
498
|
const buildNode = (page, parentPath, depth, siblingSlugCounts) => {
|
|
496
|
-
|
|
499
|
+
const nid = normalizeId(page.id);
|
|
500
|
+
const isDatabaseSubtree = syntheticDatabaseIds.has(nid) || databaseEntryPageIds.has(nid);
|
|
501
|
+
if (options?.maxDepth !== undefined &&
|
|
502
|
+
depth > options.maxDepth &&
|
|
503
|
+
!isDatabaseSubtree) {
|
|
497
504
|
return null;
|
|
498
505
|
}
|
|
499
506
|
const title = getPageTitle(page);
|
|
@@ -505,7 +512,6 @@ export function buildPageTree(pages, options) {
|
|
|
505
512
|
slug = `${slug}-${existing + 1}`;
|
|
506
513
|
}
|
|
507
514
|
const kbPath = parentPath ? `${parentPath}/${slug}` : slug;
|
|
508
|
-
const nid = normalizeId(page.id);
|
|
509
515
|
const isDatabaseEntry = databaseEntryPageIds.has(nid);
|
|
510
516
|
const node = {
|
|
511
517
|
page,
|
|
@@ -517,8 +523,9 @@ export function buildPageTree(pages, options) {
|
|
|
517
523
|
parentDatabaseId: page.parent.type === 'database_id' ? page.parent.database_id : undefined,
|
|
518
524
|
children: [],
|
|
519
525
|
};
|
|
520
|
-
// Register in path map
|
|
526
|
+
// Register in path map and title map
|
|
521
527
|
pagePathMap.set(nid, kbPath);
|
|
528
|
+
pageTitleMap.set(nid, title);
|
|
522
529
|
// Process children
|
|
523
530
|
const childPages = childrenByParent.get(nid) ?? [];
|
|
524
531
|
const childSlugCounts = new Map();
|
|
@@ -546,7 +553,7 @@ export function buildPageTree(pages, options) {
|
|
|
546
553
|
}
|
|
547
554
|
};
|
|
548
555
|
flatten(tree);
|
|
549
|
-
return { tree, allPages, pagePathMap, databaseEntryPageIds };
|
|
556
|
+
return { tree, allPages, pagePathMap, pageTitleMap, databaseEntryPageIds };
|
|
550
557
|
}
|
|
551
558
|
/** Extract the parent page ID from a Notion page's parent field. */
|
|
552
559
|
function getParentPageId(page) {
|
package/package.json
CHANGED