@moxn/kb-migrate 0.4.18 → 0.4.20
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 +17 -3
- package/dist/client.js +17 -0
- package/dist/export-notion.js +8 -2
- package/dist/index.js +50 -0
- package/dist/sources/notion-api.d.ts +1 -0
- package/dist/sources/notion-databases.d.ts +28 -3
- package/dist/sources/notion-databases.js +135 -8
- package/dist/sources/notion-databases.test.js +1 -0
- package/dist/targets/index.d.ts +2 -2
- package/dist/targets/index.js +2 -2
- package/dist/targets/notion-database-export.js +131 -24
- package/dist/targets/notion.js +30 -11
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -53,9 +53,10 @@ export declare class MoxnClient {
|
|
|
53
53
|
*/
|
|
54
54
|
addDatabaseColumn(databaseId: string, input: {
|
|
55
55
|
name: string;
|
|
56
|
-
type: 'select' | 'multi_select';
|
|
56
|
+
type: 'select' | 'multi_select' | 'status' | 'checkbox' | 'number' | 'date' | 'text' | 'url' | 'email';
|
|
57
57
|
optionTagIds?: string[];
|
|
58
58
|
newOptionParentPath?: string;
|
|
59
|
+
config?: Record<string, unknown>;
|
|
59
60
|
}): Promise<{
|
|
60
61
|
id: string;
|
|
61
62
|
name: string;
|
|
@@ -97,9 +98,10 @@ export declare class MoxnClient {
|
|
|
97
98
|
columns: Array<{
|
|
98
99
|
id: string;
|
|
99
100
|
name: string;
|
|
100
|
-
type:
|
|
101
|
+
type: string;
|
|
101
102
|
position: number;
|
|
102
103
|
newOptionParentPath: string | null;
|
|
104
|
+
config?: Record<string, unknown> | null;
|
|
103
105
|
options: Array<{
|
|
104
106
|
tagId: string;
|
|
105
107
|
tagName: string;
|
|
@@ -114,13 +116,21 @@ export declare class MoxnClient {
|
|
|
114
116
|
properties: Record<string, {
|
|
115
117
|
columnId: string;
|
|
116
118
|
columnName: string;
|
|
117
|
-
columnType:
|
|
119
|
+
columnType: string;
|
|
118
120
|
values: Array<{
|
|
119
121
|
tagId: string;
|
|
120
122
|
tagName: string;
|
|
121
123
|
tagPath: string;
|
|
122
124
|
tagColor: string | null;
|
|
123
125
|
}>;
|
|
126
|
+
} | {
|
|
127
|
+
columnId: string;
|
|
128
|
+
columnName: string;
|
|
129
|
+
columnType: string;
|
|
130
|
+
textValue: string | null;
|
|
131
|
+
numberValue: number | null;
|
|
132
|
+
dateValue: string | null;
|
|
133
|
+
booleanValue: boolean | null;
|
|
124
134
|
}>;
|
|
125
135
|
}>;
|
|
126
136
|
}>;
|
|
@@ -147,5 +157,9 @@ export declare class MoxnClient {
|
|
|
147
157
|
id: string;
|
|
148
158
|
};
|
|
149
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* Set a scalar property value on a document in a database.
|
|
162
|
+
*/
|
|
163
|
+
setPropertyValue(databaseId: string, documentId: string, columnName: string, value: string | number | boolean | null): Promise<void>;
|
|
150
164
|
private isConflictError;
|
|
151
165
|
}
|
package/dist/client.js
CHANGED
|
@@ -470,6 +470,23 @@ export class MoxnClient {
|
|
|
470
470
|
}
|
|
471
471
|
return response.json();
|
|
472
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Set a scalar property value on a document in a database.
|
|
475
|
+
*/
|
|
476
|
+
async setPropertyValue(databaseId, documentId, columnName, value) {
|
|
477
|
+
const response = await fetch(`${this.apiUrl}/api/v1/kb/databases/${databaseId}/properties`, {
|
|
478
|
+
method: 'POST',
|
|
479
|
+
headers: {
|
|
480
|
+
'Content-Type': 'application/json',
|
|
481
|
+
'x-api-key': this.apiKey,
|
|
482
|
+
},
|
|
483
|
+
body: JSON.stringify({ documentId, columnName, value }),
|
|
484
|
+
});
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const body = await response.json().catch(() => ({}));
|
|
487
|
+
throw new Error(body.error || `Failed to set property value: ${response.status}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
473
490
|
isConflictError(error) {
|
|
474
491
|
return (error instanceof Error &&
|
|
475
492
|
'documentId' in error &&
|
package/dist/export-notion.js
CHANGED
|
@@ -137,7 +137,12 @@ export async function runNotionExport(options) {
|
|
|
137
137
|
// Use the parent page ID as the target for database creation
|
|
138
138
|
const parentPageId = options.parentPageId;
|
|
139
139
|
const result = await exportDatabase(dbCtx, dbId, parentPageId);
|
|
140
|
-
const icon = {
|
|
140
|
+
const icon = {
|
|
141
|
+
created: '\u2713',
|
|
142
|
+
updated: '\u21bb',
|
|
143
|
+
skipped: '-',
|
|
144
|
+
failed: '\u2717',
|
|
145
|
+
}[result.status];
|
|
141
146
|
console.log(` ${icon} ${result.status}: ${result.kbDatabaseName} (${result.entriesCreated} entries)`);
|
|
142
147
|
if (result.error)
|
|
143
148
|
console.log(` Error: ${result.error}`);
|
|
@@ -165,7 +170,8 @@ export async function runNotionExport(options) {
|
|
|
165
170
|
// ============================================
|
|
166
171
|
if (!options.dryRun) {
|
|
167
172
|
// Find documents that have references and were successfully exported
|
|
168
|
-
const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') &&
|
|
173
|
+
const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') &&
|
|
174
|
+
r.externalId);
|
|
169
175
|
if (successfulExports.length > 0) {
|
|
170
176
|
console.log(`\nPass 2: Resolving cross-references for ${successfulExports.length} documents...`);
|
|
171
177
|
let totalResolved = 0;
|
package/dist/index.js
CHANGED
|
@@ -370,6 +370,9 @@ program
|
|
|
370
370
|
for (const col of dbImport.schema.mappedColumns) {
|
|
371
371
|
allPropertyTypes.add(col.notionType);
|
|
372
372
|
}
|
|
373
|
+
for (const col of dbImport.schema.scalarColumns) {
|
|
374
|
+
allPropertyTypes.add(col.notionType);
|
|
375
|
+
}
|
|
373
376
|
for (const col of dbImport.schema.unmappedColumns) {
|
|
374
377
|
allPropertyTypes.add(col.notionType);
|
|
375
378
|
}
|
|
@@ -560,6 +563,24 @@ async function importNotionDatabase(client, dbImport, log, options, preCreatedDb
|
|
|
560
563
|
console.warn(` Warning: Failed to create column "${col.notionPropertyName}": ${error instanceof Error ? error.message : error}`);
|
|
561
564
|
}
|
|
562
565
|
}
|
|
566
|
+
// 2b. Create scalar columns (number, date, text, url, email)
|
|
567
|
+
const scalarColumnMap = new Map();
|
|
568
|
+
for (const col of schema.scalarColumns) {
|
|
569
|
+
try {
|
|
570
|
+
const column = await client.addDatabaseColumn(dbId, {
|
|
571
|
+
name: col.notionPropertyName,
|
|
572
|
+
type: col.moxnType,
|
|
573
|
+
});
|
|
574
|
+
scalarColumnMap.set(col.notionPropertyName, {
|
|
575
|
+
columnId: column.id,
|
|
576
|
+
moxnType: col.moxnType,
|
|
577
|
+
});
|
|
578
|
+
console.log(` Column "${col.notionPropertyName}" (${col.moxnType})`);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
console.warn(` Warning: Failed to create scalar column "${col.notionPropertyName}": ${error instanceof Error ? error.message : error}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
563
584
|
// 3. Link entries and assign tags
|
|
564
585
|
// Build a map of KB path → { documentId, branchId } from migration results
|
|
565
586
|
const docByPath = new Map();
|
|
@@ -605,6 +626,35 @@ async function importNotionDatabase(client, dbImport, log, options, preCreatedDb
|
|
|
605
626
|
}
|
|
606
627
|
}
|
|
607
628
|
}
|
|
629
|
+
// Set scalar property values
|
|
630
|
+
for (const [colName, scalarVal] of values.scalarValues) {
|
|
631
|
+
const scalarColInfo = scalarColumnMap.get(colName);
|
|
632
|
+
if (!scalarColInfo)
|
|
633
|
+
continue;
|
|
634
|
+
// Determine the value to send based on type
|
|
635
|
+
let value = null;
|
|
636
|
+
switch (scalarVal.moxnType) {
|
|
637
|
+
case 'number':
|
|
638
|
+
value = scalarVal.numberValue ?? null;
|
|
639
|
+
break;
|
|
640
|
+
case 'date':
|
|
641
|
+
value = scalarVal.dateValue ?? null;
|
|
642
|
+
break;
|
|
643
|
+
case 'text':
|
|
644
|
+
case 'url':
|
|
645
|
+
case 'email':
|
|
646
|
+
value = scalarVal.textValue ?? null;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
if (value != null) {
|
|
650
|
+
try {
|
|
651
|
+
await client.setPropertyValue(dbId, docInfo.documentId, colName, value);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
console.warn(` Warning: Failed to set "${colName}" on ${docInfo.documentId}: ${error instanceof Error ? error.message : error}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
608
658
|
}
|
|
609
659
|
catch (error) {
|
|
610
660
|
console.warn(` Warning: Failed to link entry to database: ${error instanceof Error ? error.message : error}`);
|
|
@@ -8,17 +8,30 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ContentBlock, SectionInput } from '../types.js';
|
|
10
10
|
import type { NotionDatabase, NotionPage } from './notion-api.js';
|
|
11
|
-
/** Parsed Notion column that maps to a Moxn DB column. */
|
|
11
|
+
/** Parsed Notion column that maps to a Moxn tag-based DB column. */
|
|
12
12
|
export interface MappedColumn {
|
|
13
13
|
notionPropertyId: string;
|
|
14
14
|
notionPropertyName: string;
|
|
15
|
-
notionType: 'select' | 'multi_select' | 'status';
|
|
16
|
-
moxnType: 'select' | 'multi_select';
|
|
15
|
+
notionType: 'select' | 'multi_select' | 'status' | 'checkbox';
|
|
16
|
+
moxnType: 'select' | 'multi_select' | 'status' | 'checkbox';
|
|
17
17
|
options: Array<{
|
|
18
18
|
notionId: string;
|
|
19
19
|
name: string;
|
|
20
20
|
color: string;
|
|
21
21
|
}>;
|
|
22
|
+
/** Status group metadata (only for status columns). */
|
|
23
|
+
groups?: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
color: string;
|
|
26
|
+
optionNames: string[];
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/** Parsed Notion column that maps to a Moxn scalar DB column. */
|
|
30
|
+
export interface ScalarColumn {
|
|
31
|
+
notionPropertyId: string;
|
|
32
|
+
notionPropertyName: string;
|
|
33
|
+
notionType: 'number' | 'date' | 'url' | 'email' | 'rich_text' | 'checkbox';
|
|
34
|
+
moxnType: 'number' | 'date' | 'url' | 'email' | 'text';
|
|
22
35
|
}
|
|
23
36
|
/** Parsed Notion column that will be rendered as text in a Properties section. */
|
|
24
37
|
export interface UnmappedColumn {
|
|
@@ -31,14 +44,26 @@ export interface ParsedDatabaseSchema {
|
|
|
31
44
|
name: string;
|
|
32
45
|
description: string;
|
|
33
46
|
mappedColumns: MappedColumn[];
|
|
47
|
+
scalarColumns: ScalarColumn[];
|
|
34
48
|
unmappedColumns: UnmappedColumn[];
|
|
35
49
|
}
|
|
50
|
+
/** Scalar value for a single database entry property. */
|
|
51
|
+
export interface ScalarValue {
|
|
52
|
+
columnName: string;
|
|
53
|
+
moxnType: 'number' | 'date' | 'url' | 'email' | 'text';
|
|
54
|
+
textValue?: string | null;
|
|
55
|
+
numberValue?: number | null;
|
|
56
|
+
dateValue?: string | null;
|
|
57
|
+
booleanValue?: boolean | null;
|
|
58
|
+
}
|
|
36
59
|
/** Parsed property values for a single database entry. */
|
|
37
60
|
export interface ParsedEntryValues {
|
|
38
61
|
/** Document name derived from title property. */
|
|
39
62
|
title: string;
|
|
40
63
|
/** Mapped column values: column name → selected option names. */
|
|
41
64
|
tagValues: Map<string, string[]>;
|
|
65
|
+
/** Scalar property values: column name → scalar value. */
|
|
66
|
+
scalarValues: Map<string, ScalarValue>;
|
|
42
67
|
/** Unmapped property values for markdown table: property name → rendered value. */
|
|
43
68
|
textValues: Map<string, string>;
|
|
44
69
|
/** File attachments to include as content blocks. */
|
|
@@ -17,6 +17,7 @@ export function parseDatabaseSchema(db) {
|
|
|
17
17
|
const name = richTextToPlain(db.title) || 'Untitled Database';
|
|
18
18
|
const description = richTextToPlain(db.description);
|
|
19
19
|
const mappedColumns = [];
|
|
20
|
+
const scalarColumns = [];
|
|
20
21
|
const unmappedColumns = [];
|
|
21
22
|
for (const [propName, prop] of Object.entries(db.properties)) {
|
|
22
23
|
// Skip title — it becomes the document name
|
|
@@ -49,17 +50,77 @@ export function parseDatabaseSchema(db) {
|
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
else if (prop.type === 'status' && prop.status) {
|
|
52
|
-
// Status
|
|
53
|
+
// Status → Moxn status column with group metadata
|
|
53
54
|
mappedColumns.push({
|
|
54
55
|
notionPropertyId: prop.id,
|
|
55
56
|
notionPropertyName: propName,
|
|
56
57
|
notionType: 'status',
|
|
57
|
-
moxnType: '
|
|
58
|
+
moxnType: 'status',
|
|
58
59
|
options: prop.status.options.map((o) => ({
|
|
59
60
|
notionId: o.id,
|
|
60
61
|
name: o.name,
|
|
61
62
|
color: o.color,
|
|
62
63
|
})),
|
|
64
|
+
groups: prop.status.groups?.map((g) => ({
|
|
65
|
+
name: g.name,
|
|
66
|
+
color: g.color ?? 'default',
|
|
67
|
+
optionNames: g.option_ids
|
|
68
|
+
? g.option_ids.map((id) => prop.status.options.find((o) => o.id === id)?.name ?? id)
|
|
69
|
+
: [],
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (prop.type === 'checkbox') {
|
|
74
|
+
// Checkbox → Moxn checkbox column (tag-based 2-option select)
|
|
75
|
+
mappedColumns.push({
|
|
76
|
+
notionPropertyId: prop.id,
|
|
77
|
+
notionPropertyName: propName,
|
|
78
|
+
notionType: 'checkbox',
|
|
79
|
+
moxnType: 'checkbox',
|
|
80
|
+
options: [
|
|
81
|
+
{ notionId: 'checked', name: 'Checked', color: 'green' },
|
|
82
|
+
{ notionId: 'unchecked', name: 'Unchecked', color: 'gray' },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (prop.type === 'number') {
|
|
87
|
+
scalarColumns.push({
|
|
88
|
+
notionPropertyId: prop.id,
|
|
89
|
+
notionPropertyName: propName,
|
|
90
|
+
notionType: 'number',
|
|
91
|
+
moxnType: 'number',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (prop.type === 'date') {
|
|
95
|
+
scalarColumns.push({
|
|
96
|
+
notionPropertyId: prop.id,
|
|
97
|
+
notionPropertyName: propName,
|
|
98
|
+
notionType: 'date',
|
|
99
|
+
moxnType: 'date',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (prop.type === 'url') {
|
|
103
|
+
scalarColumns.push({
|
|
104
|
+
notionPropertyId: prop.id,
|
|
105
|
+
notionPropertyName: propName,
|
|
106
|
+
notionType: 'url',
|
|
107
|
+
moxnType: 'url',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (prop.type === 'email') {
|
|
111
|
+
scalarColumns.push({
|
|
112
|
+
notionPropertyId: prop.id,
|
|
113
|
+
notionPropertyName: propName,
|
|
114
|
+
notionType: 'email',
|
|
115
|
+
moxnType: 'email',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (prop.type === 'rich_text') {
|
|
119
|
+
scalarColumns.push({
|
|
120
|
+
notionPropertyId: prop.id,
|
|
121
|
+
notionPropertyName: propName,
|
|
122
|
+
notionType: 'rich_text',
|
|
123
|
+
moxnType: 'text',
|
|
63
124
|
});
|
|
64
125
|
}
|
|
65
126
|
else {
|
|
@@ -74,7 +135,7 @@ export function parseDatabaseSchema(db) {
|
|
|
74
135
|
});
|
|
75
136
|
}
|
|
76
137
|
}
|
|
77
|
-
return { name, description, mappedColumns, unmappedColumns };
|
|
138
|
+
return { name, description, mappedColumns, scalarColumns, unmappedColumns };
|
|
78
139
|
}
|
|
79
140
|
// ============================================
|
|
80
141
|
// Entry Parsing
|
|
@@ -85,20 +146,38 @@ export function parseDatabaseSchema(db) {
|
|
|
85
146
|
export function parseEntryValues(page, schema) {
|
|
86
147
|
const title = getPageTitle(page);
|
|
87
148
|
const tagValues = new Map();
|
|
149
|
+
const scalarValues = new Map();
|
|
88
150
|
const textValues = new Map();
|
|
89
151
|
const fileBlocks = [];
|
|
90
152
|
// Build lookup by property name
|
|
91
153
|
const mappedByName = new Map(schema.mappedColumns.map((c) => [c.notionPropertyName, c]));
|
|
154
|
+
const scalarByName = new Map(schema.scalarColumns.map((c) => [c.notionPropertyName, c]));
|
|
92
155
|
const unmappedByName = new Set(schema.unmappedColumns.map((c) => c.notionPropertyName));
|
|
93
156
|
for (const [propName, propValue] of Object.entries(page.properties)) {
|
|
94
157
|
if (propValue.type === 'title')
|
|
95
158
|
continue; // Already extracted
|
|
96
|
-
// Check if this is a mapped column
|
|
159
|
+
// Check if this is a tag-based mapped column
|
|
97
160
|
const mapped = mappedByName.get(propName);
|
|
98
161
|
if (mapped) {
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
162
|
+
// Checkbox uses tag-based approach: extract "Checked"/"Unchecked" option name
|
|
163
|
+
if (mapped.moxnType === 'checkbox') {
|
|
164
|
+
const isChecked = propValue.checkbox === true;
|
|
165
|
+
tagValues.set(propName, [isChecked ? 'Checked' : 'Unchecked']);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const values = extractSelectValues(propValue, mapped.notionType);
|
|
169
|
+
if (values.length > 0) {
|
|
170
|
+
tagValues.set(propName, values);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Check if this is a scalar column
|
|
176
|
+
const scalar = scalarByName.get(propName);
|
|
177
|
+
if (scalar) {
|
|
178
|
+
const extracted = extractScalarValue(propValue, scalar);
|
|
179
|
+
if (extracted) {
|
|
180
|
+
scalarValues.set(propName, extracted);
|
|
102
181
|
}
|
|
103
182
|
continue;
|
|
104
183
|
}
|
|
@@ -147,7 +226,7 @@ export function parseEntryValues(page, schema) {
|
|
|
147
226
|
}
|
|
148
227
|
}
|
|
149
228
|
}
|
|
150
|
-
return { title, tagValues, textValues, fileBlocks };
|
|
229
|
+
return { title, tagValues, scalarValues, textValues, fileBlocks };
|
|
151
230
|
}
|
|
152
231
|
/**
|
|
153
232
|
* Render unmapped properties as a markdown table section.
|
|
@@ -187,6 +266,54 @@ function extractSelectValues(propValue, notionType) {
|
|
|
187
266
|
return [];
|
|
188
267
|
}
|
|
189
268
|
}
|
|
269
|
+
function extractScalarValue(propValue, scalarCol) {
|
|
270
|
+
switch (scalarCol.moxnType) {
|
|
271
|
+
case 'number':
|
|
272
|
+
if (propValue.number == null)
|
|
273
|
+
return null;
|
|
274
|
+
return {
|
|
275
|
+
columnName: scalarCol.notionPropertyName,
|
|
276
|
+
moxnType: 'number',
|
|
277
|
+
numberValue: propValue.number,
|
|
278
|
+
};
|
|
279
|
+
case 'date':
|
|
280
|
+
if (!propValue.date?.start)
|
|
281
|
+
return null;
|
|
282
|
+
return {
|
|
283
|
+
columnName: scalarCol.notionPropertyName,
|
|
284
|
+
moxnType: 'date',
|
|
285
|
+
dateValue: propValue.date.start,
|
|
286
|
+
};
|
|
287
|
+
case 'url':
|
|
288
|
+
if (!propValue.url)
|
|
289
|
+
return null;
|
|
290
|
+
return {
|
|
291
|
+
columnName: scalarCol.notionPropertyName,
|
|
292
|
+
moxnType: 'url',
|
|
293
|
+
textValue: propValue.url,
|
|
294
|
+
};
|
|
295
|
+
case 'email':
|
|
296
|
+
if (!propValue.email)
|
|
297
|
+
return null;
|
|
298
|
+
return {
|
|
299
|
+
columnName: scalarCol.notionPropertyName,
|
|
300
|
+
moxnType: 'email',
|
|
301
|
+
textValue: propValue.email,
|
|
302
|
+
};
|
|
303
|
+
case 'text': {
|
|
304
|
+
const text = richTextToPlain(propValue.rich_text ?? []);
|
|
305
|
+
if (!text)
|
|
306
|
+
return null;
|
|
307
|
+
return {
|
|
308
|
+
columnName: scalarCol.notionPropertyName,
|
|
309
|
+
moxnType: 'text',
|
|
310
|
+
textValue: text,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
default:
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
190
317
|
function renderPropertyValue(prop) {
|
|
191
318
|
switch (prop.type) {
|
|
192
319
|
case 'rich_text':
|
package/dist/targets/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target registry and factory
|
|
3
3
|
*/
|
|
4
|
-
export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog } from './base.js';
|
|
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';
|
|
6
|
+
export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext, } from './notion-database-export.js';
|
package/dist/targets/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Target registry and factory
|
|
3
3
|
*/
|
|
4
|
-
export { ExportTarget } from './base.js';
|
|
4
|
+
export { ExportTarget, } from './base.js';
|
|
5
5
|
export { NotionExportTarget } from './notion.js';
|
|
6
|
-
export { exportDatabase } from './notion-database-export.js';
|
|
6
|
+
export { exportDatabase, } from './notion-database-export.js';
|
|
@@ -28,7 +28,7 @@ const HEX_TO_NOTION_COLOR = {
|
|
|
28
28
|
function hexToNotionColor(hex) {
|
|
29
29
|
if (!hex)
|
|
30
30
|
return 'default';
|
|
31
|
-
return HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default';
|
|
31
|
+
return (HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default');
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Export a single KB database to Notion.
|
|
@@ -130,17 +130,62 @@ async function createNotionDatabase(ctx, parentPageId, resolved) {
|
|
|
130
130
|
name: opt.tagName,
|
|
131
131
|
color: hexToNotionColor(opt.tagColor),
|
|
132
132
|
}));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
switch (col.type) {
|
|
134
|
+
case 'select':
|
|
135
|
+
properties[col.name] = {
|
|
136
|
+
type: 'select',
|
|
137
|
+
select: { options },
|
|
138
|
+
};
|
|
139
|
+
break;
|
|
140
|
+
case 'multi_select':
|
|
141
|
+
properties[col.name] = {
|
|
142
|
+
type: 'multi_select',
|
|
143
|
+
multi_select: { options },
|
|
144
|
+
};
|
|
145
|
+
break;
|
|
146
|
+
case 'status':
|
|
147
|
+
// Status exports as select in Notion (status requires specific API access)
|
|
148
|
+
properties[col.name] = {
|
|
149
|
+
type: 'select',
|
|
150
|
+
select: { options },
|
|
151
|
+
};
|
|
152
|
+
break;
|
|
153
|
+
case 'checkbox':
|
|
154
|
+
properties[col.name] = {
|
|
155
|
+
type: 'checkbox',
|
|
156
|
+
checkbox: {},
|
|
157
|
+
};
|
|
158
|
+
break;
|
|
159
|
+
case 'number':
|
|
160
|
+
properties[col.name] = {
|
|
161
|
+
type: 'number',
|
|
162
|
+
number: { format: 'number' },
|
|
163
|
+
};
|
|
164
|
+
break;
|
|
165
|
+
case 'date':
|
|
166
|
+
properties[col.name] = {
|
|
167
|
+
type: 'date',
|
|
168
|
+
date: {},
|
|
169
|
+
};
|
|
170
|
+
break;
|
|
171
|
+
case 'url':
|
|
172
|
+
properties[col.name] = {
|
|
173
|
+
type: 'url',
|
|
174
|
+
url: {},
|
|
175
|
+
};
|
|
176
|
+
break;
|
|
177
|
+
case 'email':
|
|
178
|
+
properties[col.name] = {
|
|
179
|
+
type: 'email',
|
|
180
|
+
email: {},
|
|
181
|
+
};
|
|
182
|
+
break;
|
|
183
|
+
case 'text':
|
|
184
|
+
properties[col.name] = {
|
|
185
|
+
type: 'rich_text',
|
|
186
|
+
rich_text: {},
|
|
187
|
+
};
|
|
188
|
+
break;
|
|
144
189
|
}
|
|
145
190
|
}
|
|
146
191
|
await sleep(RATE_LIMIT_MS);
|
|
@@ -216,19 +261,81 @@ async function createDatabaseEntries(ctx, dataSourceId, resolved) {
|
|
|
216
261
|
};
|
|
217
262
|
// Add column values
|
|
218
263
|
for (const [colName, propValue] of Object.entries(doc.properties)) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
select:
|
|
225
|
-
|
|
264
|
+
// Tag-based columns (select, multi_select, status, checkbox)
|
|
265
|
+
if ('values' in propValue) {
|
|
266
|
+
if (propValue.values.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
switch (propValue.columnType) {
|
|
269
|
+
case 'select':
|
|
270
|
+
case 'status':
|
|
271
|
+
properties[colName] = {
|
|
272
|
+
type: 'select',
|
|
273
|
+
select: { name: propValue.values[0].tagName },
|
|
274
|
+
};
|
|
275
|
+
break;
|
|
276
|
+
case 'multi_select':
|
|
277
|
+
properties[colName] = {
|
|
278
|
+
type: 'multi_select',
|
|
279
|
+
multi_select: propValue.values.map((v) => ({ name: v.tagName })),
|
|
280
|
+
};
|
|
281
|
+
break;
|
|
282
|
+
case 'checkbox': {
|
|
283
|
+
// The first tag is the "checked" option
|
|
284
|
+
const isChecked = propValue.values[0]?.tagName === 'Checked';
|
|
285
|
+
properties[colName] = {
|
|
286
|
+
type: 'checkbox',
|
|
287
|
+
checkbox: isChecked,
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
226
292
|
}
|
|
227
|
-
else
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
293
|
+
else {
|
|
294
|
+
// Scalar columns (number, date, text, url, email)
|
|
295
|
+
if (propValue.textValue == null && propValue.numberValue == null && propValue.dateValue == null && propValue.booleanValue == null)
|
|
296
|
+
continue;
|
|
297
|
+
switch (propValue.columnType) {
|
|
298
|
+
case 'number':
|
|
299
|
+
if (propValue.numberValue != null) {
|
|
300
|
+
properties[colName] = {
|
|
301
|
+
type: 'number',
|
|
302
|
+
number: propValue.numberValue,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
case 'date':
|
|
307
|
+
if (propValue.dateValue) {
|
|
308
|
+
properties[colName] = {
|
|
309
|
+
type: 'date',
|
|
310
|
+
date: { start: propValue.dateValue },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
case 'url':
|
|
315
|
+
if (propValue.textValue) {
|
|
316
|
+
properties[colName] = {
|
|
317
|
+
type: 'url',
|
|
318
|
+
url: propValue.textValue,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
case 'email':
|
|
323
|
+
if (propValue.textValue) {
|
|
324
|
+
properties[colName] = {
|
|
325
|
+
type: 'email',
|
|
326
|
+
email: propValue.textValue,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
case 'text':
|
|
331
|
+
if (propValue.textValue) {
|
|
332
|
+
properties[colName] = {
|
|
333
|
+
type: 'rich_text',
|
|
334
|
+
rich_text: [{ type: 'text', text: { content: propValue.textValue } }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
232
339
|
}
|
|
233
340
|
}
|
|
234
341
|
await sleep(RATE_LIMIT_MS);
|
package/dist/targets/notion.js
CHANGED
|
@@ -196,7 +196,6 @@ async function uploadFileToNotion(client, url, filename, contentType) {
|
|
|
196
196
|
*/
|
|
197
197
|
function reconstructCallouts(blocks) {
|
|
198
198
|
const emojiPrefixRe = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s/u;
|
|
199
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
199
|
return blocks.map((block) => {
|
|
201
200
|
// Case 1: paragraph with emoji prefix
|
|
202
201
|
if (block.type === 'paragraph') {
|
|
@@ -214,7 +213,7 @@ function reconstructCallouts(blocks) {
|
|
|
214
213
|
const calloutRichText = [
|
|
215
214
|
{ ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
|
|
216
215
|
...richText.slice(1),
|
|
217
|
-
].filter((rt) => rt.text?.content);
|
|
216
|
+
].filter((rt) => 'text' in rt && rt.text?.content);
|
|
218
217
|
return {
|
|
219
218
|
object: 'block',
|
|
220
219
|
type: 'callout',
|
|
@@ -311,7 +310,11 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
311
310
|
for (let i = 0; i < parts.length; i++) {
|
|
312
311
|
if (i > 0) {
|
|
313
312
|
flushText();
|
|
314
|
-
allBlocks.push({
|
|
313
|
+
allBlocks.push({
|
|
314
|
+
object: 'block',
|
|
315
|
+
type: 'divider',
|
|
316
|
+
divider: {},
|
|
317
|
+
});
|
|
315
318
|
}
|
|
316
319
|
const part = parts[i];
|
|
317
320
|
if (part.trim()) {
|
|
@@ -323,9 +326,12 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
323
326
|
else if (block.blockType === 'image' && block.url) {
|
|
324
327
|
flushText();
|
|
325
328
|
if (block.storageKey && options?.notionClient) {
|
|
326
|
-
const fname = block.filename ||
|
|
329
|
+
const fname = block.filename ||
|
|
330
|
+
deriveFilename(block.storageKey, block.mimeType || 'image/png');
|
|
327
331
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'image/png');
|
|
328
|
-
allBlocks.push(uploadId
|
|
332
|
+
allBlocks.push(uploadId
|
|
333
|
+
? notionImageUploadBlock(uploadId, block.alt)
|
|
334
|
+
: notionImageBlock(block.url, block.alt));
|
|
329
335
|
}
|
|
330
336
|
else {
|
|
331
337
|
allBlocks.push(notionImageBlock(block.url, block.alt));
|
|
@@ -334,10 +340,13 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
334
340
|
else if (block.blockType === 'document' && block.url) {
|
|
335
341
|
flushText();
|
|
336
342
|
if (block.storageKey && options?.notionClient) {
|
|
337
|
-
const fname = block.filename ||
|
|
343
|
+
const fname = block.filename ||
|
|
344
|
+
deriveFilename(block.storageKey, block.mimeType || 'application/octet-stream');
|
|
338
345
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'application/octet-stream');
|
|
339
346
|
if (uploadId) {
|
|
340
|
-
allBlocks.push(block.mimeType === 'application/pdf'
|
|
347
|
+
allBlocks.push(block.mimeType === 'application/pdf'
|
|
348
|
+
? notionPdfUploadBlock(uploadId, block.filename)
|
|
349
|
+
: notionFileUploadBlock(uploadId, block.filename));
|
|
341
350
|
}
|
|
342
351
|
else if (block.mimeType === 'application/pdf') {
|
|
343
352
|
allBlocks.push(notionPdfBlock(block.url, block.filename));
|
|
@@ -358,7 +367,9 @@ async function sectionsToNotionBlocks(sections, options) {
|
|
|
358
367
|
if (block.storageKey && options?.notionClient) {
|
|
359
368
|
const fname = block.filename || deriveFilename(block.storageKey, 'text/csv');
|
|
360
369
|
const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, 'text/csv');
|
|
361
|
-
allBlocks.push(uploadId
|
|
370
|
+
allBlocks.push(uploadId
|
|
371
|
+
? notionFileUploadBlock(uploadId, block.filename || 'data.csv')
|
|
372
|
+
: notionFileBlock(block.url, block.filename || 'data.csv'));
|
|
362
373
|
}
|
|
363
374
|
else {
|
|
364
375
|
allBlocks.push(notionFileBlock(block.url, block.filename || 'data.csv'));
|
|
@@ -453,7 +464,9 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
453
464
|
*/
|
|
454
465
|
async resolveAndAppendReferences(doc, notionPageId) {
|
|
455
466
|
// Extract references from section content (no upload needed for reference pass)
|
|
456
|
-
const { references } = await sectionsToNotionBlocks(doc.sections, {
|
|
467
|
+
const { references } = await sectionsToNotionBlocks(doc.sections, {
|
|
468
|
+
extractReferences: true,
|
|
469
|
+
});
|
|
457
470
|
if (references.length === 0) {
|
|
458
471
|
return { resolved: 0, unresolved: 0 };
|
|
459
472
|
}
|
|
@@ -623,7 +636,10 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
623
636
|
// Notion page creation / update
|
|
624
637
|
// ============================================
|
|
625
638
|
async createNotionPage(doc) {
|
|
626
|
-
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
639
|
+
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
640
|
+
extractReferences: true,
|
|
641
|
+
notionClient: this.client,
|
|
642
|
+
});
|
|
627
643
|
// First batch: up to 100 blocks as children of the new page
|
|
628
644
|
const firstBatch = blocks.slice(0, MAX_BLOCKS_PER_APPEND);
|
|
629
645
|
const remainingBlocks = blocks.slice(MAX_BLOCKS_PER_APPEND);
|
|
@@ -655,7 +671,10 @@ export class NotionExportTarget extends ExportTarget {
|
|
|
655
671
|
},
|
|
656
672
|
});
|
|
657
673
|
await this.clearPageContent(notionPageId);
|
|
658
|
-
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
674
|
+
const { blocks } = await sectionsToNotionBlocks(doc.sections, {
|
|
675
|
+
extractReferences: true,
|
|
676
|
+
notionClient: this.client,
|
|
677
|
+
});
|
|
659
678
|
await this.appendRemainingBlocks(notionPageId, blocks);
|
|
660
679
|
}
|
|
661
680
|
async clearPageContent(pageId) {
|
package/package.json
CHANGED