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.
- package/README.md +109 -139
- package/cli/auth.ts +101 -0
- package/cli/config.ts +149 -0
- package/cli/constants.ts +38 -0
- package/cli/content.ts +503 -0
- package/cli/dev-sync.ts +305 -0
- package/cli/exec.ts +61 -0
- package/cli/index.ts +779 -1545
- package/cli/log.ts +49 -0
- package/cli/preflight.ts +105 -0
- package/cli/scaffold.ts +680 -0
- package/cli/typegen.ts +190 -0
- package/cli/ui.ts +55 -0
- package/cli/wrangler.ts +367 -0
- package/package.json +7 -4
- package/src/admin/_bundle.ts +1 -1
- package/src/api/router.integration.test.ts +63 -80
- package/src/api/router.ts +85 -59
- package/src/config/define.ts +1 -1
- package/src/config/field.ts +10 -9
- package/src/config/index.ts +1 -13
- package/src/config/meta.ts +7 -7
- package/src/config/types.ts +1 -95
- package/src/db/init.ts +68 -0
- package/src/db/queries.ts +120 -211
- package/src/db/sql.ts +10 -25
- package/src/media/index.ts +105 -47
- package/src/react/Markdown.test.tsx +195 -0
- package/src/react/Markdown.tsx +40 -0
- package/src/react/index.ts +6 -22
- package/src/react/types.ts +3 -112
- package/src/db/migrate.ts +0 -182
- package/src/db/schema.ts +0 -122
- package/src/react/RichText.test.tsx +0 -535
- package/src/react/RichText.tsx +0 -350
- package/src/rich-text/index.ts +0 -4
- package/src/rich-text/koguma-to-lexical.ts +0 -340
- package/src/rich-text/lexical-compat.test.ts +0 -513
- package/src/rich-text/lexical-to-koguma.test.ts +0 -906
- package/src/rich-text/lexical-to-koguma.ts +0 -400
- package/src/rich-text/markdown-to-koguma.ts +0 -164
- package/src/rich-text/plain.test.ts +0 -208
- package/src/rich-text/plain.ts +0 -114
- 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
|
-
}
|