@moxn/kb-migrate 0.2.2 → 0.4.0
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 +49 -0
- package/dist/client.js +167 -3
- package/dist/index.js +209 -6
- package/dist/sources/index.d.ts +1 -0
- package/dist/sources/index.js +1 -3
- package/dist/sources/local.d.ts +6 -2
- package/dist/sources/local.js +48 -3
- package/dist/sources/notion-api.d.ts +240 -0
- package/dist/sources/notion-api.js +196 -0
- package/dist/sources/notion-blocks.d.ts +30 -0
- package/dist/sources/notion-blocks.js +505 -0
- package/dist/sources/notion-databases.d.ts +59 -0
- package/dist/sources/notion-databases.js +266 -0
- package/dist/sources/notion-media.d.ts +30 -0
- package/dist/sources/notion-media.js +133 -0
- package/dist/sources/notion.d.ts +66 -0
- package/dist/sources/notion.js +390 -0
- package/dist/types.d.ts +8 -2
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -28,7 +28,56 @@ export declare class MoxnClient {
|
|
|
28
28
|
private buildPath;
|
|
29
29
|
private processSections;
|
|
30
30
|
private processContentBlocks;
|
|
31
|
+
/**
|
|
32
|
+
* Upload a file to Moxn storage and return the storage key.
|
|
33
|
+
*/
|
|
34
|
+
uploadFile(data: Buffer, mimeType: string, filename?: string): Promise<{
|
|
35
|
+
key: string;
|
|
36
|
+
}>;
|
|
37
|
+
private getUploadUrl;
|
|
31
38
|
private createDocument;
|
|
32
39
|
private updateDocument;
|
|
40
|
+
/**
|
|
41
|
+
* Create a KB database.
|
|
42
|
+
*/
|
|
43
|
+
createDatabase(input: {
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Add a column to a KB database.
|
|
52
|
+
*/
|
|
53
|
+
addDatabaseColumn(databaseId: string, input: {
|
|
54
|
+
name: string;
|
|
55
|
+
type: 'select' | 'multi_select';
|
|
56
|
+
optionTagIds?: string[];
|
|
57
|
+
newOptionParentPath?: string;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Add a document to a KB database.
|
|
64
|
+
*/
|
|
65
|
+
addDocumentToDatabase(databaseId: string, documentId: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Create a tag (with automatic ancestor creation).
|
|
68
|
+
*/
|
|
69
|
+
createTag(input: {
|
|
70
|
+
path: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
color?: string;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
id: string;
|
|
75
|
+
path: string;
|
|
76
|
+
name: string;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Assign a tag to a document.
|
|
80
|
+
*/
|
|
81
|
+
assignTag(documentId: string, tagId: string, branchId: string): Promise<void>;
|
|
33
82
|
private isConflictError;
|
|
34
83
|
}
|
package/dist/client.js
CHANGED
|
@@ -179,20 +179,82 @@ export class MoxnClient {
|
|
|
179
179
|
}
|
|
180
180
|
async processContentBlocks(blocks) {
|
|
181
181
|
return Promise.all(blocks.map(async (block) => {
|
|
182
|
+
// Handle image files - upload to storage
|
|
182
183
|
if (block.blockType === 'image' && block.type === 'file' && block.path) {
|
|
183
|
-
// Convert local file to base64
|
|
184
184
|
const data = await fs.readFile(block.path);
|
|
185
|
+
const filename = block.path.split('/').pop();
|
|
186
|
+
const { key } = await this.uploadFile(data, block.mediaType, filename);
|
|
185
187
|
return {
|
|
186
188
|
blockType: block.blockType,
|
|
187
|
-
type: '
|
|
188
|
-
|
|
189
|
+
type: 'storage',
|
|
190
|
+
key,
|
|
189
191
|
mediaType: block.mediaType,
|
|
190
192
|
alt: block.alt,
|
|
191
193
|
};
|
|
192
194
|
}
|
|
195
|
+
// Handle PDF/document files - upload to storage
|
|
196
|
+
if (block.blockType === 'document' && block.type === 'file' && block.path) {
|
|
197
|
+
const data = await fs.readFile(block.path);
|
|
198
|
+
const filename = block.filename || block.path.split('/').pop();
|
|
199
|
+
const { key } = await this.uploadFile(data, block.mediaType || 'application/pdf', filename);
|
|
200
|
+
return {
|
|
201
|
+
blockType: block.blockType,
|
|
202
|
+
type: 'storage',
|
|
203
|
+
key,
|
|
204
|
+
mediaType: block.mediaType || 'application/pdf',
|
|
205
|
+
filename: block.filename,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Handle CSV files - upload to storage
|
|
209
|
+
if (block.blockType === 'csv' && block.type === 'file' && block.path) {
|
|
210
|
+
const data = await fs.readFile(block.path);
|
|
211
|
+
const { key } = await this.uploadFile(data, 'text/csv', block.filename);
|
|
212
|
+
return {
|
|
213
|
+
blockType: block.blockType,
|
|
214
|
+
type: 'storage',
|
|
215
|
+
key,
|
|
216
|
+
mediaType: 'text/csv',
|
|
217
|
+
filename: block.filename,
|
|
218
|
+
headers: block.headers,
|
|
219
|
+
rowCount: block.rowCount,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
193
222
|
return block;
|
|
194
223
|
}));
|
|
195
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Upload a file to Moxn storage and return the storage key.
|
|
227
|
+
*/
|
|
228
|
+
async uploadFile(data, mimeType, filename) {
|
|
229
|
+
// 1. Get presigned upload URL
|
|
230
|
+
const { key, uploadUrl } = await this.getUploadUrl(mimeType, filename);
|
|
231
|
+
// 2. PUT file to presigned URL
|
|
232
|
+
const response = await fetch(uploadUrl, {
|
|
233
|
+
method: 'PUT',
|
|
234
|
+
headers: { 'Content-Type': mimeType },
|
|
235
|
+
body: new Uint8Array(data),
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
throw new Error(`File upload failed: ${response.status}`);
|
|
239
|
+
}
|
|
240
|
+
return { key };
|
|
241
|
+
}
|
|
242
|
+
async getUploadUrl(type, filename) {
|
|
243
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
'x-api-key': this.apiKey,
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({ type, filename }),
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
const body = await response.text();
|
|
253
|
+
throw new Error(`Upload URL request failed: ${response.status} ${body}`);
|
|
254
|
+
}
|
|
255
|
+
const data = await response.json();
|
|
256
|
+
return { key: data.key, uploadUrl: data.uploadUrl };
|
|
257
|
+
}
|
|
196
258
|
async createDocument(request) {
|
|
197
259
|
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents`, {
|
|
198
260
|
method: 'POST',
|
|
@@ -229,6 +291,108 @@ export class MoxnClient {
|
|
|
229
291
|
}
|
|
230
292
|
return response.json();
|
|
231
293
|
}
|
|
294
|
+
// ──────────────────────────────────────────────
|
|
295
|
+
// Database & tag methods (for Notion import)
|
|
296
|
+
// ──────────────────────────────────────────────
|
|
297
|
+
/**
|
|
298
|
+
* Create a KB database.
|
|
299
|
+
*/
|
|
300
|
+
async createDatabase(input) {
|
|
301
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'Content-Type': 'application/json',
|
|
305
|
+
'x-api-key': this.apiKey,
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify(input),
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
const body = await response.json().catch(() => ({}));
|
|
311
|
+
throw new Error(body.error || `Failed to create database: ${response.status}`);
|
|
312
|
+
}
|
|
313
|
+
return response.json();
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Add a column to a KB database.
|
|
317
|
+
*/
|
|
318
|
+
async addDatabaseColumn(databaseId, input) {
|
|
319
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/columns`, {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: {
|
|
322
|
+
'Content-Type': 'application/json',
|
|
323
|
+
'x-api-key': this.apiKey,
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify(input),
|
|
326
|
+
});
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
const body = await response.json().catch(() => ({}));
|
|
329
|
+
throw new Error(body.error || `Failed to add column: ${response.status}`);
|
|
330
|
+
}
|
|
331
|
+
return response.json();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Add a document to a KB database.
|
|
335
|
+
*/
|
|
336
|
+
async addDocumentToDatabase(databaseId, documentId) {
|
|
337
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/documents`, {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
headers: {
|
|
340
|
+
'Content-Type': 'application/json',
|
|
341
|
+
'x-api-key': this.apiKey,
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify({ documentId }),
|
|
344
|
+
});
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
const body = await response.json().catch(() => ({}));
|
|
347
|
+
// Ignore 409 — document already in database
|
|
348
|
+
if (response.status === 409)
|
|
349
|
+
return;
|
|
350
|
+
throw new Error(body.error || `Failed to add document to database: ${response.status}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Create a tag (with automatic ancestor creation).
|
|
355
|
+
*/
|
|
356
|
+
async createTag(input) {
|
|
357
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/tags`, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'application/json',
|
|
361
|
+
'x-api-key': this.apiKey,
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify(input),
|
|
364
|
+
});
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
const body = await response.json().catch(() => ({}));
|
|
367
|
+
// If tag already exists (409), try to find it
|
|
368
|
+
if (response.status === 409 && body.tagId) {
|
|
369
|
+
return {
|
|
370
|
+
id: body.tagId,
|
|
371
|
+
path: input.path,
|
|
372
|
+
name: input.path.split('/').pop() || '',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
throw new Error(body.error || `Failed to create tag: ${response.status}`);
|
|
376
|
+
}
|
|
377
|
+
return response.json();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Assign a tag to a document.
|
|
381
|
+
*/
|
|
382
|
+
async assignTag(documentId, tagId, branchId) {
|
|
383
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/tags`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: {
|
|
386
|
+
'Content-Type': 'application/json',
|
|
387
|
+
'x-api-key': this.apiKey,
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify({ tagId, branchId }),
|
|
390
|
+
});
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
const body = await response.json().catch(() => ({}));
|
|
393
|
+
throw new Error(body.error || `Failed to assign tag: ${response.status}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
232
396
|
isConflictError(error) {
|
|
233
397
|
return (error instanceof Error &&
|
|
234
398
|
'documentId' in error &&
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Command } from 'commander';
|
|
16
16
|
import { LocalSource } from './sources/local.js';
|
|
17
|
+
import { NotionSource } from './sources/notion.js';
|
|
18
|
+
import { notionColorToHex } from './sources/notion-api.js';
|
|
17
19
|
import { MoxnClient } from './client.js';
|
|
18
20
|
import { runExport } from './export.js';
|
|
19
21
|
const DEFAULT_API_URL = 'https://moxn.dev';
|
|
@@ -216,10 +218,211 @@ program
|
|
|
216
218
|
process.exit(1);
|
|
217
219
|
}
|
|
218
220
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
program
|
|
222
|
+
.command('notion')
|
|
223
|
+
.description('Migrate documents from a Notion workspace')
|
|
224
|
+
.option('--token <token>', 'Notion integration token (or set NOTION_TOKEN env var)')
|
|
225
|
+
.option('--api-key <key>', 'Moxn API key (or set MOXN_API_KEY env var)')
|
|
226
|
+
.option('--api-url <url>', 'Moxn API base URL', DEFAULT_API_URL)
|
|
227
|
+
.option('--base-path <path>', 'Base path for imported documents', '/imported-from-notion')
|
|
228
|
+
.option('--root-page-id <id>', 'Import subtree starting from this Notion page ID')
|
|
229
|
+
.option('--max-depth <n>', 'Maximum nesting depth')
|
|
230
|
+
.option('--on-conflict <action>', 'Action on conflict: skip or update', 'skip')
|
|
231
|
+
.option('--default-permission <perm>', 'Default permission: edit, read, or none')
|
|
232
|
+
.option('--ai-access <perm>', 'AI access permission: edit, read, or none')
|
|
233
|
+
.option('--visibility <vis>', 'Convenience flag: team (read) or private (none)')
|
|
234
|
+
.option('--dry-run', 'Preview without making changes', false)
|
|
235
|
+
.option('--json', 'Output results as JSON', false)
|
|
236
|
+
.action(async (opts) => {
|
|
237
|
+
const token = opts.token || process.env.NOTION_TOKEN;
|
|
238
|
+
if (!token) {
|
|
239
|
+
console.error('Error: Notion token required. Use --token or set NOTION_TOKEN env var.');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const apiKey = opts.apiKey || process.env.MOXN_API_KEY;
|
|
243
|
+
if (!apiKey) {
|
|
244
|
+
console.error('Error: Moxn API key required. Use --api-key or set MOXN_API_KEY env var.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
const onConflict = opts.onConflict;
|
|
248
|
+
if (!['skip', 'update'].includes(onConflict)) {
|
|
249
|
+
console.error('Error: --on-conflict must be "skip" or "update"');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
// Resolve visibility → defaultPermission (visibility is syntactic sugar)
|
|
253
|
+
let defaultPermission = opts.defaultPermission;
|
|
254
|
+
if (!defaultPermission && opts.visibility) {
|
|
255
|
+
defaultPermission = opts.visibility === 'private' ? 'none' : 'read';
|
|
256
|
+
}
|
|
257
|
+
const source = new NotionSource({
|
|
258
|
+
token,
|
|
259
|
+
rootPageId: opts.rootPageId,
|
|
260
|
+
maxDepth: opts.maxDepth ? parseInt(opts.maxDepth, 10) : undefined,
|
|
261
|
+
});
|
|
262
|
+
const migrationOptions = {
|
|
263
|
+
apiUrl: opts.apiUrl,
|
|
264
|
+
apiKey,
|
|
265
|
+
basePath: opts.basePath,
|
|
266
|
+
onConflict,
|
|
267
|
+
dryRun: opts.dryRun,
|
|
268
|
+
defaultPermission,
|
|
269
|
+
aiAccess: opts.aiAccess,
|
|
270
|
+
visibility: opts.visibility,
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
// Run page migration
|
|
274
|
+
const log = await runMigration(source, migrationOptions);
|
|
275
|
+
// After page migration, import databases
|
|
276
|
+
if (!opts.dryRun) {
|
|
277
|
+
const dbImports = source.getDatabaseImports();
|
|
278
|
+
if (dbImports.length > 0) {
|
|
279
|
+
console.log(`\nImporting ${dbImports.length} database(s)...`);
|
|
280
|
+
const client = new MoxnClient(migrationOptions);
|
|
281
|
+
for (const dbImport of dbImports) {
|
|
282
|
+
try {
|
|
283
|
+
await importNotionDatabase(client, dbImport, log, migrationOptions);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error(` Error importing database "${dbImport.schema.name}": ${error instanceof Error ? error.message : error}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Cleanup temp files
|
|
292
|
+
await source.cleanup();
|
|
293
|
+
if (opts.json) {
|
|
294
|
+
console.log(JSON.stringify(log, null, 2));
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
printSummary(log);
|
|
298
|
+
}
|
|
299
|
+
if (log.summary.failed > 0) {
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
await source.cleanup().catch(() => { });
|
|
305
|
+
console.error('Migration failed:', error instanceof Error ? error.message : error);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
/**
|
|
310
|
+
* Import a Notion database into Moxn KB.
|
|
311
|
+
*
|
|
312
|
+
* Creates the database, columns with tags, links entries, and assigns tag values.
|
|
313
|
+
*/
|
|
314
|
+
async function importNotionDatabase(client, dbImport, log, options) {
|
|
315
|
+
const { schema, entries } = dbImport;
|
|
316
|
+
console.log(` Creating database: ${schema.name}`);
|
|
317
|
+
// 1. Create the database
|
|
318
|
+
const db = await client.createDatabase({
|
|
319
|
+
name: schema.name,
|
|
320
|
+
description: schema.description || undefined,
|
|
321
|
+
});
|
|
322
|
+
console.log(` Database created: ${db.id}`);
|
|
323
|
+
// 2. Create columns with tags
|
|
324
|
+
// Map: column name → { columnId, optionTagMap: option name → tagId }
|
|
325
|
+
const columnMap = new Map();
|
|
326
|
+
for (const col of schema.mappedColumns) {
|
|
327
|
+
const tagBasePath = `/imported/${slugifyTagPath(schema.name)}/${slugifyTagPath(col.notionPropertyName)}`;
|
|
328
|
+
// Create tags for each option
|
|
329
|
+
const optionTagMap = new Map();
|
|
330
|
+
const tagIds = [];
|
|
331
|
+
for (const option of col.options) {
|
|
332
|
+
const tagPath = `${tagBasePath}/${slugifyTagPath(option.name)}`;
|
|
333
|
+
try {
|
|
334
|
+
const tag = await client.createTag({
|
|
335
|
+
path: tagPath,
|
|
336
|
+
color: notionColorToHex(option.color),
|
|
337
|
+
});
|
|
338
|
+
optionTagMap.set(option.name, tag.id);
|
|
339
|
+
tagIds.push(tag.id);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.warn(` Warning: Failed to create tag "${tagPath}": ${error instanceof Error ? error.message : error}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Create the column
|
|
346
|
+
try {
|
|
347
|
+
const column = await client.addDatabaseColumn(db.id, {
|
|
348
|
+
name: col.notionPropertyName,
|
|
349
|
+
type: col.moxnType,
|
|
350
|
+
optionTagIds: tagIds,
|
|
351
|
+
newOptionParentPath: tagBasePath,
|
|
352
|
+
});
|
|
353
|
+
columnMap.set(col.notionPropertyName, {
|
|
354
|
+
columnId: column.id,
|
|
355
|
+
optionTagMap,
|
|
356
|
+
});
|
|
357
|
+
console.log(` Column "${col.notionPropertyName}" (${col.moxnType}) with ${tagIds.length} options`);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
console.warn(` Warning: Failed to create column "${col.notionPropertyName}": ${error instanceof Error ? error.message : error}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 3. Link entries and assign tags
|
|
364
|
+
// Build a map of KB path → { documentId, branchId } from migration results
|
|
365
|
+
const docByPath = new Map();
|
|
366
|
+
for (const result of log.results) {
|
|
367
|
+
if (result.documentId && result.branchId) {
|
|
368
|
+
docByPath.set(result.documentPath, {
|
|
369
|
+
documentId: result.documentId,
|
|
370
|
+
branchId: result.branchId,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
let linkedCount = 0;
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
const kbPath = '/' +
|
|
377
|
+
options.basePath.replace(/^\/+|\/+$/g, '') +
|
|
378
|
+
'/' +
|
|
379
|
+
entry.kbPath.replace(/^\/+/, '');
|
|
380
|
+
const docInfo = docByPath.get(kbPath);
|
|
381
|
+
if (!docInfo) {
|
|
382
|
+
// Document wasn't created (maybe skipped or failed)
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
// Add document to database
|
|
386
|
+
try {
|
|
387
|
+
await client.addDocumentToDatabase(db.id, docInfo.documentId);
|
|
388
|
+
linkedCount++;
|
|
389
|
+
// Parse and assign tag values
|
|
390
|
+
const { parseEntryValues } = await import('./sources/notion-databases.js');
|
|
391
|
+
const values = parseEntryValues(entry.page, schema);
|
|
392
|
+
for (const [colName, selectedOptions] of values.tagValues) {
|
|
393
|
+
const colInfo = columnMap.get(colName);
|
|
394
|
+
if (!colInfo)
|
|
395
|
+
continue;
|
|
396
|
+
for (const optionName of selectedOptions) {
|
|
397
|
+
const tagId = colInfo.optionTagMap.get(optionName);
|
|
398
|
+
if (tagId) {
|
|
399
|
+
try {
|
|
400
|
+
await client.assignTag(docInfo.documentId, tagId, docInfo.branchId);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
console.warn(` Warning: Failed to assign tag "${optionName}" to ${docInfo.documentId}: ${error instanceof Error ? error.message : error}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
console.warn(` Warning: Failed to link entry to database: ${error instanceof Error ? error.message : error}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
console.log(` Linked ${linkedCount}/${entries.length} entries`);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Slugify a string for use as a tag path segment.
|
|
417
|
+
* Similar to page slug but for the tag hierarchy.
|
|
418
|
+
*/
|
|
419
|
+
function slugifyTagPath(s) {
|
|
420
|
+
return (s
|
|
421
|
+
.toLowerCase()
|
|
422
|
+
.trim()
|
|
423
|
+
.replace(/[^\w\s-]/g, '')
|
|
424
|
+
.replace(/[\s_]+/g, '-')
|
|
425
|
+
.replace(/-+/g, '-')
|
|
426
|
+
.replace(/^-|-$/g, '') || 'untitled');
|
|
427
|
+
}
|
|
225
428
|
program.parse();
|
package/dist/sources/index.d.ts
CHANGED
package/dist/sources/index.js
CHANGED
|
@@ -3,6 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { MigrationSource } from './base.js';
|
|
5
5
|
export { LocalSource } from './local.js';
|
|
6
|
-
|
|
7
|
-
// export { NotionSource, type NotionSourceConfig } from './notion.js';
|
|
8
|
-
// export { GoogleDocsSource, type GoogleDocsSourceConfig } from './google-docs.js';
|
|
6
|
+
export { NotionSource } from './notion.js';
|
package/dist/sources/local.d.ts
CHANGED
|
@@ -27,10 +27,14 @@ export declare class LocalSource extends MigrationSource<LocalSourceConfig> {
|
|
|
27
27
|
private parseMarkdownSections;
|
|
28
28
|
private nodesToContentBlocks;
|
|
29
29
|
/**
|
|
30
|
-
* Extract image nodes from paragraph children, returning
|
|
31
|
-
* and whether non-
|
|
30
|
+
* Extract image nodes and CSV links from paragraph children, returning media blocks
|
|
31
|
+
* and whether non-media text content exists.
|
|
32
32
|
*/
|
|
33
33
|
private extractImagesFromParagraph;
|
|
34
|
+
/**
|
|
35
|
+
* Convert a markdown link to a CSV block if it points to a local CSV file.
|
|
36
|
+
*/
|
|
37
|
+
private linkToCSVBlock;
|
|
34
38
|
private imageToBlock;
|
|
35
39
|
private guessImageType;
|
|
36
40
|
private extensionToMediaType;
|
package/dist/sources/local.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Extracts documents from local markdown and text files.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
|
+
import * as fsSync from 'fs';
|
|
7
8
|
import * as path from 'path';
|
|
8
9
|
import { glob } from 'glob';
|
|
9
10
|
import { unified } from 'unified';
|
|
@@ -85,7 +86,6 @@ export class LocalSource extends MigrationSource {
|
|
|
85
86
|
}
|
|
86
87
|
async extractDocument(relativePath) {
|
|
87
88
|
const fullPath = path.join(this.config.directory, relativePath);
|
|
88
|
-
const content = await fs.readFile(fullPath, 'utf-8');
|
|
89
89
|
// Parse relative path to create document path
|
|
90
90
|
const parsed = path.parse(relativePath);
|
|
91
91
|
const dirParts = parsed.dir ? parsed.dir.split(path.sep) : [];
|
|
@@ -100,6 +100,7 @@ export class LocalSource extends MigrationSource {
|
|
|
100
100
|
.split(/[-_]/)
|
|
101
101
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
102
102
|
.join(' ');
|
|
103
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
103
104
|
// Parse into sections
|
|
104
105
|
const sections = this.parseMarkdownSections(content, path.dirname(fullPath));
|
|
105
106
|
if (sections.length === 0) {
|
|
@@ -218,8 +219,8 @@ export class LocalSource extends MigrationSource {
|
|
|
218
219
|
return blocks;
|
|
219
220
|
}
|
|
220
221
|
/**
|
|
221
|
-
* Extract image nodes from paragraph children, returning
|
|
222
|
-
* and whether non-
|
|
222
|
+
* Extract image nodes and CSV links from paragraph children, returning media blocks
|
|
223
|
+
* and whether non-media text content exists.
|
|
223
224
|
*/
|
|
224
225
|
extractImagesFromParagraph(children, baseDir) {
|
|
225
226
|
const images = [];
|
|
@@ -231,6 +232,20 @@ export class LocalSource extends MigrationSource {
|
|
|
231
232
|
images.push(imageBlock);
|
|
232
233
|
}
|
|
233
234
|
}
|
|
235
|
+
else if (child.type === 'link') {
|
|
236
|
+
// Check if this is a link to a CSV file
|
|
237
|
+
const csvBlock = this.linkToCSVBlock(child, baseDir);
|
|
238
|
+
if (csvBlock) {
|
|
239
|
+
images.push(csvBlock);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Regular link, treat as text
|
|
243
|
+
const text = this.nodeToMarkdown(child).trim();
|
|
244
|
+
if (text) {
|
|
245
|
+
hasText = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
234
249
|
else {
|
|
235
250
|
// Check if the child has any meaningful text
|
|
236
251
|
const text = this.nodeToMarkdown(child).trim();
|
|
@@ -241,6 +256,36 @@ export class LocalSource extends MigrationSource {
|
|
|
241
256
|
}
|
|
242
257
|
return { images, hasText };
|
|
243
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Convert a markdown link to a CSV block if it points to a local CSV file.
|
|
261
|
+
*/
|
|
262
|
+
linkToCSVBlock(node, baseDir) {
|
|
263
|
+
const href = node.url;
|
|
264
|
+
// Only handle local CSV files
|
|
265
|
+
if (href.startsWith('http://') || href.startsWith('https://')) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (!href.toLowerCase().endsWith('.csv')) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const csvPath = path.isAbsolute(href) ? href : path.join(baseDir, href);
|
|
272
|
+
if (!fsSync.existsSync(csvPath)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
// Read CSV for metadata
|
|
276
|
+
const content = fsSync.readFileSync(csvPath, 'utf-8');
|
|
277
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
278
|
+
const headers = lines[0]?.split(',').map((h) => h.trim()) || [];
|
|
279
|
+
return {
|
|
280
|
+
blockType: 'csv',
|
|
281
|
+
type: 'file',
|
|
282
|
+
path: csvPath,
|
|
283
|
+
mediaType: 'text/csv',
|
|
284
|
+
filename: path.basename(csvPath),
|
|
285
|
+
headers,
|
|
286
|
+
rowCount: Math.max(0, lines.length - 1),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
244
289
|
imageToBlock(node, baseDir) {
|
|
245
290
|
const url = node.url;
|
|
246
291
|
// Handle external URLs
|