koguma 0.6.6 → 2.0.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.
Files changed (44) hide show
  1. package/README.md +109 -139
  2. package/cli/auth.ts +101 -0
  3. package/cli/config.ts +149 -0
  4. package/cli/constants.ts +38 -0
  5. package/cli/content.ts +503 -0
  6. package/cli/dev-sync.ts +305 -0
  7. package/cli/exec.ts +61 -0
  8. package/cli/index.ts +779 -1545
  9. package/cli/log.ts +49 -0
  10. package/cli/preflight.ts +105 -0
  11. package/cli/scaffold.ts +680 -0
  12. package/cli/typegen.ts +190 -0
  13. package/cli/ui.ts +55 -0
  14. package/cli/wrangler.ts +367 -0
  15. package/package.json +7 -4
  16. package/src/admin/_bundle.ts +1 -1
  17. package/src/api/router.integration.test.ts +63 -80
  18. package/src/api/router.ts +85 -59
  19. package/src/config/define.ts +1 -1
  20. package/src/config/field.ts +10 -9
  21. package/src/config/index.ts +1 -13
  22. package/src/config/meta.ts +7 -7
  23. package/src/config/types.ts +1 -95
  24. package/src/db/init.ts +68 -0
  25. package/src/db/queries.ts +120 -211
  26. package/src/db/sql.ts +10 -25
  27. package/src/media/index.ts +105 -47
  28. package/src/react/Markdown.test.tsx +195 -0
  29. package/src/react/Markdown.tsx +40 -0
  30. package/src/react/index.ts +6 -22
  31. package/src/react/types.ts +3 -112
  32. package/src/db/migrate.ts +0 -182
  33. package/src/db/schema.ts +0 -122
  34. package/src/react/RichText.test.tsx +0 -535
  35. package/src/react/RichText.tsx +0 -350
  36. package/src/rich-text/index.ts +0 -4
  37. package/src/rich-text/koguma-to-lexical.ts +0 -340
  38. package/src/rich-text/lexical-compat.test.ts +0 -513
  39. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  40. package/src/rich-text/lexical-to-koguma.ts +0 -400
  41. package/src/rich-text/markdown-to-koguma.ts +0 -164
  42. package/src/rich-text/plain.test.ts +0 -208
  43. package/src/rich-text/plain.ts +0 -114
  44. package/src/rich-text/snapshots.test.ts +0 -284
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Core CMS types used across Koguma.
3
- * These are the runtime types for assets, rich text, and references.
3
+ * These are the runtime types for assets and references.
4
4
  */
5
5
 
6
6
  /** A media asset stored in R2 */
@@ -13,100 +13,6 @@ export interface KogumaAsset {
13
13
  height?: number;
14
14
  }
15
15
 
