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
package/src/db/migrate.ts DELETED
@@ -1,182 +0,0 @@
1
- /**
2
- * Schema migration — detects drift between site.config.ts and the D1 database,
3
- * then generates safe ALTER TABLE SQL to bring the DB in sync.
4
- *
5
- * Design:
6
- * - Only ADD COLUMN is safe in SQLite — removals/renames are warned, never applied.
7
- * - Type changes are warned but not applied (SQLite doesn't support ALTER COLUMN).
8
- * - System columns (id, created_at, updated_at, status, publishAt) are ignored.
9
- */
10
- import type { ContentTypeConfig } from '../config/define.ts';
11
- import type { FieldMeta } from '../config/field.ts';
12
-
13
- // ── Types ───────────────────────────────────────────────────────────
14
-
15
- export interface ColumnInfo {
16
- name: string;
17
- type: string;
18
- }
19
-
20
- export interface DriftResult {
21
- table: string;
22
- added: { name: string; sqlType: string; meta: FieldMeta }[];
23
- removed: string[];
24
- typeChanged: { name: string; expected: string; actual: string }[];
25
- }
26
-
27
- export interface MigrationResult {
28
- drift: DriftResult[];
29
- sql: string[];
30
- warnings: string[];
31
- }
32
-
33
- // ── System columns (skip during drift detection) ────────────────────
34
-
35
- const SYSTEM_COLUMNS = new Set([
36
- 'id',
37
- 'created_at',
38
- 'updated_at',
39
- 'status',
40
- 'publishAt'
41
- ]);
42
-
43
- // ── SQL type mapping (same as schema.ts) ────────────────────────────
44
-
45
- function sqlType(fieldType: string): string {
46
- switch (fieldType) {
47
- case 'text':
48
- case 'longText':
49
- case 'richText':
50
- case 'url':
51
- case 'email':
52
- case 'phone':
53
- case 'color':
54
- case 'youtube':
55
- case 'instagram':
56
- case 'image':
57
- case 'images':
58
- case 'reference':
59
- case 'date':
60
- case 'select':
61
- return 'TEXT';
62
- case 'number':
63
- return 'REAL';
64
- case 'boolean':
65
- return 'INTEGER';
66
- case 'references':
67
- return '__JOIN__';
68
- default:
69
- return 'TEXT';
70
- }
71
- }
72
-
73
- // ── Drift detection ─────────────────────────────────────────────────
74
-
75
- export function detectDrift(
76
- contentTypes: ContentTypeConfig[],
77
- existingColumns: Record<string, ColumnInfo[]>
78
- ): MigrationResult {
79
- const drift: DriftResult[] = [];
80
- const sql: string[] = [];
81
- const warnings: string[] = [];
82
-
83
- for (const ct of contentTypes) {
84
- const tableCols = existingColumns[ct.id];
85
- if (!tableCols || tableCols.length === 0) {
86
- // Table doesn't exist — generate CREATE TABLE
87
- const columns: string[] = [
88
- 'id TEXT PRIMARY KEY',
89
- "created_at TEXT DEFAULT (datetime('now'))",
90
- "updated_at TEXT DEFAULT (datetime('now'))",
91
- "status TEXT NOT NULL DEFAULT 'published'",
92
- 'publishAt TEXT'
93
- ];
94
-
95
- for (const [fieldId, meta] of Object.entries(ct.fieldMeta)) {
96
- const st = sqlType(meta.fieldType);
97
- if (st === '__JOIN__') {
98
- // Create join table for `references` fields
99
- const joinTable = `${ct.id}__${fieldId}`;
100
- sql.push(
101
- `CREATE TABLE IF NOT EXISTS ${joinTable} (\n` +
102
- ` source_id TEXT NOT NULL,\n` +
103
- ` target_id TEXT NOT NULL,\n` +
104
- ` sort_order INTEGER NOT NULL DEFAULT 0,\n` +
105
- ` PRIMARY KEY (source_id, target_id)\n` +
106
- `);`
107
- );
108
- sql.push(
109
- `CREATE INDEX IF NOT EXISTS idx_${joinTable}_source ON ${joinTable} (source_id);`
110
- );
111
- continue;
112
- }
113
- const notNull = meta.required ? ' NOT NULL' : '';
114
- columns.push(`${fieldId} ${st}${notNull}`);
115
- }
116
-
117
- sql.push(
118
- `CREATE TABLE IF NOT EXISTS ${ct.id} (\n ${columns.join(',\n ')}\n);`
119
- );
120
- continue;
121
- }
122
-
123
- const existing = new Map(
124
- tableCols.map(c => [c.name, c.type.toUpperCase()])
125
- );
126
- const result: DriftResult = {
127
- table: ct.id,
128
- added: [],
129
- removed: [],
130
- typeChanged: []
131
- };
132
-
133
- // Check for new fields in config not in DB
134
- for (const [fieldId, meta] of Object.entries(ct.fieldMeta)) {
135
- const st = sqlType(meta.fieldType);
136
- if (st === '__JOIN__') continue; // join tables handled separately
137
-
138
- if (!existing.has(fieldId)) {
139
- result.added.push({ name: fieldId, sqlType: st, meta });
140
- const notNull = meta.required ? " NOT NULL DEFAULT ''" : '';
141
- sql.push(`ALTER TABLE ${ct.id} ADD COLUMN ${fieldId} ${st}${notNull};`);
142
- } else {
143
- // Check type match
144
- const actualType = existing.get(fieldId)!;
145
- if (actualType !== st) {
146
- result.typeChanged.push({
147
- name: fieldId,
148
- expected: st,
149
- actual: actualType
150
- });
151
- warnings.push(
152
- `Column '${ct.id}.${fieldId}' type mismatch: config says ${st}, DB has ${actualType}. ` +
153
- `SQLite does not support ALTER COLUMN — manual migration needed.`
154
- );
155
- }
156
- }
157
- }
158
-
159
- // Check for DB columns not in config (removed fields)
160
- for (const [colName] of existing) {
161
- if (SYSTEM_COLUMNS.has(colName)) continue;
162
- const configHasField = colName in ct.fieldMeta;
163
- if (!configHasField) {
164
- result.removed.push(colName);
165
- warnings.push(
166
- `Column '${ct.id}.${colName}' exists in DB but not in config. ` +
167
- `Koguma will NOT drop this column (data safety). Remove it manually if needed.`
168
- );
169
- }
170
- }
171
-
172
- if (
173
- result.added.length ||
174
- result.removed.length ||
175
- result.typeChanged.length
176
- ) {
177
- drift.push(result);
178
- }
179
- }
180
-
181
- return { drift, sql, warnings };
182
- }
package/src/db/schema.ts DELETED
@@ -1,122 +0,0 @@
1
- /**
2
- * Schema generator — reads a Koguma config and produces SQL DDL for D1.
3
- *
4
- * Design:
5
- * - Each content type → one table
6
- * - Scalar fields → columns on that table
7
- * - `field.ref()` → TEXT column (foreign key to the target table's id)
8
- * - `field.refs()` → join table: `{source}_{field}` with (source_id, target_id, sort_order)
9
- * - `field.richText()` → TEXT column storing JSON
10
- * - `field.image()` → TEXT column (FK to assets table id)
11
- * - Every table gets `id TEXT PRIMARY KEY`, `created_at`, `updated_at`
12
- */
13
- import type { ContentTypeConfig } from '../config/define.ts';
14
- import type { FieldMeta, FieldType } from '../config/field.ts';
15
-
16
- // ── SQL type mapping ────────────────────────────────────────────────
17
-
18
- function sqlType(fieldType: FieldType): string {
19
- switch (fieldType) {
20
- case 'text':
21
- case 'longText':
22
- case 'richText':
23
- case 'url':
24
- case 'email':
25
- case 'phone':
26
- case 'color':
27
- case 'youtube':
28
- case 'instagram':
29
- case 'image':
30
- case 'reference':
31
- case 'date':
32
- case 'select':
33
- return 'TEXT';
34
- case 'number':
35
- return 'REAL';
36
- case 'boolean':
37
- return 'INTEGER'; // 0/1
38
- case 'references':
39
- return '__JOIN__';
40
- case 'images':
41
- return 'TEXT'; // JSON array
42
- }
43
- }
44
-
45
- // ── Collect all fields from a content type (flat or grouped) ────────
46
-
47
- function collectFields(ct: ContentTypeConfig): Record<string, FieldMeta> {
48
- return ct.fieldMeta;
49
- }
50
-
51
- // ── Generate SQL ─────────────────────────────────────────────────────
52
-
53
- export interface GeneratedSchema {
54
- /** CREATE TABLE statements */
55
- tables: string[];
56
- /** CREATE INDEX statements */
57
- indexes: string[];
58
- /** All SQL as a single string */
59
- sql: string;
60
- }
61
-
62
- export function generateSchema(
63
- contentTypes: ContentTypeConfig[]
64
- ): GeneratedSchema {
65
- const tables: string[] = [];
66
- const indexes: string[] = [];
67
-
68
- // ── Assets table (always present) ────────────────────────────────
69
- tables.push(`CREATE TABLE IF NOT EXISTS _assets (
70
- id TEXT PRIMARY KEY,
71
- title TEXT NOT NULL DEFAULT '',
72
- description TEXT DEFAULT '',
73
- url TEXT NOT NULL,
74
- content_type TEXT DEFAULT '',
75
- width INTEGER,
76
- height INTEGER,
77
- file_size INTEGER,
78
- created_at TEXT DEFAULT (datetime('now')),
79
- updated_at TEXT DEFAULT (datetime('now'))
80
- );`);
81
-
82
- // ── Content type tables ──────────────────────────────────────────
83
- for (const ct of contentTypes) {
84
- const fields = collectFields(ct);
85
- const columns: string[] = [
86
- 'id TEXT PRIMARY KEY',
87
- "created_at TEXT DEFAULT (datetime('now'))",
88
- "updated_at TEXT DEFAULT (datetime('now'))",
89
- "status TEXT NOT NULL DEFAULT 'published'",
90
- 'publishAt TEXT'
91
- ];
92
-
93
- for (const [fieldId, meta] of Object.entries(fields)) {
94
- const st = sqlType(meta.fieldType);
95
-
96
- if (st === '__JOIN__') {
97
- // Create a join table for `references` fields
98
- const joinTable = `${ct.id}__${fieldId}`;
99
- tables.push(`CREATE TABLE IF NOT EXISTS ${joinTable} (
100
- source_id TEXT NOT NULL,
101
- target_id TEXT NOT NULL,
102
- sort_order INTEGER NOT NULL DEFAULT 0,
103
- PRIMARY KEY (source_id, target_id)
104
- );`);
105
- indexes.push(
106
- `CREATE INDEX IF NOT EXISTS idx_${joinTable}_source ON ${joinTable} (source_id);`
107
- );
108
- continue;
109
- }
110
-
111
- const notNull = meta.required ? ' NOT NULL' : '';
112
- columns.push(`${fieldId} ${st}${notNull}`);
113
- }
114
-
115
- tables.push(
116
- `CREATE TABLE IF NOT EXISTS ${ct.id} (\n ${columns.join(',\n ')}\n);`
117
- );
118
- }
119
-
120
- const sql = [...tables, '', ...indexes].join('\n\n');
121
- return { tables, indexes, sql };
122
- }