@moxn/kb-migrate 0.4.41 → 0.6.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/api-error.d.ts +7 -0
- package/dist/api-error.js +21 -0
- package/dist/blocks-to-grammar.d.ts +51 -0
- package/dist/blocks-to-grammar.js +133 -0
- package/dist/client-http.test.d.ts +1 -0
- package/dist/client-http.test.js +89 -0
- package/dist/client.d.ts +76 -3
- package/dist/client.js +180 -154
- package/dist/export.js +94 -4
- package/dist/export.test.js +121 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +16 -0
- package/dist/import-local.js +1 -0
- package/dist/import-local.test.js +96 -0
- package/dist/index.js +39 -12
- package/dist/migrate-document.test.js +100 -20
- package/dist/output.d.ts +17 -0
- package/dist/output.js +65 -0
- package/dist/output.test.d.ts +1 -0
- package/dist/output.test.js +59 -0
- package/dist/sources/onenote/__tests__/onenote-tree.test.d.ts +1 -0
- package/dist/sources/onenote/__tests__/onenote-tree.test.js +43 -0
- package/dist/targets/notion.js +3 -2
- package/dist/types.d.ts +6 -1
- package/package.json +3 -2
package/dist/client.js
CHANGED
|
@@ -2,12 +2,40 @@
|
|
|
2
2
|
* API client for Moxn KB
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
|
-
import { formatApiError } from './api-error.js';
|
|
5
|
+
import { formatApiError, readJson } from './api-error.js';
|
|
6
|
+
import { moxnFetch } from './http.js';
|
|
7
|
+
import { sectionsToGrammarMarkdown, buildImportFrontMatter, } from './blocks-to-grammar.js';
|
|
8
|
+
/**
|
|
9
|
+
* The warning (if any) for an import whose `--default-permission` /
|
|
10
|
+
* `--ai-access` did not take effect: an existing document is never
|
|
11
|
+
* re-permissioned by a re-import, and a server that predates the fields
|
|
12
|
+
* creates documents with its defaults without echoing `permissions`.
|
|
13
|
+
*/
|
|
14
|
+
export function importPermissionWarning(requested, result) {
|
|
15
|
+
const asked = [
|
|
16
|
+
requested.defaultPermission &&
|
|
17
|
+
`--default-permission ${requested.defaultPermission}`,
|
|
18
|
+
requested.aiAccess && `--ai-access ${requested.aiAccess}`,
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
if (asked.length === 0 || result.outcome === 'skipped')
|
|
21
|
+
return undefined;
|
|
22
|
+
if (result.outcome === 'updated') {
|
|
23
|
+
return `${asked.join(', ')} not applied: this updated an existing document, whose permissions are left as they were (change them in the app)`;
|
|
24
|
+
}
|
|
25
|
+
if (!result.permissions) {
|
|
26
|
+
return `${asked.join(', ')}: the server did not apply them (it predates importer permissions) — the document was created with the server defaults`;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
6
30
|
export class MoxnClient {
|
|
7
31
|
apiUrl;
|
|
8
32
|
apiKey;
|
|
9
33
|
defaultPermission;
|
|
10
34
|
aiAccess;
|
|
35
|
+
/** Every request goes through {@link moxnFetch} (bypass-header aware). */
|
|
36
|
+
fetch(url, init = {}) {
|
|
37
|
+
return moxnFetch(url, init);
|
|
38
|
+
}
|
|
11
39
|
constructor(options) {
|
|
12
40
|
this.apiUrl = options.apiUrl.replace(/\/$/, '');
|
|
13
41
|
this.apiKey = options.apiKey;
|
|
@@ -21,6 +49,17 @@ export class MoxnClient {
|
|
|
21
49
|
/**
|
|
22
50
|
* Migrate a single document
|
|
23
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Migrate one extracted document via `import_markdown` (Phase B of the
|
|
54
|
+
* write-surface tightening — the REST block-input path is retired).
|
|
55
|
+
*
|
|
56
|
+
* Flow: upload/re-host every media block (local files, base64 payloads,
|
|
57
|
+
* remote URLs) → serialize the sections to grammar markdown → prepend a
|
|
58
|
+
* name/description front-matter → UPSERT via `import_markdown`
|
|
59
|
+
* (create-or-replace_document; `onConflict: 'skip'` skips an existing
|
|
60
|
+
* path). Idempotent: a byte-identical re-import is reported as a no-op
|
|
61
|
+
* update by the server.
|
|
62
|
+
*/
|
|
24
63
|
async migrateDocument(doc, basePath, onConflict, dryRun) {
|
|
25
64
|
const startTime = Date.now();
|
|
26
65
|
const documentPath = this.buildPath(basePath, doc.relativePath);
|
|
@@ -34,91 +73,37 @@ export class MoxnClient {
|
|
|
34
73
|
};
|
|
35
74
|
}
|
|
36
75
|
try {
|
|
37
|
-
//
|
|
76
|
+
// Upload/re-host media (local paths, base64, remote URLs) → storage keys.
|
|
38
77
|
const processedSections = await this.processSections(doc.sections);
|
|
39
|
-
|
|
40
|
-
|
|
78
|
+
const { markdown: body, dropped } = sectionsToGrammarMarkdown(processedSections);
|
|
79
|
+
if (dropped > 0) {
|
|
80
|
+
console.error(` ! ${dropped} media block(s) had no uploadable payload and were dropped: ${doc.sourcePath}`);
|
|
81
|
+
}
|
|
82
|
+
const markdown = buildImportFrontMatter(doc.name, doc.description) + body;
|
|
83
|
+
const result = await this.importMarkdown({
|
|
84
|
+
markdown,
|
|
41
85
|
path: documentPath,
|
|
42
|
-
|
|
43
|
-
description: doc.description,
|
|
44
|
-
defaultPermission: this.defaultPermission,
|
|
45
|
-
aiAccess: this.aiAccess,
|
|
46
|
-
sections: processedSections,
|
|
86
|
+
onConflict,
|
|
47
87
|
});
|
|
48
88
|
return {
|
|
49
89
|
sourcePath: doc.sourcePath,
|
|
50
|
-
documentPath,
|
|
51
|
-
status: 'created'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
documentPath: result.path,
|
|
91
|
+
status: result.outcome === 'created'
|
|
92
|
+
? 'created'
|
|
93
|
+
: result.outcome === 'updated'
|
|
94
|
+
? 'updated'
|
|
95
|
+
: 'skipped',
|
|
96
|
+
documentId: result.id,
|
|
97
|
+
branchId: result.branchId,
|
|
98
|
+
sectionsCount: result.sectionIds?.length,
|
|
99
|
+
sectionIds: result.sectionIds,
|
|
56
100
|
references: doc.references,
|
|
57
101
|
sourcePageId: doc.metadata?.notionPageId,
|
|
102
|
+
...this.permissionWarningFor(result, doc.sourcePath),
|
|
58
103
|
duration: Date.now() - startTime,
|
|
59
104
|
};
|
|
60
105
|
}
|
|
61
106
|
catch (error) {
|
|
62
|
-
// Check for conflict (409)
|
|
63
|
-
if (this.isConflictError(error)) {
|
|
64
|
-
if (onConflict === 'skip') {
|
|
65
|
-
return {
|
|
66
|
-
sourcePath: doc.sourcePath,
|
|
67
|
-
documentPath,
|
|
68
|
-
status: 'skipped',
|
|
69
|
-
documentId: error.documentId,
|
|
70
|
-
branchId: error.branchId,
|
|
71
|
-
duration: Date.now() - startTime,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
// Update existing document
|
|
75
|
-
try {
|
|
76
|
-
const processedSections = await this.processSections(doc.sections);
|
|
77
|
-
const updateResult = await this.updateDocument(error.documentId, {
|
|
78
|
-
name: doc.name,
|
|
79
|
-
description: doc.description,
|
|
80
|
-
sections: processedSections,
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
sourcePath: doc.sourcePath,
|
|
84
|
-
documentPath,
|
|
85
|
-
status: 'updated',
|
|
86
|
-
documentId: updateResult.id,
|
|
87
|
-
branchId: updateResult.branchId,
|
|
88
|
-
sectionsCount: updateResult.sections.length,
|
|
89
|
-
sectionIds: updateResult.sections.map((s) => s.id),
|
|
90
|
-
references: doc.references,
|
|
91
|
-
sourcePageId: doc.metadata?.notionPageId,
|
|
92
|
-
duration: Date.now() - startTime,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
catch (updateError) {
|
|
96
|
-
// Re-importing BYTE-IDENTICAL content makes the server reject the
|
|
97
|
-
// empty commit ("No changes to commit"). For an importer that's a
|
|
98
|
-
// NO-OP, not a failure — report a successful (no-op) update, matching
|
|
99
|
-
// the grammar path's idempotent re-import. (Audit finding B.)
|
|
100
|
-
const msg = updateError instanceof Error ? updateError.message : '';
|
|
101
|
-
if (msg.includes('No changes to commit')) {
|
|
102
|
-
return {
|
|
103
|
-
sourcePath: doc.sourcePath,
|
|
104
|
-
documentPath,
|
|
105
|
-
status: 'updated',
|
|
106
|
-
documentId: error.documentId,
|
|
107
|
-
branchId: error.branchId,
|
|
108
|
-
sourcePageId: doc.metadata?.notionPageId,
|
|
109
|
-
duration: Date.now() - startTime,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
sourcePath: doc.sourcePath,
|
|
114
|
-
documentPath,
|
|
115
|
-
status: 'failed',
|
|
116
|
-
documentId: error.documentId,
|
|
117
|
-
error: updateError instanceof Error ? updateError.message : 'Update failed',
|
|
118
|
-
duration: Date.now() - startTime,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
107
|
return {
|
|
123
108
|
sourcePath: doc.sourcePath,
|
|
124
109
|
documentPath,
|
|
@@ -128,6 +113,17 @@ export class MoxnClient {
|
|
|
128
113
|
};
|
|
129
114
|
}
|
|
130
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* {@link importPermissionWarning} for this client's requested permissions,
|
|
118
|
+
* logged once and returned as a spreadable `{ warning }`.
|
|
119
|
+
*/
|
|
120
|
+
permissionWarningFor(result, sourcePath) {
|
|
121
|
+
const warning = importPermissionWarning({ defaultPermission: this.defaultPermission, aiAccess: this.aiAccess }, result);
|
|
122
|
+
if (!warning)
|
|
123
|
+
return {};
|
|
124
|
+
console.warn(` ⚠ ${warning}: ${sourcePath}`);
|
|
125
|
+
return { warning };
|
|
126
|
+
}
|
|
131
127
|
// ──────────────────────────────────────────────
|
|
132
128
|
// Export methods
|
|
133
129
|
// ──────────────────────────────────────────────
|
|
@@ -155,14 +151,14 @@ export class MoxnClient {
|
|
|
155
151
|
params.set('modifiedAfter', dateFilter.modifiedAfter);
|
|
156
152
|
if (dateFilter?.modifiedBefore)
|
|
157
153
|
params.set('modifiedBefore', dateFilter.modifiedBefore);
|
|
158
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents?${params}`, {
|
|
154
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents?${params}`, {
|
|
159
155
|
headers: { 'x-api-key': this.apiKey },
|
|
160
156
|
});
|
|
161
157
|
if (!response.ok) {
|
|
162
158
|
const error = await response.text();
|
|
163
159
|
throw new Error(`Failed to list documents: ${response.status} ${error}`);
|
|
164
160
|
}
|
|
165
|
-
const data = await response
|
|
161
|
+
const data = await readJson(response);
|
|
166
162
|
allDocs.push(...data.documents);
|
|
167
163
|
if (!data.pagination.hasMore)
|
|
168
164
|
break;
|
|
@@ -174,14 +170,14 @@ export class MoxnClient {
|
|
|
174
170
|
* Get full document detail with sections and content.
|
|
175
171
|
*/
|
|
176
172
|
async getDocument(documentId) {
|
|
177
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
173
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
178
174
|
headers: { 'x-api-key': this.apiKey },
|
|
179
175
|
});
|
|
180
176
|
if (!response.ok) {
|
|
181
177
|
const error = await response.text();
|
|
182
178
|
throw new Error(`Failed to get document ${documentId}: ${response.status} ${error}`);
|
|
183
179
|
}
|
|
184
|
-
return response
|
|
180
|
+
return readJson(response);
|
|
185
181
|
}
|
|
186
182
|
/**
|
|
187
183
|
* Get a document's GRAMMAR markdown (front-matter + body + `:::image/:::csv/
|
|
@@ -193,7 +189,7 @@ export class MoxnClient {
|
|
|
193
189
|
* address sections stably.
|
|
194
190
|
*/
|
|
195
191
|
async getDocumentMarkdown(documentId, opts) {
|
|
196
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
192
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
197
193
|
method: 'POST',
|
|
198
194
|
headers: {
|
|
199
195
|
'Content-Type': 'application/json',
|
|
@@ -209,9 +205,38 @@ export class MoxnClient {
|
|
|
209
205
|
const body = await response.text();
|
|
210
206
|
throw new Error(`Failed to get document markdown ${documentId}: ${response.status} ${body}`);
|
|
211
207
|
}
|
|
212
|
-
const data = await response
|
|
208
|
+
const data = await readJson(response);
|
|
213
209
|
return (data.result?.document ?? null);
|
|
214
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Read one document through the `read` tool (`/api/v1/kb/tools`) — the same
|
|
213
|
+
* serialization agents and the CLI see. The export uses it for the kinds the
|
|
214
|
+
* grammar export has no body for (report / file / slides). Returns the single
|
|
215
|
+
* read item: a document shape carrying `kind`, or an `{ error }` item.
|
|
216
|
+
*/
|
|
217
|
+
async readDocument(documentId) {
|
|
218
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/tools`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
headers: {
|
|
221
|
+
'Content-Type': 'application/json',
|
|
222
|
+
'x-api-key': this.apiKey,
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
tool: 'read',
|
|
226
|
+
params: { items: [{ type: 'document', documentId }] },
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
const body = await response.json().catch(() => ({}));
|
|
231
|
+
throw new Error(formatApiError(response.status, body));
|
|
232
|
+
}
|
|
233
|
+
const data = await readJson(response);
|
|
234
|
+
const item = data.result?.items?.[0];
|
|
235
|
+
if (!item) {
|
|
236
|
+
throw new Error(`read returned no item for document ${documentId}`);
|
|
237
|
+
}
|
|
238
|
+
return item;
|
|
239
|
+
}
|
|
215
240
|
/**
|
|
216
241
|
* Build a `storageKey → signedDownloadUrl` map for a document's media, by
|
|
217
242
|
* reusing the `get_document_content` export action (the blocks read, which
|
|
@@ -221,7 +246,7 @@ export class MoxnClient {
|
|
|
221
246
|
* Returns an empty map when the document has no resolvable media.
|
|
222
247
|
*/
|
|
223
248
|
async getDocumentMediaUrls(documentId) {
|
|
224
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
249
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/export`, {
|
|
225
250
|
method: 'POST',
|
|
226
251
|
headers: {
|
|
227
252
|
'Content-Type': 'application/json',
|
|
@@ -233,7 +258,7 @@ export class MoxnClient {
|
|
|
233
258
|
const body = await response.text();
|
|
234
259
|
throw new Error(`Failed to get document content ${documentId}: ${response.status} ${body}`);
|
|
235
260
|
}
|
|
236
|
-
const data = await response
|
|
261
|
+
const data = await readJson(response);
|
|
237
262
|
const doc = data.result?.document;
|
|
238
263
|
const map = new Map();
|
|
239
264
|
if (!doc)
|
|
@@ -258,7 +283,7 @@ export class MoxnClient {
|
|
|
258
283
|
* onConflict 'update' → replace_document (full-set PUT)
|
|
259
284
|
*/
|
|
260
285
|
async importMarkdown(input) {
|
|
261
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/import`, {
|
|
286
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/import`, {
|
|
262
287
|
method: 'POST',
|
|
263
288
|
headers: {
|
|
264
289
|
'Content-Type': 'application/json',
|
|
@@ -269,20 +294,25 @@ export class MoxnClient {
|
|
|
269
294
|
markdown: input.markdown,
|
|
270
295
|
path: input.path,
|
|
271
296
|
onConflict: input.onConflict,
|
|
297
|
+
// Applied by the server to a document this import CREATES.
|
|
298
|
+
...(this.defaultPermission
|
|
299
|
+
? { defaultPermission: this.defaultPermission }
|
|
300
|
+
: {}),
|
|
301
|
+
...(this.aiAccess ? { aiAccess: this.aiAccess } : {}),
|
|
272
302
|
}),
|
|
273
303
|
});
|
|
274
304
|
if (!response.ok) {
|
|
275
305
|
const body = await response.json().catch(() => ({}));
|
|
276
306
|
throw new Error(formatApiError(response.status, body));
|
|
277
307
|
}
|
|
278
|
-
const data = await response
|
|
308
|
+
const data = await readJson(response);
|
|
279
309
|
return data.result;
|
|
280
310
|
}
|
|
281
311
|
/**
|
|
282
312
|
* Download a file from a URL to a local path.
|
|
283
313
|
*/
|
|
284
314
|
async downloadFile(url, destPath) {
|
|
285
|
-
const response = await fetch(url);
|
|
315
|
+
const response = await this.fetch(url);
|
|
286
316
|
if (!response.ok) {
|
|
287
317
|
throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
288
318
|
}
|
|
@@ -358,6 +388,45 @@ export class MoxnClient {
|
|
|
358
388
|
filename: block.filename,
|
|
359
389
|
};
|
|
360
390
|
}
|
|
391
|
+
// Base64 payloads (extractors that inline-downloaded media) → upload.
|
|
392
|
+
if ((block.blockType === 'image' ||
|
|
393
|
+
block.blockType === 'document' ||
|
|
394
|
+
block.blockType === 'csv' ||
|
|
395
|
+
block.blockType === 'file') &&
|
|
396
|
+
block.type === 'base64' &&
|
|
397
|
+
block.base64) {
|
|
398
|
+
const data = Buffer.from(block.base64, 'base64');
|
|
399
|
+
const filename = ('filename' in block ? block.filename : undefined) || 'media';
|
|
400
|
+
const { key } = await this.uploadFile(data, block.mediaType || 'application/octet-stream', filename);
|
|
401
|
+
return { ...block, type: 'storage', key, base64: undefined };
|
|
402
|
+
}
|
|
403
|
+
// Remote URLs (e.g. expiring Notion signed URLs) → download + re-host,
|
|
404
|
+
// matching the durability the server-side blocksToTipTap re-hosting
|
|
405
|
+
// used to provide. A failed download keeps the URL ref (the grammar
|
|
406
|
+
// carries external URLs) rather than failing the document.
|
|
407
|
+
if ((block.blockType === 'image' ||
|
|
408
|
+
block.blockType === 'document' ||
|
|
409
|
+
block.blockType === 'csv' ||
|
|
410
|
+
block.blockType === 'file') &&
|
|
411
|
+
block.type === 'url' &&
|
|
412
|
+
block.url) {
|
|
413
|
+
try {
|
|
414
|
+
const response = await this.fetch(block.url);
|
|
415
|
+
if (!response.ok)
|
|
416
|
+
throw new Error(`HTTP ${response.status}`);
|
|
417
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
418
|
+
const filename = ('filename' in block ? block.filename : undefined) ||
|
|
419
|
+
new URL(block.url).pathname.split('/').pop() ||
|
|
420
|
+
'media';
|
|
421
|
+
const { key } = await this.uploadFile(data, block.mediaType || 'application/octet-stream', filename);
|
|
422
|
+
return { ...block, type: 'storage', key, url: undefined };
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
426
|
+
console.error(` ! media re-host failed, keeping external URL ref: ${block.url}: ${msg}`);
|
|
427
|
+
return block;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
361
430
|
return block;
|
|
362
431
|
}));
|
|
363
432
|
}
|
|
@@ -368,7 +437,7 @@ export class MoxnClient {
|
|
|
368
437
|
// 1. Get presigned upload URL
|
|
369
438
|
const { key, uploadUrl } = await this.getUploadUrl(mimeType, filename);
|
|
370
439
|
// 2. PUT file to presigned URL
|
|
371
|
-
const response = await fetch(uploadUrl, {
|
|
440
|
+
const response = await this.fetch(uploadUrl, {
|
|
372
441
|
method: 'PUT',
|
|
373
442
|
headers: { 'Content-Type': mimeType },
|
|
374
443
|
body: new Uint8Array(data),
|
|
@@ -379,7 +448,7 @@ export class MoxnClient {
|
|
|
379
448
|
return { key };
|
|
380
449
|
}
|
|
381
450
|
async getUploadUrl(type, filename) {
|
|
382
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
451
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/upload`, {
|
|
383
452
|
method: 'POST',
|
|
384
453
|
headers: {
|
|
385
454
|
'Content-Type': 'application/json',
|
|
@@ -391,45 +460,9 @@ export class MoxnClient {
|
|
|
391
460
|
const body = await response.text();
|
|
392
461
|
throw new Error(`Upload URL request failed: ${response.status} ${body}`);
|
|
393
462
|
}
|
|
394
|
-
const data = await response
|
|
463
|
+
const data = await readJson(response);
|
|
395
464
|
return { key: data.key, uploadUrl: data.uploadUrl };
|
|
396
465
|
}
|
|
397
|
-
async createDocument(request) {
|
|
398
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents`, {
|
|
399
|
-
method: 'POST',
|
|
400
|
-
headers: {
|
|
401
|
-
'Content-Type': 'application/json',
|
|
402
|
-
'x-api-key': this.apiKey,
|
|
403
|
-
},
|
|
404
|
-
body: JSON.stringify(request),
|
|
405
|
-
});
|
|
406
|
-
if (!response.ok) {
|
|
407
|
-
const body = await response.json().catch(() => ({}));
|
|
408
|
-
if (response.status === 409 && body.documentId) {
|
|
409
|
-
const error = new Error(body.error || 'Document already exists');
|
|
410
|
-
error.documentId = body.documentId;
|
|
411
|
-
error.branchId = body.branchId;
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
throw new Error(formatApiError(response.status, body));
|
|
415
|
-
}
|
|
416
|
-
return response.json();
|
|
417
|
-
}
|
|
418
|
-
async updateDocument(documentId, request) {
|
|
419
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}`, {
|
|
420
|
-
method: 'PUT',
|
|
421
|
-
headers: {
|
|
422
|
-
'Content-Type': 'application/json',
|
|
423
|
-
'x-api-key': this.apiKey,
|
|
424
|
-
},
|
|
425
|
-
body: JSON.stringify(request),
|
|
426
|
-
});
|
|
427
|
-
if (!response.ok) {
|
|
428
|
-
const body = await response.json().catch(() => ({}));
|
|
429
|
-
throw new Error(formatApiError(response.status, body));
|
|
430
|
-
}
|
|
431
|
-
return response.json();
|
|
432
|
-
}
|
|
433
466
|
// ──────────────────────────────────────────────
|
|
434
467
|
// Database & tag methods (for Notion import)
|
|
435
468
|
// ──────────────────────────────────────────────
|
|
@@ -437,7 +470,7 @@ export class MoxnClient {
|
|
|
437
470
|
* Create a KB database.
|
|
438
471
|
*/
|
|
439
472
|
async createDatabase(input) {
|
|
440
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
473
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
441
474
|
method: 'POST',
|
|
442
475
|
headers: {
|
|
443
476
|
'Content-Type': 'application/json',
|
|
@@ -449,13 +482,13 @@ export class MoxnClient {
|
|
|
449
482
|
const body = await response.json().catch(() => ({}));
|
|
450
483
|
throw new Error(body.error || `Failed to create database: ${response.status}`);
|
|
451
484
|
}
|
|
452
|
-
return response
|
|
485
|
+
return readJson(response);
|
|
453
486
|
}
|
|
454
487
|
/**
|
|
455
488
|
* Add a column to a KB database.
|
|
456
489
|
*/
|
|
457
490
|
async addDatabaseColumn(databaseId, input) {
|
|
458
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/columns`, {
|
|
491
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/columns`, {
|
|
459
492
|
method: 'POST',
|
|
460
493
|
headers: {
|
|
461
494
|
'Content-Type': 'application/json',
|
|
@@ -467,13 +500,13 @@ export class MoxnClient {
|
|
|
467
500
|
const body = await response.json().catch(() => ({}));
|
|
468
501
|
throw new Error(body.error || `Failed to add column: ${response.status}`);
|
|
469
502
|
}
|
|
470
|
-
return response
|
|
503
|
+
return readJson(response);
|
|
471
504
|
}
|
|
472
505
|
/**
|
|
473
506
|
* Add a document to a KB database.
|
|
474
507
|
*/
|
|
475
508
|
async addDocumentToDatabase(databaseId, documentId) {
|
|
476
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/documents`, {
|
|
509
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/documents`, {
|
|
477
510
|
method: 'POST',
|
|
478
511
|
headers: {
|
|
479
512
|
'Content-Type': 'application/json',
|
|
@@ -493,7 +526,7 @@ export class MoxnClient {
|
|
|
493
526
|
* Create a tag (with automatic ancestor creation).
|
|
494
527
|
*/
|
|
495
528
|
async createTag(input) {
|
|
496
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/tags`, {
|
|
529
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/tags`, {
|
|
497
530
|
method: 'POST',
|
|
498
531
|
headers: {
|
|
499
532
|
'Content-Type': 'application/json',
|
|
@@ -513,13 +546,13 @@ export class MoxnClient {
|
|
|
513
546
|
}
|
|
514
547
|
throw new Error(body.error || `Failed to create tag: ${response.status}`);
|
|
515
548
|
}
|
|
516
|
-
return response
|
|
549
|
+
return readJson(response);
|
|
517
550
|
}
|
|
518
551
|
/**
|
|
519
552
|
* Assign a tag to a document.
|
|
520
553
|
*/
|
|
521
554
|
async assignTag(documentId, tagId, branchId) {
|
|
522
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/tags`, {
|
|
555
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/tags`, {
|
|
523
556
|
method: 'POST',
|
|
524
557
|
headers: {
|
|
525
558
|
'Content-Type': 'application/json',
|
|
@@ -537,7 +570,7 @@ export class MoxnClient {
|
|
|
537
570
|
* Returns { created, skipped, errors }.
|
|
538
571
|
*/
|
|
539
572
|
async createReferences(documentId, references) {
|
|
540
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/references`, {
|
|
573
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/documents/${documentId}/references`, {
|
|
541
574
|
method: 'POST',
|
|
542
575
|
headers: {
|
|
543
576
|
'Content-Type': 'application/json',
|
|
@@ -549,51 +582,51 @@ export class MoxnClient {
|
|
|
549
582
|
const body = await response.json().catch(() => ({}));
|
|
550
583
|
throw new Error(body.error || `Failed to create references: ${response.status}`);
|
|
551
584
|
}
|
|
552
|
-
return response
|
|
585
|
+
return readJson(response);
|
|
553
586
|
}
|
|
554
587
|
/**
|
|
555
588
|
* List all KB databases for the tenant.
|
|
556
589
|
*/
|
|
557
590
|
async listDatabases() {
|
|
558
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
591
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases`, {
|
|
559
592
|
headers: { 'x-api-key': this.apiKey },
|
|
560
593
|
});
|
|
561
594
|
if (!response.ok) {
|
|
562
595
|
const body = await response.json().catch(() => ({}));
|
|
563
596
|
throw new Error(body.error || `Failed to list databases: ${response.status}`);
|
|
564
597
|
}
|
|
565
|
-
const data = await response
|
|
598
|
+
const data = await readJson(response);
|
|
566
599
|
return data.databases;
|
|
567
600
|
}
|
|
568
601
|
/**
|
|
569
602
|
* Get fully resolved database with columns, options, and document property values.
|
|
570
603
|
*/
|
|
571
604
|
async resolveDatabase(databaseId) {
|
|
572
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/resolve`, {
|
|
605
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/resolve`, {
|
|
573
606
|
headers: { 'x-api-key': this.apiKey },
|
|
574
607
|
});
|
|
575
608
|
if (!response.ok) {
|
|
576
609
|
const body = await response.json().catch(() => ({}));
|
|
577
610
|
throw new Error(body.error || `Failed to resolve database: ${response.status}`);
|
|
578
611
|
}
|
|
579
|
-
return response
|
|
612
|
+
return readJson(response);
|
|
580
613
|
}
|
|
581
614
|
/**
|
|
582
615
|
* Get Notion database mapping for a KB database.
|
|
583
616
|
*/
|
|
584
617
|
async getNotionDatabaseMapping(kbDatabaseId) {
|
|
585
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases/by-kb-database/${kbDatabaseId}`, { headers: { 'x-api-key': this.apiKey } });
|
|
618
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases/by-kb-database/${kbDatabaseId}`, { headers: { 'x-api-key': this.apiKey } });
|
|
586
619
|
if (!response.ok) {
|
|
587
620
|
const body = await response.json().catch(() => ({}));
|
|
588
621
|
throw new Error(body.error || `Failed to get database mapping: ${response.status}`);
|
|
589
622
|
}
|
|
590
|
-
return response
|
|
623
|
+
return readJson(response);
|
|
591
624
|
}
|
|
592
625
|
/**
|
|
593
626
|
* Create or upsert a Notion database mapping.
|
|
594
627
|
*/
|
|
595
628
|
async createNotionDatabaseMapping(input) {
|
|
596
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
629
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
597
630
|
method: 'POST',
|
|
598
631
|
headers: {
|
|
599
632
|
'Content-Type': 'application/json',
|
|
@@ -605,13 +638,13 @@ export class MoxnClient {
|
|
|
605
638
|
const body = await response.json().catch(() => ({}));
|
|
606
639
|
throw new Error(body.error || `Failed to create database mapping: ${response.status}`);
|
|
607
640
|
}
|
|
608
|
-
return response
|
|
641
|
+
return readJson(response);
|
|
609
642
|
}
|
|
610
643
|
/**
|
|
611
644
|
* Set a scalar property value on a document in a database.
|
|
612
645
|
*/
|
|
613
646
|
async setPropertyValue(databaseId, documentId, columnName, value) {
|
|
614
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/properties`, {
|
|
647
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/properties`, {
|
|
615
648
|
method: 'POST',
|
|
616
649
|
headers: {
|
|
617
650
|
'Content-Type': 'application/json',
|
|
@@ -631,7 +664,7 @@ export class MoxnClient {
|
|
|
631
664
|
* Create or upsert a Notion page → KB document mapping.
|
|
632
665
|
*/
|
|
633
666
|
async createNotionPageMapping(input) {
|
|
634
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings`, {
|
|
667
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings`, {
|
|
635
668
|
method: 'POST',
|
|
636
669
|
headers: {
|
|
637
670
|
'Content-Type': 'application/json',
|
|
@@ -649,14 +682,14 @@ export class MoxnClient {
|
|
|
649
682
|
* Returns a Map of notionDatabaseId → kbDatabaseId.
|
|
650
683
|
*/
|
|
651
684
|
async getAllDatabaseMappings() {
|
|
652
|
-
const response = await fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
685
|
+
const response = await this.fetch(`${this.apiUrl}/api/v1/kb/notion-mappings/databases`, {
|
|
653
686
|
headers: { 'x-api-key': this.apiKey },
|
|
654
687
|
});
|
|
655
688
|
if (!response.ok) {
|
|
656
689
|
const body = await response.json().catch(() => ({}));
|
|
657
690
|
throw new Error(body.error || `Failed to get database mappings: ${response.status}`);
|
|
658
691
|
}
|
|
659
|
-
const data = await response
|
|
692
|
+
const data = await readJson(response);
|
|
660
693
|
const map = new Map();
|
|
661
694
|
for (const mapping of data.mappings) {
|
|
662
695
|
map.set(mapping.notionDatabaseId, mapping.kbDatabaseId);
|
|
@@ -670,11 +703,4 @@ export class MoxnClient {
|
|
|
670
703
|
const databases = await this.listDatabases();
|
|
671
704
|
return databases.find((db) => db.id === databaseId) ?? null;
|
|
672
705
|
}
|
|
673
|
-
isConflictError(error) {
|
|
674
|
-
return (error instanceof Error &&
|
|
675
|
-
'documentId' in error &&
|
|
676
|
-
'branchId' in error &&
|
|
677
|
-
typeof error.documentId === 'string' &&
|
|
678
|
-
typeof error.branchId === 'string');
|
|
679
|
-
}
|
|
680
706
|
}
|