16
- // ── Rich Text ─────────────────────────────────────────────────────────
17
-
18
- /**
19
- * A koguma-normalized rich text document.
20
- * Editor-agnostic — does not expose Lexical internals.
21
- * Produced server-side from Lexical's SerializedEditorState.
22
- */
23
- export interface KogumaDocument {
24
- nodes: KogumaBlockNode[];
25
- }
26
-
27
- export type KogumaBlockNode =
28
- | {
29
- type: 'paragraph';
30
- key?: string;
31
- align?: 'left' | 'center' | 'right' | 'justify';
32
- children: KogumaInlineNode[];
33
- }
34
- | {
35
- type: 'heading';
36
- key?: string;
37
- level: 1 | 2 | 3 | 4 | 5 | 6;
38
- children: KogumaInlineNode[];
39
- }
40
- | { type: 'list'; key?: string; ordered: boolean; items: KogumaListItem[] }
41
- | { type: 'quote'; key?: string; children: KogumaInlineNode[] }
42
- | { type: 'code'; key?: string; language?: string; text: string }
43
- | {
44
- type: 'image';
45
- key?: string;
46
- url: string;
47
- alt?: string;
48
- width?: number;
49
- height?: number;
50
- }
51
- | { type: 'hr'; key?: string }
52
- | { type: 'table'; key?: string; rows: KogumaTableRow[] }
53
- | { type: 'layout'; key?: string; columns: KogumaBlockNode[][] }
54
- | {
55
- type: 'custom';
56
- key?: string;
57
- name: string;
58
- data: Record<string, unknown>;
59
- };
60
-
61
- /** A list item — supports nesting and checklists */
62
- export interface KogumaListItem {
63
- key?: string;
64
- children: KogumaInlineNode[];
65
- /** Defined = checklist item. true = checked, false = unchecked */
66
- checked?: boolean;
67
- nestedList?: { ordered: boolean; items: KogumaListItem[] };
68
- }
69
-
70
- export interface KogumaTableRow {
71
- key?: string;
72
- isHeader?: boolean;
73
- cells: KogumaTableCell[];
74
- }
75
-
76
- export interface KogumaTableCell {
77
- key?: string;
78
- children: KogumaInlineNode[];
79
- }
80
-
81
- export type KogumaInlineNode =
82
- | {
83
- type: 'text';
84
- key?: string;
85
- text: string;
86
- bold?: boolean;
87
- italic?: boolean;
88
- underline?: boolean;
89
- code?: boolean;
90
- strikethrough?: boolean;
91
- superscript?: boolean;
92
- subscript?: boolean;
93
- }
94
- | {
95
- type: 'link';
96
- key?: string;
97
- url: string;
98
- newTab?: boolean;
99
- children: KogumaInlineNode[];
100
- }
101
- | { type: 'inline-image'; key?: string; url: string; alt?: string }
102
- | { type: 'line-break'; key?: string }
103
- | {
104
- type: 'custom';
105
- key?: string;
106
- name: string;
107
- data: Record<string, unknown>;
108
- };
109
-
110
16
  /** Reference to another entry */
