@moxn/kb-migrate 0.4.7 → 0.4.9
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 +55 -0
- package/dist/client.js +38 -0
- package/dist/export-notion.js +54 -0
- package/dist/targets/index.d.ts +1 -0
- package/dist/targets/index.js +1 -0
- package/dist/targets/notion-database-export.d.ts +36 -0
- package/dist/targets/notion-database-export.js +251 -0
- package/dist/targets/notion.js +28 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -80,6 +80,61 @@ export declare class MoxnClient {
|
|
|
80
80
|
* Assign a tag to a document.
|
|
81
81
|
*/
|
|
82
82
|
assignTag(documentId: string, tagId: string, branchId: string): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* List all KB databases for the tenant.
|
|
85
|
+
*/
|
|
86
|
+
listDatabases(): Promise<Array<{
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
description: string | null;
|
|
90
|
+
}>>;
|
|
91
|
+
/**
|
|
92
|
+
* Get fully resolved database with columns, options, and document property values.
|
|
93
|
+
*/
|
|
94
|
+
resolveDatabase(databaseId: string): Promise<{
|
|
95
|
+
databaseId: string;
|
|
96
|
+
databaseName: string;
|
|
97
|
+
columns: Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
type: 'select' | 'multi_select';
|
|
101
|
+
position: number;
|
|
102
|
+
newOptionParentPath: string | null;
|
|
103
|
+
options: Array<{
|
|
104
|
+
tagId: string;
|
|
105
|
+
tagName: string;
|
|
106
|
+
tagPath: string;
|
|
107
|
+
tagColor: string | null;
|
|
108
|
+
}>;
|
|
109
|
+
}>;
|
|
110
|
+
documents: Array<{
|
|
111
|
+
documentId: string;
|
|
112
|
+
documentName: string | null;
|
|
113
|
+
documentPath: string | null;
|
|
114
|
+
properties: Record<string, {
|
|
115
|
+
columnId: string;
|
|
116
|
+
columnName: string;
|
|
117
|
+
columnType: 'select' | 'multi_select';
|
|
118
|
+
values: Array<{
|
|
119
|
+
tagId: string;
|
|
120
|
+
tagName: string;
|
|
121
|
+
tagPath: string;
|
|
122
|
+
tagColor: string | null;
|
|
123
|
+
}>;
|
|
124
|
+
}>;
|
|
125
|
+
}>;
|
|
126
|
+
}>;
|
|
127
|
+
/**
|
|
128
|
+
* Get Notion database mapping for a KB database.
|
|
129
|
+
*/
|
|
130
|
+
getNotionDatabaseMapping(kbDatabaseId: string): Promise<{
|
|
131
|
+
mapping: {
|
|
132
|
+
id: string;
|
|
133
|
+
kbDatabaseId: string;
|
|
134
|
+
notionDatabaseId: string;
|
|
135
|
+
notionDatabaseTitle: string | null;
|
|
136
|
+
} | null;
|
|
137
|
+
}>;
|
|
83
138
|
/**
|
|
84
139
|
* Create or upsert a Notion database mapping.
|
|
85
140
|
*/
|
package/dist/client.js
CHANGED
|
@@ -401,6 +401,44 @@ export class MoxnClient {
|
|
|
401
401
|
throw new Error(body.error || `Failed to assign tag: ${response.status}`);
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* List all KB databases for the tenant.
|
|
406
|
+
*/
|
|
407
|
+
async listDatabases() {
|
|
408
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
409
|
+
headers: { 'x-api-key': this.apiKey },
|
|
410
|
+
});
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
const body = await response.json().catch(() => ({}));
|
|
413
|
+
throw new Error(body.error || `Failed to list databases: ${response.status}`);
|
|
414
|
+
}
|
|
415
|
+
const data = await response.json();
|
|
416
|
+
return data.databases;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get fully resolved database with columns, options, and document property values.
|
|
420
|
+
*/
|
|
421
|
+
async resolveDatabase(databaseId) {
|
|
422
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/resolve`, {
|
|
423
|
+
headers: { 'x-api-key': this.apiKey },
|
|
424
|
+
});
|
|
425
|
+
if (!response.ok) {
|
|
426
|
+
const body = await response.json().catch(() => ({}));
|
|
427
|
+
throw new Error(body.error || `Failed to resolve database: ${response.status}`);
|
|
428
|
+
}
|
|
429
|
+
return response.json();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Get Notion database mapping for a KB database.
|
|
433
|
+
*/
|
|
434
|
+
async getNotionDatabaseMapping(kbDatabaseId) {
|
|
435
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases/by-kb-database/${kbDatabaseId}`, { headers: { 'x-api-key': this.apiKey } });
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
const body = await response.json().catch(() => ({}));
|
|
438
|
+
throw new Error(body.error || `Failed to get database mapping: ${response.status}`);
|
|
439
|
+
}
|
|
440
|
+
return response.json();
|
|
441
|
+
}
|
|
404
442
|
/**
|
|
405
443
|
* Create or upsert a Notion database mapping.
|
|
406
444
|
*/
|
package/dist/export-notion.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* Pass 1: Create/update all Notion pages (content without cross-references)
|
|
8
8
|
* Pass 2: Resolve KB path references to Notion page IDs and append link blocks
|
|
9
9
|
*/
|
|
10
|
+
import { Client } from '@notionhq/client';
|
|
10
11
|
import { MoxnClient } from './client.js';
|
|
11
12
|
import { matchesDateFilter } from './date-filter.js';
|
|
12
13
|
import { NotionExportTarget } from './targets/notion.js';
|
|
14
|
+
import { exportDatabase } from './targets/notion-database-export.js';
|
|
13
15
|
export async function runNotionExport(options) {
|
|
14
16
|
const startTime = Date.now();
|
|
15
17
|
// Create Moxn client for reading docs
|
|
@@ -107,6 +109,58 @@ export async function runNotionExport(options) {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
// ============================================
|
|
112
|
+
// Pass 1.5: Export databases
|
|
113
|
+
// ============================================
|
|
114
|
+
if (!options.dryRun) {
|
|
115
|
+
// Collect unique database IDs from exported documents
|
|
116
|
+
const databaseIdsToExport = new Set();
|
|
117
|
+
for (const { detail: doc } of docDetails) {
|
|
118
|
+
for (const section of doc.sections) {
|
|
119
|
+
for (const block of section.content) {
|
|
120
|
+
if (block.blockType === 'database_embed' && block.databaseId) {
|
|
121
|
+
databaseIdsToExport.add(block.databaseId);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (databaseIdsToExport.size > 0) {
|
|
127
|
+
console.log(`\nPass 1.5: Exporting ${databaseIdsToExport.size} databases to Notion...`);
|
|
128
|
+
const dbCtx = {
|
|
129
|
+
notionClient: new Client({ auth: options.notionToken }),
|
|
130
|
+
moxnClient: client,
|
|
131
|
+
conflictStrategy: options.conflictStrategy,
|
|
132
|
+
dryRun: options.dryRun,
|
|
133
|
+
};
|
|
134
|
+
let dbCreated = 0, dbUpdated = 0, dbSkipped = 0, dbFailed = 0;
|
|
135
|
+
for (const dbId of databaseIdsToExport) {
|
|
136
|
+
try {
|
|
137
|
+
// Use the parent page ID as the target for database creation
|
|
138
|
+
const parentPageId = options.parentPageId;
|
|
139
|
+
const result = await exportDatabase(dbCtx, dbId, parentPageId);
|
|
140
|
+
const icon = { created: '\u2713', updated: '\u21bb', skipped: '-', failed: '\u2717' }[result.status];
|
|
141
|
+
console.log(` ${icon} ${result.status}: ${result.kbDatabaseName} (${result.entriesCreated} entries)`);
|
|
142
|
+
if (result.error)
|
|
143
|
+
console.log(` Error: ${result.error}`);
|
|
144
|
+
if (result.status === 'created')
|
|
145
|
+
dbCreated++;
|
|
146
|
+
else if (result.status === 'updated')
|
|
147
|
+
dbUpdated++;
|
|
148
|
+
else if (result.status === 'skipped')
|
|
149
|
+
dbSkipped++;
|
|
150
|
+
else if (result.status === 'failed')
|
|
151
|
+
dbFailed++;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.log(` \u2717 failed: database ${dbId}`);
|
|
156
|
+
console.log(` Error: ${msg}`);
|
|
157
|
+
dbFailed++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
console.log(`\nDatabases: ${dbCreated} created, ${dbUpdated} updated, ${dbSkipped} skipped, ${dbFailed} failed`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ============================================
|
|
110
164
|
// Pass 2: Resolve cross-references
|
|
111
165
|
// ============================================
|
|
112
166
|
if (!options.dryRun) {
|
package/dist/targets/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
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';
|
package/dist/targets/index.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Database Export
|
|
3
|
+
*
|
|
4
|
+
* Exports KB databases to Notion databases.
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Creating new Notion databases from KB database schemas
|
|
7
|
+
* - Updating existing Notion databases (clear + recreate entries)
|
|
8
|
+
* - Column mapping (select/multi_select backed by tags -> Notion select/multi_select)
|
|
9
|
+
* - Color mapping (hex -> Notion color names)
|
|
10
|
+
* - Entry creation with correct property values
|
|
11
|
+
*/
|
|
12
|
+
import { Client } from '@notionhq/client';
|
|
13
|
+
import type { MoxnClient } from '../client.js';
|
|
14
|
+
export interface DatabaseExportResult {
|
|
15
|
+
kbDatabaseId: string;
|
|
16
|
+
kbDatabaseName: string;
|
|
17
|
+
notionDatabaseId: string;
|
|
18
|
+
status: 'created' | 'updated' | 'skipped' | 'failed';
|
|
19
|
+
entriesCreated: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DatabaseExportContext {
|
|
23
|
+
notionClient: Client;
|
|
24
|
+
moxnClient: MoxnClient;
|
|
25
|
+
conflictStrategy: 'skip' | 'update';
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Export a single KB database to Notion.
|
|
30
|
+
*
|
|
31
|
+
* @param ctx Export context with Notion client, Moxn client, and options
|
|
32
|
+
* @param kbDatabaseId KB database ID to export
|
|
33
|
+
* @param parentNotionPageId Notion page ID to create the database under
|
|
34
|
+
* @returns Export result
|
|
35
|
+
*/
|
|
36
|
+
export declare function exportDatabase(ctx: DatabaseExportContext, kbDatabaseId: string, parentNotionPageId: string): Promise<DatabaseExportResult>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Database Export
|
|
3
|
+
*
|
|
4
|
+
* Exports KB databases to Notion databases.
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Creating new Notion databases from KB database schemas
|
|
7
|
+
* - Updating existing Notion databases (clear + recreate entries)
|
|
8
|
+
* - Column mapping (select/multi_select backed by tags -> Notion select/multi_select)
|
|
9
|
+
* - Color mapping (hex -> Notion color names)
|
|
10
|
+
* - Entry creation with correct property values
|
|
11
|
+
*/
|
|
12
|
+
const RATE_LIMIT_MS = 350;
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
// Reverse mapping: hex color -> Notion color name
|
|
17
|
+
const HEX_TO_NOTION_COLOR = {
|
|
18
|
+
'#6B7280': 'gray',
|
|
19
|
+
'#92400E': 'brown',
|
|
20
|
+
'#F97316': 'orange',
|
|
21
|
+
'#EAB308': 'yellow',
|
|
22
|
+
'#22C55E': 'green',
|
|
23
|
+
'#3B82F6': 'blue',
|
|
24
|
+
'#8B5CF6': 'purple',
|
|
25
|
+
'#EC4899': 'pink',
|
|
26
|
+
'#EF4444': 'red',
|
|
27
|
+
};
|
|
28
|
+
function hexToNotionColor(hex) {
|
|
29
|
+
if (!hex)
|
|
30
|
+
return 'default';
|
|
31
|
+
return HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Export a single KB database to Notion.
|
|
35
|
+
*
|
|
36
|
+
* @param ctx Export context with Notion client, Moxn client, and options
|
|
37
|
+
* @param kbDatabaseId KB database ID to export
|
|
38
|
+
* @param parentNotionPageId Notion page ID to create the database under
|
|
39
|
+
* @returns Export result
|
|
40
|
+
*/
|
|
41
|
+
export async function exportDatabase(ctx, kbDatabaseId, parentNotionPageId) {
|
|
42
|
+
try {
|
|
43
|
+
// 1. Fetch resolved database from KB API
|
|
44
|
+
const resolved = await ctx.moxnClient.resolveDatabase(kbDatabaseId);
|
|
45
|
+
// 2. Check for existing Notion database mapping
|
|
46
|
+
let existingMapping = null;
|
|
47
|
+
try {
|
|
48
|
+
const mappingResult = await ctx.moxnClient.getNotionDatabaseMapping(kbDatabaseId);
|
|
49
|
+
existingMapping = mappingResult.mapping;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// No mapping found -- that's fine
|
|
53
|
+
}
|
|
54
|
+
// 3. Handle conflict strategy
|
|
55
|
+
if (existingMapping) {
|
|
56
|
+
if (ctx.conflictStrategy === 'skip') {
|
|
57
|
+
return {
|
|
58
|
+
kbDatabaseId,
|
|
59
|
+
kbDatabaseName: resolved.databaseName,
|
|
60
|
+
notionDatabaseId: existingMapping.notionDatabaseId,
|
|
61
|
+
status: 'skipped',
|
|
62
|
+
entriesCreated: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (ctx.dryRun) {
|
|
66
|
+
return {
|
|
67
|
+
kbDatabaseId,
|
|
68
|
+
kbDatabaseName: resolved.databaseName,
|
|
69
|
+
notionDatabaseId: existingMapping.notionDatabaseId,
|
|
70
|
+
status: 'updated',
|
|
71
|
+
entriesCreated: resolved.documents.length,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Update existing: archive all pages, then recreate entries
|
|
75
|
+
const entriesCreated = await updateExistingNotionDatabase(ctx, existingMapping.notionDatabaseId, resolved);
|
|
76
|
+
return {
|
|
77
|
+
kbDatabaseId,
|
|
78
|
+
kbDatabaseName: resolved.databaseName,
|
|
79
|
+
notionDatabaseId: existingMapping.notionDatabaseId,
|
|
80
|
+
status: 'updated',
|
|
81
|
+
entriesCreated,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// 4. No existing mapping -- create new Notion database
|
|
85
|
+
if (ctx.dryRun) {
|
|
86
|
+
return {
|
|
87
|
+
kbDatabaseId,
|
|
88
|
+
kbDatabaseName: resolved.databaseName,
|
|
89
|
+
notionDatabaseId: 'dry-run',
|
|
90
|
+
status: 'created',
|
|
91
|
+
entriesCreated: resolved.documents.length,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const { notionDatabaseId, dataSourceId } = await createNotionDatabase(ctx, parentNotionPageId, resolved);
|
|
95
|
+
// 5. Create entries
|
|
96
|
+
const entriesCreated = await createDatabaseEntries(ctx, dataSourceId, resolved);
|
|
97
|
+
// 6. Upsert mapping
|
|
98
|
+
await ctx.moxnClient.createNotionDatabaseMapping({
|
|
99
|
+
kbDatabaseId,
|
|
100
|
+
notionDatabaseId: notionDatabaseId.replace(/-/g, ''),
|
|
101
|
+
notionDatabaseTitle: resolved.databaseName,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
kbDatabaseId,
|
|
105
|
+
kbDatabaseName: resolved.databaseName,
|
|
106
|
+
notionDatabaseId: notionDatabaseId.replace(/-/g, ''),
|
|
107
|
+
status: 'created',
|
|
108
|
+
entriesCreated,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
kbDatabaseId,
|
|
114
|
+
kbDatabaseName: 'Unknown',
|
|
115
|
+
notionDatabaseId: '',
|
|
116
|
+
status: 'failed',
|
|
117
|
+
entriesCreated: 0,
|
|
118
|
+
error: error instanceof Error ? error.message : String(error),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create a new Notion database with the correct schema.
|
|
124
|
+
*/
|
|
125
|
+
async function createNotionDatabase(ctx, parentPageId, resolved) {
|
|
126
|
+
// Build properties from KB columns
|
|
127
|
+
const properties = {};
|
|
128
|
+
for (const col of resolved.columns) {
|
|
129
|
+
const options = col.options.map((opt) => ({
|
|
130
|
+
name: opt.tagName,
|
|
131
|
+
color: hexToNotionColor(opt.tagColor),
|
|
132
|
+
}));
|
|
133
|
+
if (col.type === 'select') {
|
|
134
|
+
properties[col.name] = {
|
|
135
|
+
type: 'select',
|
|
136
|
+
select: { options },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (col.type === 'multi_select') {
|
|
140
|
+
properties[col.name] = {
|
|
141
|
+
type: 'multi_select',
|
|
142
|
+
multi_select: { options },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await sleep(RATE_LIMIT_MS);
|
|
147
|
+
// Use client.request() for v5 API compatibility
|
|
148
|
+
// Properties go under initial_data_source.properties (not top-level)
|
|
149
|
+
const response = (await ctx.notionClient.request({
|
|
150
|
+
method: 'post',
|
|
151
|
+
path: 'databases',
|
|
152
|
+
body: {
|
|
153
|
+
parent: { type: 'page_id', page_id: parentPageId },
|
|
154
|
+
title: [{ type: 'text', text: { content: resolved.databaseName } }],
|
|
155
|
+
initial_data_source: { properties },
|
|
156
|
+
},
|
|
157
|
+
}));
|
|
158
|
+
const notionDatabaseId = response.id;
|
|
159
|
+
const dataSourceId = response.data_sources?.[0]?.id ?? notionDatabaseId;
|
|
160
|
+
return { notionDatabaseId, dataSourceId };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Update an existing Notion database by archiving all pages and recreating entries.
|
|
164
|
+
*/
|
|
165
|
+
async function updateExistingNotionDatabase(ctx, notionDatabaseId, resolved) {
|
|
166
|
+
// Archive existing pages (Notion v5: query database, archive each page)
|
|
167
|
+
await sleep(RATE_LIMIT_MS);
|
|
168
|
+
let hasMore = true;
|
|
169
|
+
let startCursor;
|
|
170
|
+
while (hasMore) {
|
|
171
|
+
const queryResult = await ctx.notionClient.databases.query({
|
|
172
|
+
database_id: notionDatabaseId,
|
|
173
|
+
start_cursor: startCursor,
|
|
174
|
+
page_size: 100,
|
|
175
|
+
});
|
|
176
|
+
for (const page of queryResult.results) {
|
|
177
|
+
await sleep(RATE_LIMIT_MS);
|
|
178
|
+
try {
|
|
179
|
+
await ctx.notionClient.pages.update({
|
|
180
|
+
page_id: page.id,
|
|
181
|
+
archived: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Continue if individual archive fails
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
hasMore = queryResult.has_more;
|
|
189
|
+
startCursor = queryResult.next_cursor ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
// Now get the data_source_id for entry creation
|
|
192
|
+
// Query the database to get its data_sources
|
|
193
|
+
await sleep(RATE_LIMIT_MS);
|
|
194
|
+
const dbInfo = (await ctx.notionClient.request({
|
|
195
|
+
method: 'get',
|
|
196
|
+
path: `databases/${notionDatabaseId}`,
|
|
197
|
+
}));
|
|
198
|
+
const dataSourceId = dbInfo.data_sources?.[0]?.id ?? notionDatabaseId;
|
|
199
|
+
// Create new entries
|
|
200
|
+
return createDatabaseEntries(ctx, dataSourceId, resolved);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create database entries as Notion pages.
|
|
204
|
+
*/
|
|
205
|
+
async function createDatabaseEntries(ctx, dataSourceId, resolved) {
|
|
206
|
+
let created = 0;
|
|
207
|
+
for (const doc of resolved.documents) {
|
|
208
|
+
try {
|
|
209
|
+
// Build property values for this entry
|
|
210
|
+
const properties = {
|
|
211
|
+
// Title property (always "Name" for databases)
|
|
212
|
+
Name: {
|
|
213
|
+
type: 'title',
|
|
214
|
+
title: [{ type: 'text', text: { content: doc.documentName ?? 'Untitled' } }],
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
// Add column values
|
|
218
|
+
for (const [colName, propValue] of Object.entries(doc.properties)) {
|
|
219
|
+
if (propValue.values.length === 0)
|
|
220
|
+
continue;
|
|
221
|
+
if (propValue.columnType === 'select') {
|
|
222
|
+
properties[colName] = {
|
|
223
|
+
type: 'select',
|
|
224
|
+
select: { name: propValue.values[0].tagName },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else if (propValue.columnType === 'multi_select') {
|
|
228
|
+
properties[colName] = {
|
|
229
|
+
type: 'multi_select',
|
|
230
|
+
multi_select: propValue.values.map((v) => ({ name: v.tagName })),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
await sleep(RATE_LIMIT_MS);
|
|
235
|
+
// Create entry as page with data_source_id parent
|
|
236
|
+
await ctx.notionClient.request({
|
|
237
|
+
method: 'post',
|
|
238
|
+
path: 'pages',
|
|
239
|
+
body: {
|
|
240
|
+
parent: { type: 'data_source_id', data_source_id: dataSourceId },
|
|
241
|
+
properties,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
created++;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error(` Warning: Failed to create entry "${doc.documentName}": ${error instanceof Error ? error.message : error}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return created;
|
|
251
|
+
}
|
package/dist/targets/notion.js
CHANGED
|
@@ -46,6 +46,22 @@ function extractKBPathReferences(text) {
|
|
|
46
46
|
});
|
|
47
47
|
return { cleanedText, references };
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Strip invalid internal links from markdown text.
|
|
51
|
+
* Notion rejects links that aren't valid URLs (e.g., relative KB paths
|
|
52
|
+
* like "some/relative/path"). Convert them to plain text.
|
|
53
|
+
*/
|
|
54
|
+
function stripInvalidLinks(text) {
|
|
55
|
+
// Match markdown links [text](url) where url is NOT a valid URL
|
|
56
|
+
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, displayText, url) => {
|
|
57
|
+
// Keep links that look like valid URLs (http/https/mailto/tel)
|
|
58
|
+
if (/^(https?:\/\/|mailto:|tel:|#)/.test(url)) {
|
|
59
|
+
return match;
|
|
60
|
+
}
|
|
61
|
+
// Strip relative/internal paths — just keep the display text
|
|
62
|
+
return displayText;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
49
65
|
/**
|
|
50
66
|
* Convert a KB document's sections into a single markdown string.
|
|
51
67
|
* Section names become H2 headings (mirrors the import convention).
|
|
@@ -56,6 +72,7 @@ function extractKBPathReferences(text) {
|
|
|
56
72
|
function sectionsToMarkdown(sections, options) {
|
|
57
73
|
const parts = [];
|
|
58
74
|
const allReferences = [];
|
|
75
|
+
const databaseIds = [];
|
|
59
76
|
const extractRefs = options?.extractReferences ?? false;
|
|
60
77
|
for (const section of sections) {
|
|
61
78
|
parts.push(`## ${section.name}\n`);
|
|
@@ -67,6 +84,9 @@ function sectionsToMarkdown(sections, options) {
|
|
|
67
84
|
text = cleanedText;
|
|
68
85
|
allReferences.push(...references);
|
|
69
86
|
}
|
|
87
|
+
// Strip relative/internal links that aren't valid URLs
|
|
88
|
+
// (Notion rejects links without a protocol)
|
|
89
|
+
text = stripInvalidLinks(text);
|
|
70
90
|
parts.push(text);
|
|
71
91
|
parts.push('');
|
|
72
92
|
}
|
|
@@ -82,9 +102,16 @@ function sectionsToMarkdown(sections, options) {
|
|
|
82
102
|
parts.push(`[${block.filename || 'data.csv'}](${block.url})`);
|
|
83
103
|
parts.push('');
|
|
84
104
|
}
|
|
105
|
+
else if (block.blockType === 'database_embed' && block.databaseId) {
|
|
106
|
+
// Collect database ID for Pass 1.5 export
|
|
107
|
+
databaseIds.push(block.databaseId);
|
|
108
|
+
// Add a placeholder in the markdown
|
|
109
|
+
parts.push(`> **[Database]** *(exported as inline database)*`);
|
|
110
|
+
parts.push('');
|
|
111
|
+
}
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
|
-
return { markdown: parts.join('\n').trim(), references: allReferences };
|
|
114
|
+
return { markdown: parts.join('\n').trim(), references: allReferences, databaseIds };
|
|
88
115
|
}
|
|
89
116
|
// Max 100 blocks per API call
|
|
90
117
|
const MAX_BLOCKS_PER_APPEND = 100;
|
package/package.json
CHANGED