@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 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: 'select' | 'multi_select';
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: 'select' | 'multi_select';
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 &&
@@ -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 = { created: '\u2713', updated: '\u21bb', skipped: '-', failed: '\u2717' }[result.status];
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') && r.externalId);
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}`);
@@ -129,6 +129,7 @@ export interface NotionPropertySchema {
129
129
  groups: Array<{
130
130
  id: string;
131
131
  name: string;
132
+ color?: string;
132
133
  option_ids: string[];
133
134
  }>;
134
135
  };
@@ -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 is structurally identical to select
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: 'select',
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
- const values = extractSelectValues(propValue, mapped.notionType);
100
- if (values.length > 0) {
101
- tagValues.set(propName, values);
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':
@@ -41,6 +41,7 @@ function makeSchema(unmappedColumns) {
41
41
  name: 'Test DB',
42
42
  description: '',
43
43
  mappedColumns: [],
44
+ scalarColumns: [],
44
45
  unmappedColumns: unmappedColumns.map((name) => ({
45
46
  notionPropertyId: `prop-${name}`,
46
47
  notionPropertyName: name,
@@ -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';
@@ -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
- if (col.type === 'select') {
134
- properties[col.name] = {
135
- type: 'select',
136
- select: { options },
137
- };
138
- }
139
- else if (col.type === 'multi_select') {
140
- properties[col.name] = {
141
- type: 'multi_select',
142
- multi_select: { options },
143
- };
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
- if (propValue.values.length === 0)
220
- continue;
221
- if (propValue.columnType === 'select') {
222
- properties[colName] = {
223
- type: 'select',
224
- select: { name: propValue.values[0].tagName },
225
- };
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 if (propValue.columnType === 'multi_select') {
228
- properties[colName] = {
229
- type: 'multi_select',
230
- multi_select: propValue.values.map((v) => ({ name: v.tagName })),
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);
@@ -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({ object: 'block', type: 'divider', divider: {} });
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 || deriveFilename(block.storageKey, block.mimeType || 'image/png');
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 ? notionImageUploadBlock(uploadId, block.alt) : notionImageBlock(block.url, block.alt));
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 || deriveFilename(block.storageKey, block.mimeType || 'application/octet-stream');
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' ? notionPdfUploadBlock(uploadId, block.filename) : notionFileUploadBlock(uploadId, block.filename));
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 ? notionFileUploadBlock(uploadId, block.filename || 'data.csv') : notionFileBlock(block.url, block.filename || 'data.csv'));
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, { extractReferences: true });
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, { extractReferences: true, notionClient: this.client });
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, { extractReferences: true, notionClient: this.client });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "Migration tool for importing documents into Moxn Knowledge Base from local files, Notion, Google Docs, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",