111
17
  export interface EntryReference<T = Record<string, unknown>> {
112
18
  id: string;
package/src/db/init.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Database initialization — hardcoded 2-table schema for the JSON document store.
3
+ *
4
+ * Two tables, forever. No migrations.
5
+ *
6
+ * entries — all content, keyed by content_type. Field data stored as JSON blob.
7
+ * assets — media metadata as JSON blob. Files live in R2.
8
+ */
9
+
10
+ // ── Schema SQL ──────────────────────────────────────────────────────
11
+
12
+ export const INIT_SQL = `
13
+ CREATE TABLE IF NOT EXISTS entries (
14
+ id TEXT PRIMARY KEY,
15
+ content_type TEXT NOT NULL,
16
+ slug TEXT,
17
+ data TEXT NOT NULL,
18
+ status TEXT NOT NULL DEFAULT 'draft',
19
+ publish_at TEXT,
20
+ created_at TEXT DEFAULT (datetime('now')),
21
+ updated_at TEXT DEFAULT (datetime('now'))
22
+ );
23
+ CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(content_type);
24
+ CREATE INDEX IF NOT EXISTS idx_entries_slug ON entries(content_type, slug);
25
+
26
+ CREATE TABLE IF NOT EXISTS assets (
27
+ id TEXT PRIMARY KEY,
28
+ data TEXT NOT NULL,
29
+ created_at TEXT DEFAULT (datetime('now')),
30
+ updated_at TEXT DEFAULT (datetime('now'))
31
+ );
32
+ `.trim();
33
+
34
+ // ── D1 database interface ───────────────────────────────────────────
35
+
36
+ interface D1Database {
37
+ prepare(query: string): D1PreparedStatement;
38
+ batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
39
+ exec(sql: string): Promise<D1ExecResult>;
40
+ }
41
+
42
+ interface D1PreparedStatement {
43
+ bind(...values: unknown[]): D1PreparedStatement;
44
+ first<T = Record<string, unknown>>(): Promise<T | null>;
45
+ all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
46
+ run(): Promise<D1Result>;
47
+ }
48
+
49
+ interface D1Result<T = unknown> {
50
+ results?: T[];
51
+ success: boolean;
52
+ meta: Record<string, unknown>;
53
+ }
54
+
55
+ interface D1ExecResult {
56
+ count: number;
57
+ duration: number;
58
+ }
59
+
60
+ // ── Init function ───────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Initialize the database with the 2-table schema.
64
+ * Safe to call multiple times (uses IF NOT EXISTS).
65
+ */
66
+ export async function initDatabase(db: D1Database): Promise<void> {
67
+ await db.exec(INIT_SQL);
68
+ }
package/src/db/queries.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Generic CRUD queries parameterized by the content type config.
2
+ * Generic CRUD queries for the JSON document store.
3
3
  *
4
- * These work with any D1 database generated by schema.ts.
5
- * All queries use parameterized SQL to prevent injection.
4
+ * All content is stored in a single `entries` table with a `data` JSON blob.
5
+ * No per-content-type tables, no join tables, no schema generation.
6
6
  */
7
- import type { ContentTypeConfig } from '../config/define.ts';
8
- import type { FieldMeta } from '../config/field.ts';
9
7
 
10
8
  // ── D1 database interface (Cloudflare Workers binding) ──────────────
11
9
 
@@ -29,271 +27,182 @@ interface D1Result<T = unknown> {
29
27
 
30
28
  // ── Helpers ──────────────────────────────────────────────────────────
31
29
 
32
- function scalarFields(ct: ContentTypeConfig): [string, FieldMeta][] {
33
- return Object.entries(ct.fieldMeta).filter(
34
- ([_, meta]) => meta.fieldType !== 'references'
35
- );
30
+ /** Unpack a DB row into a flat entry: system fields + spread data blob. */
31
+ function unpackRow(row: Record<string, unknown>): Record<string, unknown> {
32
+ const {
33
+ id,
34
+ content_type,
35
+ slug,
36
+ data,
37
+ status,
38
+ publish_at,
39
+ created_at,
40
+ updated_at,
41
+ ...rest
42
+ } = row;
43
+ const parsed = typeof data === 'string' ? JSON.parse(data) : (data ?? {});
44
+ return {
45
+ id,
46
+ content_type,
47
+ slug,
48
+ status,
49
+ publish_at,
50
+ created_at,
51
+ updated_at,
52
+ ...parsed,
53
+ ...rest
54
+ };
36
55
  }
37
56
 
38
- function refArrayFields(ct: ContentTypeConfig): [string, FieldMeta][] {
39
- return Object.entries(ct.fieldMeta).filter(
40
- ([_, meta]) => meta.fieldType === 'references'
41
- );
57
+ /** Pack a flat entry into system columns + data JSON blob. */
58
+ function packData(data: Record<string, unknown>): string {
59
+ // Strip system fields they live in columns, not the blob
60
+ const {
61
+ id,
62
+ content_type,
63
+ slug,
64
+ status,
65
+ publish_at,
66
+ publishAt,
67
+ created_at,
68
+ updated_at,
69
+ ...fields
70
+ } = data;
71
+ return JSON.stringify(fields);
42
72
  }
43
73
 
44
74
  // ── GET ONE ─────────────────────────────────────────────────────────
45
75
 
46
76
  export async function getEntry(
47
77
  db: D1Database,
48
- ct: ContentTypeConfig,
78
+ contentType: string,
49
79
  id: string
50
80
  ): Promise<Record<string, unknown> | null> {
51
81
  const row = await db
52
- .prepare(`SELECT * FROM ${ct.id} WHERE id = ?`)
53
- .bind(id)
82
+ .prepare('SELECT * FROM entries WHERE content_type = ? AND id = ?')
83
+ .bind(contentType, id)
54
84
  .first();
55
85
 
56
86
  if (!row) return null;
57
-
58
- // Parse JSON fields (richText and images stored as JSON strings)
59
- const entry = { ...row } as Record<string, unknown>;
60
- for (const [fieldId, meta] of scalarFields(ct)) {
61
- if (
62
- (meta.fieldType === 'richText' || meta.fieldType === 'images') &&
63
- typeof entry[fieldId] === 'string'
64
- ) {
65
- try {
66
- entry[fieldId] = JSON.parse(entry[fieldId] as string);
67
- } catch {
68
- // leave as-is if not valid JSON
69
- }
70
- }
71
- }
72
-
73
- // Load references arrays
74
- for (const [fieldId] of refArrayFields(ct)) {
75
- const joinTable = `${ct.id}__${fieldId}`;
76
- const refs = await db
77
- .prepare(
78
- `SELECT target_id FROM ${joinTable} WHERE source_id = ? ORDER BY sort_order`
79
- )
80
- .bind(id)
81
- .all();
82
- entry[fieldId] =
83
- refs.results?.map((r: Record<string, unknown>) => r.target_id) ?? [];
84
- }
85
-
86
- return entry;
87
+ return unpackRow(row);
87
88
  }
88
89
 
89
90
  // ── GET ALL ─────────────────────────────────────────────────────────
90
91
 
91
92
  export async function getEntries(
92
93
  db: D1Database,
93
- ct: ContentTypeConfig,
94
+ contentType: string,
94
95
  opts?: { publishedOnly?: boolean }
95
96
  ): Promise<Record<string, unknown>[]> {
96
- const whereClause = opts?.publishedOnly
97
- ? " WHERE status = 'published' AND (publishAt IS NULL OR publishAt <= datetime('now'))"
98
- : '';
99
- const result = await db
100
- .prepare(`SELECT * FROM ${ct.id}${whereClause} ORDER BY created_at DESC`)
101
- .all();
102
-
103
- const rows = (result.results ?? []) as Record<string, unknown>[];
104
-
105
- // Parse JSON + load refs for each row
106
- const entries: Record<string, unknown>[] = [];
107
- for (const row of rows) {
108
- const entry = { ...row };
109
- for (const [fieldId, meta] of scalarFields(ct)) {
110
- if (
111
- (meta.fieldType === 'richText' || meta.fieldType === 'images') &&
112
- typeof entry[fieldId] === 'string'
113
- ) {
114
- try {
115
- entry[fieldId] = JSON.parse(entry[fieldId] as string);
116
- } catch {
117
- /* skip */
118
- }
119
- }
120
- }
121
-
122
- for (const [fieldId] of refArrayFields(ct)) {
123
- const joinTable = `${ct.id}__${fieldId}`;
124
- const refs = await db
125
- .prepare(
126
- `SELECT target_id FROM ${joinTable} WHERE source_id = ? ORDER BY sort_order`
127
- )
128
- .bind(entry.id)
129
- .all();
130
- entry[fieldId] =
131
- refs.results?.map((r: Record<string, unknown>) => r.target_id) ?? [];
132
- }
133
-
134
- entries.push(entry);
97
+ let query = 'SELECT * FROM entries WHERE content_type = ?';
98
+ if (opts?.publishedOnly) {
99
+ query +=
100
+ " AND status = 'published' AND (publish_at IS NULL OR publish_at <= datetime('now'))";
135
101
  }
102
+ query += ' ORDER BY created_at DESC';
136
103
 
137
- return entries;
104
+ const result = await db.prepare(query).bind(contentType).all();
105
+ return (result.results ?? []).map(unpackRow);
138
106
  }
139
107
 
140
108
  // ── CREATE ──────────────────────────────────────────────────────────
141
109
 
142
110
  export async function createEntry(
143
111
  db: D1Database,
144
- ct: ContentTypeConfig,
112
+ contentType: string,
145
113
  data: Record<string, unknown>
146
114
  ): Promise<Record<string, unknown>> {
147
115
  const id = (data.id as string) ?? crypto.randomUUID();
148
- const fields = scalarFields(ct);
149
- const refFields = refArrayFields(ct);
150
-
151
- const columns = ['id', 'status', 'publishAt', ...fields.map(([f]) => f)];
152
- const placeholders = columns.map(() => '?').join(', ');
153
- const values = [
154
- id,
155
- (data.status as string) ?? 'draft',
156
- (data.publishAt as string) ?? null,
157
- ...fields.map(([f, meta]) => {
158
- const val = data[f];
159
- if (
160
- (meta.fieldType === 'richText' || meta.fieldType === 'images') &&
161
- val != null &&
162
- typeof val === 'object'
163
- ) {
164
- return JSON.stringify(val);
165
- }
166
- return val ?? null;
167
- })
168
- ];
169
-
170
- const statements: D1PreparedStatement[] = [
171
- db
172
- .prepare(
173
- `INSERT INTO ${ct.id} (${columns.join(', ')}) VALUES (${placeholders})`
174
- )
175
- .bind(...values)
176
- ];
177
-
178
- // Insert join table entries for refs
179
- for (const [fieldId] of refFields) {
180
- const refs = data[fieldId] as string[] | undefined;
181
- if (refs?.length) {
182
- const joinTable = `${ct.id}__${fieldId}`;
183
- for (let i = 0; i < refs.length; i++) {
184
- statements.push(
185
- db
186
- .prepare(
187
- `INSERT INTO ${joinTable} (source_id, target_id, sort_order) VALUES (?, ?, ?)`
188
- )
189
- .bind(id, refs[i], i)
190
- );
191
- }
192
- }
193
- }
194
-
195
- await db.batch(statements);
196
- return (await getEntry(db, ct, id))!;
116
+ const slug = (data.slug as string) ?? null;
117
+ const status = (data.status as string) ?? 'draft';
118
+ const publishAt =
119
+ (data.publishAt as string) ?? (data.publish_at as string) ?? null;
120
+ const blob = packData(data);
121
+
122
+ await db
123
+ .prepare(
124
+ `INSERT INTO entries (id, content_type, slug, data, status, publish_at) VALUES (?, ?, ?, ?, ?, ?)`
125
+ )
126
+ .bind(id, contentType, slug, blob, status, publishAt)
127
+ .run();
128
+
129
+ return (await getEntry(db, contentType, id))!;
197
130
  }
198
131
 
199
132
  // ── UPDATE ──────────────────────────────────────────────────────────
200
133
 
201
134
  export async function updateEntry(
202
135
  db: D1Database,
203
- ct: ContentTypeConfig,
136
+ contentType: string,
204
137
  id: string,
205
138
  data: Record<string, unknown>
206
139
  ): Promise<Record<string, unknown> | null> {
207
- const fields = scalarFields(ct);
208
- const refFields = refArrayFields(ct);
140
+ // Read existing entry to merge data blobs
141
+ const existing = await db
142
+ .prepare('SELECT * FROM entries WHERE content_type = ? AND id = ?')
143
+ .bind(contentType, id)
144
+ .first();
209
145
 
210
- // Only update fields that are present in data
211
- const updates: string[] = ["updated_at = datetime('now')"];
212
- const values: unknown[] = [];
146
+ if (!existing) return null;
147
+
148
+ const existingData =
149
+ typeof existing.data === 'string'
150
+ ? JSON.parse(existing.data as string)
151
+ : {};
152
+
153
+ // Merge new fields into existing data blob
154
+ const {
155
+ id: _,
156
+ content_type: __,
157
+ slug: newSlug,
158
+ status: newStatus,
159
+ publishAt,
160
+ publish_at,
161
+ created_at,
162
+ updated_at,
163
+ ...newFields
164
+ } = data;
165
+ const mergedBlob = JSON.stringify({ ...existingData, ...newFields });
166
+
167
+ // Build SET clause for system columns that were provided
168
+ const sets: string[] = ['data = ?', "updated_at = datetime('now')"];
169
+ const values: unknown[] = [mergedBlob];
213
170
 
214
- // Handle status updates
215
171
  if ('status' in data) {
216
- updates.push('status = ?');
172
+ sets.push('status = ?');
217
173
  values.push(data.status);
218
174
  }
219
-
220
- // Handle publishAt updates
221
- if ('publishAt' in data) {
222
- updates.push('publishAt = ?');
223
- values.push(data.publishAt);
175
+ if ('publishAt' in data || 'publish_at' in data) {
176
+ sets.push('publish_at = ?');
177
+ values.push(publishAt ?? publish_at ?? null);
224
178
  }
225
-
226
- for (const [fieldId, meta] of fields) {
227
- if (fieldId in data) {
228
- updates.push(`${fieldId} = ?`);
229
- const val = data[fieldId];
230
- if (
231
- (meta.fieldType === 'richText' || meta.fieldType === 'images') &&
232
- val != null &&
233
- typeof val === 'object'
234
- ) {
235
- values.push(JSON.stringify(val));
236
- } else {
237
- values.push(val ?? null);
238
- }
239
- }
179
+ if ('slug' in data) {
180
+ sets.push('slug = ?');
181
+ values.push(data.slug);
240
182
  }
241
183
 
242
- values.push(id);
184
+ values.push(contentType, id);
243
185
 
244
- const statements: D1PreparedStatement[] = [
245
- db
246
- .prepare(`UPDATE ${ct.id} SET ${updates.join(', ')} WHERE id = ?`)
247
- .bind(...values)
248
- ];
186
+ await db
187
+ .prepare(
188
+ `UPDATE entries SET ${sets.join(', ')} WHERE content_type = ? AND id = ?`
189
+ )
190
+ .bind(...values)
191
+ .run();
249
192
 
250
- // Replace ref arrays (delete + re-insert)
251
- for (const [fieldId] of refFields) {
252
- if (fieldId in data) {
253
- const joinTable = `${ct.id}__${fieldId}`;
254
- statements.push(
255
- db.prepare(`DELETE FROM ${joinTable} WHERE source_id = ?`).bind(id)
256
- );
257
- const refs = data[fieldId] as string[] | undefined;
258
- if (refs?.length) {
259
- for (let i = 0; i < refs.length; i++) {
260
- statements.push(
261
- db
262
- .prepare(
263
- `INSERT INTO ${joinTable} (source_id, target_id, sort_order) VALUES (?, ?, ?)`
264
- )
265
- .bind(id, refs[i], i)
266
- );
267
- }
268
- }
269
- }
270
- }
271
-
272
- await db.batch(statements);
273
- return getEntry(db, ct, id);
193
+ return getEntry(db, contentType, id);
274
194
  }
275
195
 
276
196
  // ── DELETE ───────────────────────────────────────────────────────────
277
197
 
278
198
  export async function deleteEntry(
279
199
  db: D1Database,
280
- ct: ContentTypeConfig,
200
+ contentType: string,
281
201
  id: string
282
202
  ): Promise<boolean> {
283
- const refFields = refArrayFields(ct);
284
-
285
- const statements: D1PreparedStatement[] = [
286
- db.prepare(`DELETE FROM ${ct.id} WHERE id = ?`).bind(id)
287
- ];
288
-
289
- // Clean up join tables
290
- for (const [fieldId] of refFields) {
291
- const joinTable = `${ct.id}__${fieldId}`;
292
- statements.push(
293
- db.prepare(`DELETE FROM ${joinTable} WHERE source_id = ?`).bind(id)
294
- );
295
- }
296
-
297
- await db.batch(statements);
203
+ await db
204
+ .prepare('DELETE FROM entries WHERE content_type = ? AND id = ?')
205
+ .bind(contentType, id)
206
+ .run();
298
207
  return true;
299
208
  }
package/src/db/sql.ts CHANGED
@@ -50,7 +50,7 @@ export interface AssetIndex {
50
50
  }
51
51
 
52
52
  /**
53
- * Build an asset index from raw _assets rows.
53
+ * Build an asset index from raw assets rows.
54
54
  * Maps both full title and title-without-extension to asset IDs.
55
55
  */
56
56
  export function buildAssetIndex(
@@ -90,18 +90,15 @@ export interface FieldMeta {
90
90
  }
91
91
 
92
92
  /**
93
- * Process a seed entry by resolving image references and converting
94
- * markdown strings in richText fields to KogumaDocument JSON.
93
+ * Process a seed entry by resolving image references.
94
+ * Markdown fields are left as-is (already markdown strings).
95
95
  *
96
96
  * Returns a new processed entry (does not mutate the input).
97
97
  */
98
98
  export function processSeedEntry(
99
99
  entry: Record<string, unknown>,
100
100
  fieldMeta: Record<string, FieldMeta>,
101
- assetIndex: AssetIndex,
102
- markdownToKoguma: (md: string) => { nodes: unknown[] },
103
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
- kogumaToLexical: (doc: any) => Record<string, unknown>
101
+ assetIndex: AssetIndex
105
102
  ): { processed: Record<string, unknown>; resolutions: string[] } {
106
103
  const processed: Record<string, unknown> = {
107
104
  id: (entry.id as string) ?? crypto.randomUUID(),
@@ -121,11 +118,6 @@ export function processSeedEntry(
121
118
  processed[fieldId] = result.id;
122
119
  resolutions.push(`${fieldId}: "${value}" → ${result.id}`);
123
120
  }
124
- } else if (meta.fieldType === 'richText' && typeof value === 'string') {
125
- const doc = markdownToKoguma(value);
126
- const lexical = kogumaToLexical(doc);
127
- processed[fieldId] = JSON.stringify(lexical);
128
- resolutions.push(`${fieldId}: markdown → Lexical JSON`);
129
121
  } else if (meta.fieldType === 'images' && Array.isArray(value)) {
130
122
  const ids = value.map((v: unknown) => {
131
123
  if (typeof v !== 'string') return v;
@@ -136,8 +128,9 @@ export function processSeedEntry(
136
128
  }
137
129
  return v;
138
130
  });
139
- processed[fieldId] = JSON.stringify(ids);
131
+ processed[fieldId] = ids;
140
132
  }
133
+ // markdown: left as-is (markdown string) — no conversion needed
141
134
  }
142
135
 
143
136
  return { processed, resolutions };
@@ -147,23 +140,15 @@ export function processSeedEntry(
147
140
 
148
141
  /**
149
142
  * Generate all INSERT OR REPLACE statements for importing a set of entries
150
- * into a table, including join table rows.
143
+ * into a table.
151
144
  */
152
145
  export function buildImportSql(
153
- typeId: string,
154
- entries: Record<string, unknown>[],
155
- joinTables?: Record<string, Record<string, unknown>[]>
146
+ table: string,
147
+ entries: Record<string, unknown>[]
156
148
  ): string[] {
157
149
  const statements: string[] = [];
158
150
  for (const entry of entries) {
159
- statements.push(buildInsertSql(typeId, entry));
160
- }
161
- if (joinTables) {
162
- for (const [jtName, rows] of Object.entries(joinTables)) {
163
- for (const row of rows) {
164
- statements.push(buildInsertSql(jtName, row));
165
- }
166
- }
151
+ statements.push(buildInsertSql(table, entry));
167
152
  }
168
153
  return statements;
169
154
  }