@moxn/kb-migrate 0.2.1 → 0.3.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 +7 -0
- package/dist/client.js +52 -3
- package/dist/export.js +2 -8
- package/dist/sources/local.d.ts +6 -2
- package/dist/sources/local.js +48 -3
- package/dist/types.d.ts +6 -2
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ 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;
|
|
33
40
|
private isConflictError;
|
package/dist/client.js
CHANGED
|
@@ -179,20 +179,69 @@ 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 CSV files - upload to storage
|
|
196
|
+
if (block.blockType === 'csv' && block.type === 'file' && block.path) {
|
|
197
|
+
const data = await fs.readFile(block.path);
|
|
198
|
+
const { key } = await this.uploadFile(data, 'text/csv', block.filename);
|
|
199
|
+
return {
|
|
200
|
+
blockType: block.blockType,
|
|
201
|
+
type: 'storage',
|
|
202
|
+
key,
|
|
203
|
+
mediaType: 'text/csv',
|
|
204
|
+
filename: block.filename,
|
|
205
|
+
headers: block.headers,
|
|
206
|
+
rowCount: block.rowCount,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
193
209
|
return block;
|
|
194
210
|
}));
|
|
195
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Upload a file to Moxn storage and return the storage key.
|
|
214
|
+
*/
|
|
215
|
+
async uploadFile(data, mimeType, filename) {
|
|
216
|
+
// 1. Get presigned upload URL
|
|
217
|
+
const { key, uploadUrl } = await this.getUploadUrl(mimeType, filename);
|
|
218
|
+
// 2. PUT file to presigned URL
|
|
219
|
+
const response = await fetch(uploadUrl, {
|
|
220
|
+
method: 'PUT',
|
|
221
|
+
headers: { 'Content-Type': mimeType },
|
|
222
|
+
body: new Uint8Array(data),
|
|
223
|
+
});
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`File upload failed: ${response.status}`);
|
|
226
|
+
}
|
|
227
|
+
return { key };
|
|
228
|
+
}
|
|
229
|
+
async getUploadUrl(type, filename) {
|
|
230
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
'x-api-key': this.apiKey,
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify({ type, filename }),
|
|
237
|
+
});
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
const body = await response.text();
|
|
240
|
+
throw new Error(`Upload URL request failed: ${response.status} ${body}`);
|
|
241
|
+
}
|
|
242
|
+
const data = await response.json();
|
|
243
|
+
return { key: data.key, uploadUrl: data.uploadUrl };
|
|
244
|
+
}
|
|
196
245
|
async createDocument(request) {
|
|
197
246
|
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents`, {
|
|
198
247
|
method: 'POST',
|
package/dist/export.js
CHANGED
|
@@ -10,15 +10,9 @@ import { MoxnClient } from './client.js';
|
|
|
10
10
|
// ──────────────────────────────────────────────
|
|
11
11
|
// Utility functions
|
|
12
12
|
// ──────────────────────────────────────────────
|
|
13
|
-
/** Strip <moxn:comment> tags from text, preserving the inner text.
|
|
13
|
+
/** Strip <moxn:comment> tags from text, preserving the inner text. */
|
|
14
14
|
function stripCommentTags(text) {
|
|
15
|
-
|
|
16
|
-
let result = text;
|
|
17
|
-
// Loop until no more comment tags remain (handles nested/double-wrapped tags from TipTap CRDT)
|
|
18
|
-
while (pattern.test(result)) {
|
|
19
|
-
result = result.replace(pattern, '$1');
|
|
20
|
-
}
|
|
21
|
-
return result;
|
|
15
|
+
return text.replace(/<moxn:comment[^>]*>([\s\S]*?)<\/moxn:comment>/g, '$1');
|
|
22
16
|
}
|
|
23
17
|
/** Derive a local filename from a storage key or URL. */
|
|
24
18
|
function deriveFilename(storageKey, url, fallbackExt) {
|
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
|
package/dist/types.d.ts
CHANGED
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
* A content block in a section
|
|
6
6
|
*/
|
|
7
7
|
export interface ContentBlock {
|
|
8
|
-
blockType: 'text' | 'image' | 'document';
|
|
8
|
+
blockType: 'text' | 'image' | 'document' | 'csv';
|
|
9
9
|
text?: string;
|
|
10
|
-
type?: 'base64' | 'url' | 'file';
|
|
10
|
+
type?: 'base64' | 'url' | 'file' | 'storage';
|
|
11
11
|
mediaType?: string;
|
|
12
12
|
path?: string;
|
|
13
13
|
url?: string;
|
|
14
14
|
base64?: string;
|
|
15
|
+
key?: string;
|
|
15
16
|
alt?: string;
|
|
16
17
|
filename?: string;
|
|
18
|
+
/** CSV-specific metadata */
|
|
19
|
+
headers?: string[];
|
|
20
|
+
rowCount?: number;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* A section to be created in a document
|
package/package.json
CHANGED