cosmolo 0.3.9 → 0.5.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 +161 -5
- package/dist/cli/index.js +9 -1
- package/dist/cli/init.js +173 -21
- package/dist/cli/migrate/drizzle-setup.d.ts +2 -0
- package/dist/cli/migrate/drizzle-setup.js +411 -0
- package/dist/cli/migrate/schema.d.ts +12 -0
- package/dist/cli/migrate/schema.js +56 -0
- package/dist/cli/migrate/sql-execute.d.ts +2 -0
- package/dist/cli/migrate/sql-execute.js +67 -0
- package/dist/cli/migrate/sql-export.d.ts +4 -0
- package/dist/cli/migrate/sql-export.js +76 -0
- package/dist/cli/migrate.d.ts +1 -0
- package/dist/cli/migrate.js +54 -0
- package/dist/cli/setup/r2.d.ts +1 -0
- package/dist/cli/setup/r2.js +90 -0
- package/dist/markdown.js +7 -3
- package/dist/types.d.ts +0 -3
- package/package.json +2 -2
- package/templates/shared/config/site.json +1 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { ARTICLE_COLUMNS, CATEGORY_COLUMNS } from './schema.js';
|
|
6
|
+
// ─── readline helpers ─────────────────────────────────────────────────────────
|
|
7
|
+
function ask(rl, question, fallback = '') {
|
|
8
|
+
const hint = fallback ? ` [${fallback}]` : '';
|
|
9
|
+
return new Promise((resolve) => rl.question(` ${question}${hint}: `, (ans) => resolve(ans.trim() || fallback)));
|
|
10
|
+
}
|
|
11
|
+
function confirm(rl, question) {
|
|
12
|
+
return new Promise((resolve) => rl.question(` ${question} [y/N] `, (ans) => resolve(ans.trim().toLowerCase() === 'y')));
|
|
13
|
+
}
|
|
14
|
+
// ─── Drizzle schema generation ────────────────────────────────────────────────
|
|
15
|
+
function drizzleColumnDef(col) {
|
|
16
|
+
const fn = col.type === 'TEXT' ? 'text' : 'integer';
|
|
17
|
+
let chain = ` ${col.name}: ${fn}('${col.name}')`;
|
|
18
|
+
if (col.primaryKey) {
|
|
19
|
+
chain += '.primaryKey()';
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (col.notNull)
|
|
23
|
+
chain += '.notNull()';
|
|
24
|
+
if (col.defaultValue !== undefined) {
|
|
25
|
+
const val = typeof col.defaultValue === 'string' ? `'${col.defaultValue}'` : col.defaultValue;
|
|
26
|
+
chain += `.default(${val})`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return chain + ',';
|
|
30
|
+
}
|
|
31
|
+
function generateDrizzleSchema() {
|
|
32
|
+
const catCols = CATEGORY_COLUMNS.map(drizzleColumnDef).join('\n');
|
|
33
|
+
const artCols = ARTICLE_COLUMNS.map(drizzleColumnDef).join('\n');
|
|
34
|
+
return (`import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';\n\n` +
|
|
35
|
+
`export const categories = sqliteTable('categories', {\n${catCols}\n});\n\n` +
|
|
36
|
+
`export const articles = sqliteTable('articles', {\n${artCols}\n});\n`);
|
|
37
|
+
}
|
|
38
|
+
// ─── CRUD file generation ─────────────────────────────────────────────────────
|
|
39
|
+
function generateArticlesCrud() {
|
|
40
|
+
return `import { drizzle } from 'drizzle-orm/d1';
|
|
41
|
+
import { and, desc, eq, sql } from 'drizzle-orm';
|
|
42
|
+
import { articles } from '../../drizzle/schema';
|
|
43
|
+
|
|
44
|
+
export function createDb(d1: D1Database) {
|
|
45
|
+
return drizzle(d1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseArticle<T extends { tags: string | null; related: string | null }>(row: T) {
|
|
49
|
+
return {
|
|
50
|
+
...row,
|
|
51
|
+
tags: JSON.parse(row.tags ?? '[]') as string[],
|
|
52
|
+
related: JSON.parse(row.related ?? '[]') as string[],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getArticles(d1: D1Database) {
|
|
57
|
+
return createDb(d1)
|
|
58
|
+
.select()
|
|
59
|
+
.from(articles)
|
|
60
|
+
.where(eq(articles.draft, 0))
|
|
61
|
+
.orderBy(desc(articles.sort));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function getArticlesByCategory(d1: D1Database, category: string) {
|
|
65
|
+
return createDb(d1)
|
|
66
|
+
.select()
|
|
67
|
+
.from(articles)
|
|
68
|
+
.where(and(eq(articles.draft, 0), eq(articles.category, category)))
|
|
69
|
+
.orderBy(desc(articles.sort));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getArticlesByTag(d1: D1Database, tag: string) {
|
|
73
|
+
return createDb(d1)
|
|
74
|
+
.select()
|
|
75
|
+
.from(articles)
|
|
76
|
+
.where(
|
|
77
|
+
and(
|
|
78
|
+
eq(articles.draft, 0),
|
|
79
|
+
sql\`EXISTS (SELECT 1 FROM json_each(\${articles.tags}) WHERE value = \${tag})\`
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.orderBy(desc(articles.sort));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getArticle(d1: D1Database, slug: string) {
|
|
86
|
+
const [row] = await createDb(d1).select().from(articles).where(eq(articles.slug, slug));
|
|
87
|
+
return row ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function createArticle(d1: D1Database, data: typeof articles.$inferInsert) {
|
|
91
|
+
return createDb(d1).insert(articles).values(data).returning();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function updateArticle(
|
|
95
|
+
d1: D1Database,
|
|
96
|
+
slug: string,
|
|
97
|
+
data: Partial<typeof articles.$inferInsert>
|
|
98
|
+
) {
|
|
99
|
+
return createDb(d1).update(articles).set(data).where(eq(articles.slug, slug)).returning();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function deleteArticle(d1: D1Database, slug: string) {
|
|
103
|
+
return createDb(d1).delete(articles).where(eq(articles.slug, slug));
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function generateCategoriesCrud() {
|
|
108
|
+
return `import { drizzle } from 'drizzle-orm/d1';
|
|
109
|
+
import { eq } from 'drizzle-orm';
|
|
110
|
+
import { categories } from '../../drizzle/schema';
|
|
111
|
+
|
|
112
|
+
export function createDb(d1: D1Database) {
|
|
113
|
+
return drizzle(d1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function getCategories(d1: D1Database) {
|
|
117
|
+
return createDb(d1).select().from(categories);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function getCategory(d1: D1Database, key: string) {
|
|
121
|
+
const [row] = await createDb(d1).select().from(categories).where(eq(categories.key, key));
|
|
122
|
+
return row ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createCategory(d1: D1Database, data: typeof categories.$inferInsert) {
|
|
126
|
+
return createDb(d1).insert(categories).values(data).returning();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function updateCategory(
|
|
130
|
+
d1: D1Database,
|
|
131
|
+
key: string,
|
|
132
|
+
data: Partial<typeof categories.$inferInsert>
|
|
133
|
+
) {
|
|
134
|
+
return createDb(d1).update(categories).set(data).where(eq(categories.key, key)).returning();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function deleteCategory(d1: D1Database, key: string) {
|
|
138
|
+
return createDb(d1).delete(categories).where(eq(categories.key, key));
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
// ─── Route file generation ────────────────────────────────────────────────────
|
|
143
|
+
function generateHomeRoute() {
|
|
144
|
+
return `import type { PageServerLoad } from './$types';
|
|
145
|
+
import { getArticles, parseArticle } from '$lib/db/articles';
|
|
146
|
+
import { getCategories } from '$lib/db/categories';
|
|
147
|
+
import siteConfig from '../../config/site.json';
|
|
148
|
+
|
|
149
|
+
export const load: PageServerLoad = async ({ platform }) => {
|
|
150
|
+
const db = platform!.env.DB;
|
|
151
|
+
const [rawArticles, categories] = await Promise.all([getArticles(db), getCategories(db)]);
|
|
152
|
+
return {
|
|
153
|
+
articles: rawArticles.map(parseArticle),
|
|
154
|
+
categories,
|
|
155
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
function generateArticleRoute() {
|
|
161
|
+
return `import type { PageServerLoad } from './$types';
|
|
162
|
+
import { error } from '@sveltejs/kit';
|
|
163
|
+
import { marked } from 'marked';
|
|
164
|
+
import { getArticle, parseArticle } from '$lib/db/articles';
|
|
165
|
+
import { getCategories } from '$lib/db/categories';
|
|
166
|
+
|
|
167
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
168
|
+
const db = platform!.env.DB;
|
|
169
|
+
const [raw, categories] = await Promise.all([getArticle(db, params.slug), getCategories(db)]);
|
|
170
|
+
if (!raw) error(404, 'Article not found');
|
|
171
|
+
const article = {
|
|
172
|
+
...parseArticle(raw),
|
|
173
|
+
body: await marked(raw.body ?? ''),
|
|
174
|
+
};
|
|
175
|
+
return { article, categories };
|
|
176
|
+
};
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
function generateCategoryRoute() {
|
|
180
|
+
return `import type { PageServerLoad } from './$types';
|
|
181
|
+
import { error } from '@sveltejs/kit';
|
|
182
|
+
import { getArticlesByCategory, parseArticle } from '$lib/db/articles';
|
|
183
|
+
import { getCategory } from '$lib/db/categories';
|
|
184
|
+
import siteConfig from '../../../../config/site.json';
|
|
185
|
+
|
|
186
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
187
|
+
const db = platform!.env.DB;
|
|
188
|
+
const [rawArticles, category] = await Promise.all([
|
|
189
|
+
getArticlesByCategory(db, params.slug),
|
|
190
|
+
getCategory(db, params.slug),
|
|
191
|
+
]);
|
|
192
|
+
if (!category) error(404, 'Category not found');
|
|
193
|
+
return {
|
|
194
|
+
articles: rawArticles.map(parseArticle),
|
|
195
|
+
category,
|
|
196
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
function generateTagRoute() {
|
|
202
|
+
return `import type { PageServerLoad } from './$types';
|
|
203
|
+
import { getArticlesByTag, parseArticle } from '$lib/db/articles';
|
|
204
|
+
import siteConfig from '../../../../config/site.json';
|
|
205
|
+
|
|
206
|
+
export const load: PageServerLoad = async ({ params, platform }) => {
|
|
207
|
+
const db = platform!.env.DB;
|
|
208
|
+
const rawArticles = await getArticlesByTag(db, params.tag);
|
|
209
|
+
return {
|
|
210
|
+
articles: rawArticles.map(parseArticle),
|
|
211
|
+
tag: params.tag,
|
|
212
|
+
articlesPerPage: siteConfig.articlesPerPage ?? 10,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
function generateDrizzleConfig() {
|
|
218
|
+
return `import type { Config } from 'drizzle-kit';
|
|
219
|
+
|
|
220
|
+
export default {
|
|
221
|
+
schema: './drizzle/schema.ts',
|
|
222
|
+
out: './drizzle/migrations',
|
|
223
|
+
dialect: 'sqlite',
|
|
224
|
+
} satisfies Config;
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
function generateDevVarsExample() {
|
|
228
|
+
return `CLOUDFLARE_ACCOUNT_ID=
|
|
229
|
+
CLOUDFLARE_DATABASE_ID=
|
|
230
|
+
CLOUDFLARE_D1_TOKEN=
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
// ─── wrangler.toml helpers ────────────────────────────────────────────────────
|
|
234
|
+
function d1Section(dbName) {
|
|
235
|
+
return (`\n[[d1_databases]]\n` +
|
|
236
|
+
`binding = "DB"\n` +
|
|
237
|
+
`database_name = "${dbName}"\n` +
|
|
238
|
+
`database_id = "REPLACE_WITH_YOUR_DATABASE_ID"\n`);
|
|
239
|
+
}
|
|
240
|
+
function updateWranglerToml(root, dbName) {
|
|
241
|
+
const wranglerPath = path.join(root, 'wrangler.toml');
|
|
242
|
+
if (fs.existsSync(wranglerPath)) {
|
|
243
|
+
const existing = fs.readFileSync(wranglerPath, 'utf-8');
|
|
244
|
+
fs.writeFileSync(wranglerPath, existing + d1Section(dbName));
|
|
245
|
+
return 'appended';
|
|
246
|
+
}
|
|
247
|
+
const minimal = `name = "cosmolo-site"\n` +
|
|
248
|
+
`compatibility_date = "2025-01-01"\n` +
|
|
249
|
+
d1Section(dbName);
|
|
250
|
+
fs.writeFileSync(wranglerPath, minimal);
|
|
251
|
+
return 'created';
|
|
252
|
+
}
|
|
253
|
+
// ─── preflight checks ─────────────────────────────────────────────────────────
|
|
254
|
+
async function preflight(root, rl) {
|
|
255
|
+
console.log('\n Checking environment...\n');
|
|
256
|
+
// drizzle-orm installed?
|
|
257
|
+
const drizzleInstalled = fs.existsSync(path.join(root, 'node_modules', 'drizzle-orm'));
|
|
258
|
+
if (!drizzleInstalled) {
|
|
259
|
+
console.log(' drizzle-orm is not installed.');
|
|
260
|
+
const install = await confirm(rl, 'Install drizzle-orm and drizzle-kit now?');
|
|
261
|
+
if (install) {
|
|
262
|
+
console.log(' Running: bun add drizzle-orm drizzle-kit ...');
|
|
263
|
+
execSync('bun add drizzle-orm drizzle-kit', { stdio: 'inherit', cwd: root });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
console.log(' Install drizzle-orm and drizzle-kit manually, then re-run migrate:db.');
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// wrangler.toml: existing D1 binding?
|
|
271
|
+
const wranglerPath = path.join(root, 'wrangler.toml');
|
|
272
|
+
if (fs.existsSync(wranglerPath)) {
|
|
273
|
+
const content = fs.readFileSync(wranglerPath, 'utf-8');
|
|
274
|
+
if (content.includes('[[d1_databases]]')) {
|
|
275
|
+
console.log(' Warning: wrangler.toml already contains [[d1_databases]].');
|
|
276
|
+
const proceed = await confirm(rl, 'Add another D1 binding anyway?');
|
|
277
|
+
if (!proceed)
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// drizzle/schema.ts exists?
|
|
282
|
+
const schemaPath = path.join(root, 'drizzle', 'schema.ts');
|
|
283
|
+
if (fs.existsSync(schemaPath)) {
|
|
284
|
+
const ok = await confirm(rl, 'drizzle/schema.ts already exists. Overwrite?');
|
|
285
|
+
if (!ok)
|
|
286
|
+
return null;
|
|
287
|
+
// table name conflict check
|
|
288
|
+
const existing = fs.readFileSync(schemaPath, 'utf-8');
|
|
289
|
+
const conflicts = ['articles', 'categories'].filter((t) => existing.includes(`sqliteTable('${t}'`));
|
|
290
|
+
if (conflicts.length > 0) {
|
|
291
|
+
console.log(` Warning: table(s) already defined in schema: ${conflicts.join(', ')}`);
|
|
292
|
+
const ok2 = await confirm(rl, 'Continue and overwrite?');
|
|
293
|
+
if (!ok2)
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// drizzle.config.ts exists?
|
|
298
|
+
const drizzleConfigPath = path.join(root, 'drizzle.config.ts');
|
|
299
|
+
if (fs.existsSync(drizzleConfigPath)) {
|
|
300
|
+
const ok = await confirm(rl, 'drizzle.config.ts already exists. Overwrite?');
|
|
301
|
+
if (!ok)
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
// src/lib/db/ CRUD files exist?
|
|
305
|
+
const dbDir = path.join(root, 'src', 'lib', 'db');
|
|
306
|
+
const crudFiles = ['articles.ts', 'categories.ts'].filter((f) => fs.existsSync(path.join(dbDir, f)));
|
|
307
|
+
if (crudFiles.length > 0) {
|
|
308
|
+
console.log(` Warning: src/lib/db/${crudFiles.join(', ')} already exist.`);
|
|
309
|
+
const ok = await confirm(rl, 'Overwrite CRUD files?');
|
|
310
|
+
if (!ok)
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
// D1 database name
|
|
314
|
+
const dbName = await ask(rl, 'D1 database name', 'cosmolo');
|
|
315
|
+
// Generate D1-backed routes?
|
|
316
|
+
const routePaths = [
|
|
317
|
+
path.join('src', 'routes', '+page.server.ts'),
|
|
318
|
+
path.join('src', 'routes', 'articles', '[slug]', '+page.server.ts'),
|
|
319
|
+
path.join('src', 'routes', 'categories', '[slug]', '+page.server.ts'),
|
|
320
|
+
path.join('src', 'routes', 'tags', '[tag]', '+page.server.ts'),
|
|
321
|
+
];
|
|
322
|
+
const existingRoutes = routePaths.filter((p) => fs.existsSync(path.join(root, p)));
|
|
323
|
+
let generateRoutes = true;
|
|
324
|
+
if (existingRoutes.length > 0) {
|
|
325
|
+
console.log(`\n The following route files will be replaced with D1-backed versions:`);
|
|
326
|
+
for (const p of existingRoutes)
|
|
327
|
+
console.log(` ${p}`);
|
|
328
|
+
generateRoutes = await confirm(rl, 'Replace with D1-backed route files?');
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
generateRoutes = await confirm(rl, 'Generate D1-backed route files?');
|
|
332
|
+
}
|
|
333
|
+
return { dbName, generateRoutes };
|
|
334
|
+
}
|
|
335
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
336
|
+
export async function drizzleSetup(_config) {
|
|
337
|
+
const root = process.cwd();
|
|
338
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
339
|
+
const result = await preflight(root, rl);
|
|
340
|
+
rl.close();
|
|
341
|
+
if (!result)
|
|
342
|
+
return;
|
|
343
|
+
const { dbName, generateRoutes } = result;
|
|
344
|
+
// Generate drizzle/schema.ts
|
|
345
|
+
const drizzleDir = path.join(root, 'drizzle');
|
|
346
|
+
fs.mkdirSync(drizzleDir, { recursive: true });
|
|
347
|
+
fs.writeFileSync(path.join(drizzleDir, 'schema.ts'), generateDrizzleSchema());
|
|
348
|
+
// Generate src/lib/db/articles.ts and categories.ts
|
|
349
|
+
const dbDir = path.join(root, 'src', 'lib', 'db');
|
|
350
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
351
|
+
fs.writeFileSync(path.join(dbDir, 'articles.ts'), generateArticlesCrud());
|
|
352
|
+
fs.writeFileSync(path.join(dbDir, 'categories.ts'), generateCategoriesCrud());
|
|
353
|
+
// drizzle.config.ts
|
|
354
|
+
fs.writeFileSync(path.join(root, 'drizzle.config.ts'), generateDrizzleConfig());
|
|
355
|
+
// .dev.vars.example
|
|
356
|
+
if (!fs.existsSync(path.join(root, '.dev.vars.example'))) {
|
|
357
|
+
fs.writeFileSync(path.join(root, '.dev.vars.example'), generateDevVarsExample());
|
|
358
|
+
}
|
|
359
|
+
// wrangler.toml
|
|
360
|
+
const wranglerAction = updateWranglerToml(root, dbName);
|
|
361
|
+
// D1-backed route files
|
|
362
|
+
if (generateRoutes) {
|
|
363
|
+
const routesDir = path.join(root, 'src', 'routes');
|
|
364
|
+
const routeFiles = [
|
|
365
|
+
[path.join(routesDir, '+page.server.ts'), generateHomeRoute()],
|
|
366
|
+
[path.join(routesDir, 'articles', '[slug]', '+page.server.ts'), generateArticleRoute()],
|
|
367
|
+
[path.join(routesDir, 'categories', '[slug]', '+page.server.ts'), generateCategoryRoute()],
|
|
368
|
+
[path.join(routesDir, 'tags', '[tag]', '+page.server.ts'), generateTagRoute()],
|
|
369
|
+
];
|
|
370
|
+
for (const [filePath, content] of routeFiles) {
|
|
371
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
372
|
+
fs.writeFileSync(filePath, content);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
console.log('\n✓ Files generated:');
|
|
376
|
+
console.log(' drizzle/schema.ts');
|
|
377
|
+
console.log(' src/lib/db/articles.ts');
|
|
378
|
+
console.log(' src/lib/db/categories.ts');
|
|
379
|
+
console.log(' drizzle.config.ts');
|
|
380
|
+
console.log(' .dev.vars.example');
|
|
381
|
+
console.log(` wrangler.toml (${wranglerAction})`);
|
|
382
|
+
if (generateRoutes) {
|
|
383
|
+
console.log(' src/routes/+page.server.ts');
|
|
384
|
+
console.log(' src/routes/articles/[slug]/+page.server.ts');
|
|
385
|
+
console.log(' src/routes/categories/[slug]/+page.server.ts');
|
|
386
|
+
console.log(' src/routes/tags/[tag]/+page.server.ts');
|
|
387
|
+
}
|
|
388
|
+
let step = 1;
|
|
389
|
+
console.log('\nNext steps:\n');
|
|
390
|
+
console.log(` ${step++}. Create the D1 database (if not done yet):`);
|
|
391
|
+
console.log(` bunx wrangler d1 create ${dbName}`);
|
|
392
|
+
console.log(` Copy the database_id into wrangler.toml.\n`);
|
|
393
|
+
console.log(` ${step++}. Generate migration files:`);
|
|
394
|
+
console.log(` bunx drizzle-kit generate\n`);
|
|
395
|
+
console.log(` ${step++}. Apply migrations locally:`);
|
|
396
|
+
console.log(` bunx wrangler d1 migrations apply ${dbName} --local\n`);
|
|
397
|
+
console.log(` ${step++}. Seed content from Markdown files:`);
|
|
398
|
+
console.log(` bunx cosmolo migrate:db → choose Option 1 to export SQL`);
|
|
399
|
+
console.log(` bunx wrangler d1 execute ${dbName} --local --file=cosmolo-migration/002_seed_categories.sql`);
|
|
400
|
+
console.log(` bunx wrangler d1 execute ${dbName} --local --file=cosmolo-migration/003_seed_articles.sql\n`);
|
|
401
|
+
if (generateRoutes) {
|
|
402
|
+
console.log(` ${step++}. Install marked for Markdown rendering in routes:`);
|
|
403
|
+
console.log(` bun add marked\n`);
|
|
404
|
+
}
|
|
405
|
+
console.log(` ${step++}. Install Cloudflare Workers types for TypeScript:`);
|
|
406
|
+
console.log(` bun add -d @cloudflare/workers-types\n`);
|
|
407
|
+
console.log(` ${step++}. Ensure src/app.d.ts declares the DB binding:`);
|
|
408
|
+
console.log(` interface Platform { env: { DB: D1Database } }`);
|
|
409
|
+
console.log(` (cosmolo init --cloudflare does this automatically)\n`);
|
|
410
|
+
console.log(` See docs/DB_MIGRATION.md for full details.`);
|
|
411
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SqlColumn {
|
|
2
|
+
name: string;
|
|
3
|
+
type: 'TEXT' | 'INTEGER';
|
|
4
|
+
notNull: boolean;
|
|
5
|
+
defaultValue?: string | number;
|
|
6
|
+
primaryKey?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const ARTICLE_COLUMNS: SqlColumn[];
|
|
9
|
+
export declare const CATEGORY_COLUMNS: SqlColumn[];
|
|
10
|
+
export declare function createTableSql(tableName: string, columns: SqlColumn[]): string;
|
|
11
|
+
export declare function escapeSql(val: string): string;
|
|
12
|
+
export declare function toSqlLiteral(val: unknown): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Derived from articleFrontmatterSchema in articles.ts.
|
|
2
|
+
// Arrays are stored as JSON text. Booleans as INTEGER (0/1, SQLite convention).
|
|
3
|
+
export const ARTICLE_COLUMNS = [
|
|
4
|
+
{ name: 'slug', type: 'TEXT', notNull: true, primaryKey: true },
|
|
5
|
+
{ name: 'title', type: 'TEXT', notNull: true },
|
|
6
|
+
{ name: 'category', type: 'TEXT', notNull: true },
|
|
7
|
+
{ name: 'excerpt', type: 'TEXT', notNull: true },
|
|
8
|
+
{ name: 'sort', type: 'INTEGER', notNull: true, defaultValue: 0 },
|
|
9
|
+
{ name: 'date', type: 'TEXT', notNull: true, defaultValue: '' },
|
|
10
|
+
{ name: 'tags', type: 'TEXT', notNull: true, defaultValue: '[]' },
|
|
11
|
+
{ name: 'series', type: 'TEXT', notNull: false },
|
|
12
|
+
{ name: 'series_order', type: 'INTEGER', notNull: false },
|
|
13
|
+
{ name: 'draft', type: 'INTEGER', notNull: true, defaultValue: 0 },
|
|
14
|
+
{ name: 'related', type: 'TEXT', notNull: true, defaultValue: '[]' },
|
|
15
|
+
{ name: 'body', type: 'TEXT', notNull: true },
|
|
16
|
+
];
|
|
17
|
+
export const CATEGORY_COLUMNS = [
|
|
18
|
+
{ name: 'key', type: 'TEXT', notNull: true, primaryKey: true },
|
|
19
|
+
{ name: 'label', type: 'TEXT', notNull: true },
|
|
20
|
+
{ name: 'description', type: 'TEXT', notNull: true, defaultValue: '' },
|
|
21
|
+
];
|
|
22
|
+
function columnDef(col) {
|
|
23
|
+
let def = ` ${col.name} ${col.type}`;
|
|
24
|
+
if (col.primaryKey) {
|
|
25
|
+
def += ' PRIMARY KEY';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (col.notNull)
|
|
29
|
+
def += ' NOT NULL';
|
|
30
|
+
if (col.defaultValue !== undefined) {
|
|
31
|
+
const val = typeof col.defaultValue === 'string' ? `'${col.defaultValue}'` : col.defaultValue;
|
|
32
|
+
def += ` DEFAULT ${val}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return def;
|
|
36
|
+
}
|
|
37
|
+
export function createTableSql(tableName, columns) {
|
|
38
|
+
const defs = columns.map(columnDef).join(',\n');
|
|
39
|
+
return `CREATE TABLE IF NOT EXISTS ${tableName} (\n${defs}\n);`;
|
|
40
|
+
}
|
|
41
|
+
export function escapeSql(val) {
|
|
42
|
+
return val.replace(/'/g, "''");
|
|
43
|
+
}
|
|
44
|
+
export function toSqlLiteral(val) {
|
|
45
|
+
if (val === null || val === undefined)
|
|
46
|
+
return 'NULL';
|
|
47
|
+
if (typeof val === 'boolean')
|
|
48
|
+
return val ? '1' : '0';
|
|
49
|
+
if (typeof val === 'number')
|
|
50
|
+
return String(val);
|
|
51
|
+
if (val instanceof Date)
|
|
52
|
+
return `'${escapeSql(val.toISOString().split('T')[0])}'`;
|
|
53
|
+
if (Array.isArray(val))
|
|
54
|
+
return `'${escapeSql(JSON.stringify(val))}'`;
|
|
55
|
+
return `'${escapeSql(String(val))}'`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import { ARTICLE_COLUMNS, CATEGORY_COLUMNS, createTableSql } from './schema.js';
|
|
4
|
+
import { buildCategoriesInserts, buildArticlesInserts } from './sql-export.js';
|
|
5
|
+
function confirm(question) {
|
|
6
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return new Promise((resolve) => rl.question(`${question} [y/N] `, (ans) => {
|
|
8
|
+
rl.close();
|
|
9
|
+
resolve(ans.trim().toLowerCase() === 'y');
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
function resolveDbPath(url) {
|
|
13
|
+
// Reject known non-SQLite URL schemes
|
|
14
|
+
if (/^(postgres|postgresql|mysql|mariadb|mongodb):\/\//i.test(url))
|
|
15
|
+
return null;
|
|
16
|
+
return url.replace(/^(sqlite:|file:)/, '');
|
|
17
|
+
}
|
|
18
|
+
export async function executeSqlDirect(config) {
|
|
19
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
20
|
+
if (!dbUrl) {
|
|
21
|
+
console.error('\n DATABASE_URL is not set.');
|
|
22
|
+
console.error(' Example: DATABASE_URL=./mysite.db bunx cosmolo migrate:db');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const dbPath = resolveDbPath(dbUrl);
|
|
26
|
+
if (!dbPath) {
|
|
27
|
+
console.error('\n Only SQLite databases are supported for direct execution.');
|
|
28
|
+
console.error(' Use Option 1 to export SQL files for other database engines.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const root = process.cwd();
|
|
32
|
+
const catInserts = buildCategoriesInserts(path.join(root, config.categoriesConfigPath));
|
|
33
|
+
const artInserts = buildArticlesInserts(path.join(root, config.articlesDir));
|
|
34
|
+
console.log(`\n Target: ${dbPath}`);
|
|
35
|
+
console.log(` Categories: ${catInserts.length} rows`);
|
|
36
|
+
console.log(` Articles: ${artInserts.length} rows`);
|
|
37
|
+
const ok = await confirm('\n Proceed?');
|
|
38
|
+
if (!ok) {
|
|
39
|
+
console.log(' Aborted.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// bun:sqlite is a bun built-in; the 'bun:' scheme is not a standard ESM URL
|
|
43
|
+
// so we suppress the TypeScript module resolution error here.
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
const { Database } = await import('bun:sqlite');
|
|
47
|
+
const db = new Database(dbPath);
|
|
48
|
+
try {
|
|
49
|
+
db.run('BEGIN TRANSACTION');
|
|
50
|
+
db.run(createTableSql('categories', CATEGORY_COLUMNS));
|
|
51
|
+
db.run(createTableSql('articles', ARTICLE_COLUMNS));
|
|
52
|
+
for (const sql of catInserts)
|
|
53
|
+
db.run(sql);
|
|
54
|
+
for (const sql of artInserts)
|
|
55
|
+
db.run(sql);
|
|
56
|
+
db.run('COMMIT');
|
|
57
|
+
console.log('\n ✓ Migration complete.');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
db.run('ROLLBACK');
|
|
61
|
+
console.error('\n Migration failed:', err);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResolvedCosmoloConfig } from '../../types.js';
|
|
2
|
+
export declare function buildCategoriesInserts(categoriesPath: string): string[];
|
|
3
|
+
export declare function buildArticlesInserts(articlesDir: string): string[];
|
|
4
|
+
export declare function exportSqlFiles(config: ResolvedCosmoloConfig): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { ARTICLE_COLUMNS, CATEGORY_COLUMNS, createTableSql, escapeSql, toSqlLiteral, } from './schema.js';
|
|
5
|
+
export function buildCategoriesInserts(categoriesPath) {
|
|
6
|
+
if (!fs.existsSync(categoriesPath))
|
|
7
|
+
return [];
|
|
8
|
+
const raw = JSON.parse(fs.readFileSync(categoriesPath, 'utf-8'));
|
|
9
|
+
return Object.entries(raw).map(([key, entry]) => {
|
|
10
|
+
const label = escapeSql(entry.label ?? '');
|
|
11
|
+
const description = escapeSql(entry.description ?? '');
|
|
12
|
+
return `INSERT INTO categories (key, label, description) VALUES ('${escapeSql(key)}', '${label}', '${description}');`;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function collectArticleFiles(dir, baseDir) {
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
results.push(...collectArticleFiles(fullPath, baseDir));
|
|
21
|
+
}
|
|
22
|
+
else if (/\.(md|svx)$/.test(entry.name)) {
|
|
23
|
+
const rel = path.relative(baseDir, fullPath).replace(/\.(md|svx)$/, '');
|
|
24
|
+
// Normalize path separators to forward slashes for slug
|
|
25
|
+
results.push({ filePath: fullPath, slug: rel.replace(/\\/g, '/') });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
export function buildArticlesInserts(articlesDir) {
|
|
31
|
+
if (!fs.existsSync(articlesDir))
|
|
32
|
+
return [];
|
|
33
|
+
return collectArticleFiles(articlesDir, articlesDir).map(({ filePath, slug }) => {
|
|
34
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
35
|
+
const { data, content } = matter(raw);
|
|
36
|
+
const dateVal = data.date instanceof Date
|
|
37
|
+
? data.date.toISOString().split('T')[0]
|
|
38
|
+
: (data.date ?? '');
|
|
39
|
+
const values = [
|
|
40
|
+
toSqlLiteral(slug),
|
|
41
|
+
toSqlLiteral(data.title ?? ''),
|
|
42
|
+
toSqlLiteral(data.category ?? ''),
|
|
43
|
+
toSqlLiteral(data.excerpt ?? ''),
|
|
44
|
+
toSqlLiteral(data.sort ?? 0),
|
|
45
|
+
toSqlLiteral(dateVal),
|
|
46
|
+
toSqlLiteral(data.tags ?? []),
|
|
47
|
+
toSqlLiteral(data.series ?? null),
|
|
48
|
+
toSqlLiteral(data.seriesOrder ?? null),
|
|
49
|
+
toSqlLiteral(data.draft ?? false),
|
|
50
|
+
toSqlLiteral(data.related ?? []),
|
|
51
|
+
toSqlLiteral(content),
|
|
52
|
+
].join(', ');
|
|
53
|
+
const cols = 'slug, title, category, excerpt, sort, date, tags, series, series_order, draft, related, body';
|
|
54
|
+
return `INSERT INTO articles (${cols}) VALUES (${values});`;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function exportSqlFiles(config) {
|
|
58
|
+
const root = process.cwd();
|
|
59
|
+
const outputDir = path.join(root, 'cosmolo-migration');
|
|
60
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
61
|
+
const createSql = createTableSql('categories', CATEGORY_COLUMNS) +
|
|
62
|
+
'\n\n' +
|
|
63
|
+
createTableSql('articles', ARTICLE_COLUMNS);
|
|
64
|
+
fs.writeFileSync(path.join(outputDir, '001_create_tables.sql'), createSql + '\n');
|
|
65
|
+
const categoriesPath = path.join(root, config.categoriesConfigPath);
|
|
66
|
+
const catInserts = buildCategoriesInserts(categoriesPath);
|
|
67
|
+
fs.writeFileSync(path.join(outputDir, '002_seed_categories.sql'), catInserts.join('\n') + '\n');
|
|
68
|
+
const articlesDir = path.join(root, config.articlesDir);
|
|
69
|
+
const artInserts = buildArticlesInserts(articlesDir);
|
|
70
|
+
fs.writeFileSync(path.join(outputDir, '003_seed_articles.sql'), artInserts.join('\n') + '\n');
|
|
71
|
+
console.log(`\n✓ SQL files written to cosmolo-migration/`);
|
|
72
|
+
console.log(' 001_create_tables.sql');
|
|
73
|
+
console.log(` 002_seed_categories.sql (${catInserts.length} rows)`);
|
|
74
|
+
console.log(` 003_seed_articles.sql (${artInserts.length} rows)`);
|
|
75
|
+
console.log('\nReview the files and run them against your database.');
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(): Promise<void>;
|