@beechcms/cli 0.5.0 → 0.6.0-preview.2
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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +116 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +116 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/validate.ts.html +325 -0
- package/coverage/lcov.info +80 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/validate.ts.html +325 -0
- package/dist/commands/seed-load.d.ts +8 -0
- package/dist/commands/seed-load.d.ts.map +1 -0
- package/dist/commands/seed-load.js +89 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +241 -72
- package/dist/lib/schema-diff.d.ts +15 -0
- package/dist/lib/schema-diff.d.ts.map +1 -0
- package/dist/lib/schema-diff.js +37 -0
- package/dist/lib/wrangler.d.ts +17 -0
- package/dist/lib/wrangler.d.ts.map +1 -0
- package/dist/lib/wrangler.js +65 -0
- package/package.json +9 -5
- package/src/commands/deploy.ts +126 -123
- package/src/commands/init.ts +599 -557
- package/src/commands/onboard.ts +32 -0
- package/src/commands/seed-create.ts +192 -189
- package/src/commands/seed-load.ts +235 -155
- package/src/commands/update.ts +54 -51
- package/src/commands/validate.ts +80 -83
- package/src/index.ts +17 -12
- package/src/lib/schema-diff.ts +150 -64
- package/src/lib/wrangler.ts +129 -106
- package/tsconfig.json +16 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +33 -0
package/dist/index.js
CHANGED
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
generateDraftTable,
|
|
7
7
|
generateIndexes,
|
|
8
8
|
generateFtsTable,
|
|
9
|
-
generateFtsTriggers
|
|
9
|
+
generateFtsTriggers,
|
|
10
|
+
generateJunctionTable,
|
|
11
|
+
generateJunctionIndexes,
|
|
12
|
+
generateJunctionDraftTable,
|
|
13
|
+
sortSeedsByDependencies
|
|
10
14
|
} from "@beechcms/core";
|
|
11
15
|
|
|
12
16
|
// src/lib/wrangler.ts
|
|
@@ -36,8 +40,18 @@ function executeD1File(sql, options) {
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
function queryD1(sql, options) {
|
|
39
|
-
const
|
|
40
|
-
|
|
43
|
+
const tmpFile = join(tmpdir(), `beech-query-${Date.now()}.sql`);
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(tmpFile, sql, "utf-8");
|
|
47
|
+
const args = ["d1", "execute", options.db, "--file", tmpFile, "--json", ...buildArgs(options)];
|
|
48
|
+
result = spawnSync("npx", ["wrangler", ...args], { encoding: "utf-8", cwd: process.cwd(), shell: true });
|
|
49
|
+
} finally {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(tmpFile);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
if (result.status !== 0) {
|
|
42
56
|
throw new Error(`wrangler d1 execute failed:
|
|
43
57
|
${result.stderr}`);
|
|
@@ -67,6 +81,9 @@ function findWranglerConfig() {
|
|
|
67
81
|
}
|
|
68
82
|
return null;
|
|
69
83
|
}
|
|
84
|
+
function sqlQuote(value) {
|
|
85
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
86
|
+
}
|
|
70
87
|
function resolveDbName(configPath) {
|
|
71
88
|
if (!configPath) return "beech-db";
|
|
72
89
|
try {
|
|
@@ -107,11 +124,11 @@ async function diffSeed(seed, options) {
|
|
|
107
124
|
const expectedSet = new Set(expected.map((c) => c.name));
|
|
108
125
|
const columns = [];
|
|
109
126
|
for (const col of expected) {
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
127
|
+
const actualRow = actualMap.get(col.name);
|
|
128
|
+
if (!actualRow) {
|
|
112
129
|
columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
|
|
113
|
-
} else if (
|
|
114
|
-
columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType:
|
|
130
|
+
} else if (actualRow.type.toUpperCase() !== col.sqlType) {
|
|
131
|
+
columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
|
|
115
132
|
} else {
|
|
116
133
|
columns.push({ name: col.name, status: "ok" });
|
|
117
134
|
}
|
|
@@ -121,37 +138,57 @@ async function diffSeed(seed, options) {
|
|
|
121
138
|
columns.push({ name: row.name, status: "extra", actualType: row.type });
|
|
122
139
|
}
|
|
123
140
|
}
|
|
141
|
+
const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
|
|
142
|
+
if (relationBranches.length > 0) {
|
|
143
|
+
let fkList = [];
|
|
144
|
+
let indexList = [];
|
|
145
|
+
try {
|
|
146
|
+
fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
|
|
147
|
+
indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
const fkByCol = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const fk of fkList) {
|
|
152
|
+
fkByCol.set(fk.from, fk);
|
|
153
|
+
}
|
|
154
|
+
const indexNames = new Set(indexList.map((i) => i.name));
|
|
155
|
+
for (const branch of relationBranches) {
|
|
156
|
+
const expectedFkTable = `content_${branch.targetSeed}`;
|
|
157
|
+
const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
|
|
158
|
+
const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
|
|
159
|
+
const colDiff = columns.find((c) => c.name === branch.alias);
|
|
160
|
+
if (!colDiff || colDiff.status === "missing") continue;
|
|
161
|
+
const fk = fkByCol.get(branch.alias);
|
|
162
|
+
if (!fk) {
|
|
163
|
+
colDiff.status = "fk_missing";
|
|
164
|
+
colDiff.expectedTarget = branch.targetSeed;
|
|
165
|
+
} else {
|
|
166
|
+
const actualTable = fk.table;
|
|
167
|
+
const actualOnDelete = fk.on_delete.toUpperCase();
|
|
168
|
+
if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
|
|
169
|
+
colDiff.status = "fk_mismatch";
|
|
170
|
+
colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
|
|
171
|
+
colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
|
|
172
|
+
colDiff.expectedTarget = branch.targetSeed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!indexNames.has(expectedIndexName)) {
|
|
176
|
+
if (colDiff.status === "ok") {
|
|
177
|
+
colDiff.status = "index_missing";
|
|
178
|
+
} else {
|
|
179
|
+
columns.push({ name: branch.alias, status: "index_missing" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
124
184
|
return { slug: seed.slug, tableExists: true, columns };
|
|
125
185
|
}
|
|
126
186
|
|
|
127
187
|
// src/commands/validate.ts
|
|
128
188
|
import pc from "picocolors";
|
|
129
|
-
import { SEED_REGISTRY } from "@beechcms/core";
|
|
189
|
+
import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
|
|
130
190
|
function validateSeeds(registry) {
|
|
131
|
-
|
|
132
|
-
const slugsSeen = /* @__PURE__ */ new Set();
|
|
133
|
-
for (const seed of Object.values(registry)) {
|
|
134
|
-
const messages = [];
|
|
135
|
-
if (slugsSeen.has(seed.slug)) {
|
|
136
|
-
messages.push(`duplicate slug "${seed.slug}" \u2014 each seed must have a unique slug`);
|
|
137
|
-
}
|
|
138
|
-
slugsSeen.add(seed.slug);
|
|
139
|
-
const aliasesSeen = /* @__PURE__ */ new Set();
|
|
140
|
-
for (const branch of seed.branches) {
|
|
141
|
-
if (aliasesSeen.has(branch.alias)) {
|
|
142
|
-
messages.push(`duplicate branch alias "${branch.alias}"`);
|
|
143
|
-
}
|
|
144
|
-
aliasesSeen.add(branch.alias);
|
|
145
|
-
}
|
|
146
|
-
const allAliases = new Set(seed.branches.map((b) => b.alias));
|
|
147
|
-
if (!allAliases.has(seed.displayNameAlias)) {
|
|
148
|
-
messages.push(`displayNameAlias "${seed.displayNameAlias}" not found in branches`);
|
|
149
|
-
}
|
|
150
|
-
if (messages.length > 0) {
|
|
151
|
-
result.push({ slug: seed.slug, messages });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return result;
|
|
191
|
+
return validateSeedDefinitions(Object.values(registry));
|
|
155
192
|
}
|
|
156
193
|
async function validate(args) {
|
|
157
194
|
const registry = args.registry ?? SEED_REGISTRY;
|
|
@@ -161,32 +198,57 @@ async function validate(args) {
|
|
|
161
198
|
}
|
|
162
199
|
console.log(pc.cyan("\n beech validate \u2014 checking seeds\n"));
|
|
163
200
|
const errors = validateSeeds(registry);
|
|
164
|
-
const
|
|
165
|
-
|
|
201
|
+
const fatalErrors = errors.filter((e) => e.fatal);
|
|
202
|
+
const warnings = errors.filter((e) => !e.fatal);
|
|
203
|
+
for (const e of fatalErrors) {
|
|
204
|
+
console.log(pc.red(` \u2717 ${e.slug} (fatal)`));
|
|
205
|
+
for (const msg of e.messages) {
|
|
206
|
+
console.log(pc.red(` \u2192 ${msg}`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
|
|
210
|
+
const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
|
|
166
211
|
for (const seed of Object.values(registry)) {
|
|
167
|
-
const msgs =
|
|
212
|
+
const msgs = warningMap.get(seed.slug);
|
|
168
213
|
if (!msgs) {
|
|
169
|
-
|
|
214
|
+
if (!allWarningSlugsSeen.has(seed.slug)) {
|
|
215
|
+
const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
|
|
216
|
+
if (!hasFatal) console.log(pc.green(` \u2713 ${seed.slug}`));
|
|
217
|
+
}
|
|
170
218
|
} else {
|
|
171
|
-
|
|
172
|
-
console.log(pc.red(` \u2717 ${seed.slug}`));
|
|
219
|
+
console.log(pc.yellow(` \u26A0 ${seed.slug}`));
|
|
173
220
|
for (const msg of msgs) {
|
|
174
|
-
console.log(pc.
|
|
221
|
+
console.log(pc.yellow(` \u2192 ${msg}`));
|
|
175
222
|
}
|
|
176
223
|
}
|
|
177
224
|
}
|
|
178
225
|
console.log("");
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
226
|
+
const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
|
|
227
|
+
const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
|
|
228
|
+
if (totalFatal > 0) {
|
|
229
|
+
const s = totalFatal !== 1 ? "s" : "";
|
|
230
|
+
console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
|
|
182
231
|
`));
|
|
183
232
|
process.exit(1);
|
|
233
|
+
} else if (totalWarnings > 0) {
|
|
234
|
+
const s = totalWarnings !== 1 ? "s" : "";
|
|
235
|
+
console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
|
|
236
|
+
`));
|
|
184
237
|
} else {
|
|
185
238
|
console.log(pc.green(" All seeds valid.\n"));
|
|
186
239
|
}
|
|
187
240
|
}
|
|
188
241
|
|
|
189
242
|
// src/commands/seed-load.ts
|
|
243
|
+
function buildSeedRegistrationSql(seed) {
|
|
244
|
+
const json = sqlQuote(JSON.stringify(seed));
|
|
245
|
+
return [
|
|
246
|
+
`INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
|
|
247
|
+
`VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
|
|
248
|
+
`ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
|
|
249
|
+
].join("\n");
|
|
250
|
+
}
|
|
251
|
+
var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
|
|
190
252
|
function buildStatements(seed) {
|
|
191
253
|
const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
|
|
192
254
|
const draft = generateDraftTable(seed);
|
|
@@ -195,10 +257,16 @@ function buildStatements(seed) {
|
|
|
195
257
|
if (fts) {
|
|
196
258
|
stmts.push(fts, ...generateFtsTriggers(seed));
|
|
197
259
|
}
|
|
260
|
+
for (const branch of seed.branches) {
|
|
261
|
+
if (branch.type !== "relation" || branch.multiple !== true) continue;
|
|
262
|
+
stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
|
|
263
|
+
const draftJunction = generateJunctionDraftTable(seed, branch);
|
|
264
|
+
if (draftJunction) stmts.push(draftJunction);
|
|
265
|
+
}
|
|
198
266
|
return stmts;
|
|
199
267
|
}
|
|
200
268
|
async function runDiff(options, registry) {
|
|
201
|
-
const seeds = Object.values(registry);
|
|
269
|
+
const seeds = sortSeedsByDependencies(Object.values(registry));
|
|
202
270
|
console.log(pc2.cyan("\n Diffing schema\u2026\n"));
|
|
203
271
|
let allOk = true;
|
|
204
272
|
for (const seed of seeds) {
|
|
@@ -223,6 +291,12 @@ async function runDiff(options, registry) {
|
|
|
223
291
|
console.log(pc2.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 exists in DB but not in seeds.ts`));
|
|
224
292
|
} else if (col.status === "type_mismatch") {
|
|
225
293
|
console.log(pc2.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
|
|
294
|
+
} else if (col.status === "fk_missing") {
|
|
295
|
+
console.log(pc2.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
|
|
296
|
+
} else if (col.status === "fk_mismatch") {
|
|
297
|
+
console.log(pc2.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
|
|
298
|
+
} else if (col.status === "index_missing") {
|
|
299
|
+
console.log(pc2.yellow(` \u2298 missing index on ${col.name}`));
|
|
226
300
|
}
|
|
227
301
|
}
|
|
228
302
|
}
|
|
@@ -234,7 +308,7 @@ async function runDiff(options, registry) {
|
|
|
234
308
|
}
|
|
235
309
|
}
|
|
236
310
|
async function runLoad(options, dryRun, registry) {
|
|
237
|
-
const seeds = Object.values(registry);
|
|
311
|
+
const seeds = sortSeedsByDependencies(Object.values(registry));
|
|
238
312
|
if (dryRun) {
|
|
239
313
|
console.log(pc2.cyan("\n -- dry-run: SQL that would be executed\n"));
|
|
240
314
|
for (const seed of seeds) {
|
|
@@ -243,14 +317,18 @@ async function runLoad(options, dryRun, registry) {
|
|
|
243
317
|
for (const stmt of stmts) {
|
|
244
318
|
console.log(stmt + "\n");
|
|
245
319
|
}
|
|
320
|
+
console.log(pc2.dim(` -- register ${seed.slug} in seeds table`));
|
|
321
|
+
console.log(buildSeedRegistrationSql(seed) + "\n");
|
|
246
322
|
}
|
|
323
|
+
console.log(pc2.dim(" -- bump registry_version"));
|
|
324
|
+
console.log(SEED_META_BUMP_SQL + "\n");
|
|
247
325
|
return;
|
|
248
326
|
}
|
|
249
327
|
console.log(pc2.cyan(`
|
|
250
328
|
Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
|
|
251
329
|
`));
|
|
252
330
|
for (const seed of seeds) {
|
|
253
|
-
const stmts = buildStatements(seed);
|
|
331
|
+
const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
|
|
254
332
|
const sql = stmts.join("\n\n") + "\n";
|
|
255
333
|
process.stdout.write(` ${pc2.dim("\u2192")} content_${seed.slug}\u2026 `);
|
|
256
334
|
const ok = executeD1File(sql, options);
|
|
@@ -274,7 +352,10 @@ async function runLoad(options, dryRun, registry) {
|
|
|
274
352
|
}
|
|
275
353
|
console.log(pc2.green("done"));
|
|
276
354
|
}
|
|
355
|
+
executeD1File(SEED_META_BUMP_SQL, options);
|
|
277
356
|
console.log(pc2.green("\n All seeds loaded.\n"));
|
|
357
|
+
console.log(pc2.dim(" Definitions registered in the database."));
|
|
358
|
+
console.log(pc2.dim(" seed.ts is no longer required at runtime \u2014 you may keep it for code-first edits or delete it.\n"));
|
|
278
359
|
}
|
|
279
360
|
async function seedLoad(args) {
|
|
280
361
|
const registry = args.registry ?? SEED_REGISTRY2;
|
|
@@ -285,8 +366,25 @@ async function seedLoad(args) {
|
|
|
285
366
|
return;
|
|
286
367
|
}
|
|
287
368
|
const validationErrors = validateSeeds(registry);
|
|
288
|
-
|
|
289
|
-
|
|
369
|
+
const fatalErrors = validationErrors.filter((e) => e.fatal);
|
|
370
|
+
const warnings = validationErrors.filter((e) => !e.fatal);
|
|
371
|
+
if (fatalErrors.length > 0) {
|
|
372
|
+
const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
|
|
373
|
+
const s = total !== 1 ? "s" : "";
|
|
374
|
+
console.log(pc2.red(`
|
|
375
|
+
\u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
|
|
376
|
+
`));
|
|
377
|
+
for (const e of fatalErrors) {
|
|
378
|
+
console.log(pc2.red(` \u2717 ${e.slug}`));
|
|
379
|
+
for (const msg of e.messages) {
|
|
380
|
+
console.log(pc2.red(` \u2192 ${msg}`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
console.log("");
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
if (warnings.length > 0) {
|
|
387
|
+
const total = warnings.reduce((n, e) => n + e.messages.length, 0);
|
|
290
388
|
const s = total !== 1 ? "s" : "";
|
|
291
389
|
console.log(pc2.yellow(`
|
|
292
390
|
\u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
|
|
@@ -300,6 +398,27 @@ async function seedLoad(args) {
|
|
|
300
398
|
local: args.local,
|
|
301
399
|
configPath
|
|
302
400
|
};
|
|
401
|
+
if (!args.dryRun && !args.diff) {
|
|
402
|
+
try {
|
|
403
|
+
const rows = queryD1(
|
|
404
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
|
|
405
|
+
options
|
|
406
|
+
);
|
|
407
|
+
if (rows.length < 2) {
|
|
408
|
+
console.log(pc2.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
|
|
409
|
+
console.log(pc2.dim(" Run `beech init --db` first to initialise the database."));
|
|
410
|
+
const flag = args.local ? " --local" : "";
|
|
411
|
+
console.log(pc2.cyan(`
|
|
412
|
+
\u2192 Run: npx beech init --db${flag}
|
|
413
|
+
`));
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
console.log(pc2.red("\n \u2717 Could not query the database\n"));
|
|
418
|
+
console.log(pc2.dim(" Run `beech init --db` first to initialise the database."));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
303
422
|
if (args.diff) {
|
|
304
423
|
await runDiff(options, registry);
|
|
305
424
|
} else {
|
|
@@ -324,7 +443,10 @@ var SYSTEM_TABLES = [
|
|
|
324
443
|
"notifications",
|
|
325
444
|
"media_objects",
|
|
326
445
|
"content_event_log",
|
|
327
|
-
"automations"
|
|
446
|
+
"automations",
|
|
447
|
+
"seeds",
|
|
448
|
+
"seed_meta",
|
|
449
|
+
"site_settings"
|
|
328
450
|
];
|
|
329
451
|
var BASE_SCHEMA_SQL = `
|
|
330
452
|
CREATE TABLE IF NOT EXISTS users (
|
|
@@ -462,6 +584,31 @@ CREATE TABLE IF NOT EXISTS automations (
|
|
|
462
584
|
|
|
463
585
|
CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
|
|
464
586
|
CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
|
|
587
|
+
|
|
588
|
+
CREATE TABLE IF NOT EXISTS seeds (
|
|
589
|
+
slug TEXT NOT NULL PRIMARY KEY,
|
|
590
|
+
definition TEXT NOT NULL,
|
|
591
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
592
|
+
CHECK (status IN ('active', 'deleted')),
|
|
593
|
+
source TEXT NOT NULL DEFAULT 'runtime'
|
|
594
|
+
CHECK (source IN ('code', 'runtime')),
|
|
595
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
596
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
CREATE INDEX IF NOT EXISTS idx_seeds_status ON seeds(status);
|
|
600
|
+
|
|
601
|
+
CREATE TABLE IF NOT EXISTS seed_meta (
|
|
602
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
603
|
+
value TEXT NOT NULL
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
INSERT OR IGNORE INTO seed_meta (id, value) VALUES ('registry_version', '1');
|
|
607
|
+
|
|
608
|
+
CREATE TABLE IF NOT EXISTS site_settings (
|
|
609
|
+
key TEXT NOT NULL PRIMARY KEY,
|
|
610
|
+
value TEXT NOT NULL
|
|
611
|
+
);
|
|
465
612
|
`.trim();
|
|
466
613
|
var PLACEHOLDER_DB_IDS = [
|
|
467
614
|
"INCOLLA_QUI_IL_TUO_ID_D1",
|
|
@@ -601,7 +748,7 @@ function checkFiles(cwd, checkDevVars) {
|
|
|
601
748
|
}
|
|
602
749
|
const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
|
|
603
750
|
if (!seedsExists) {
|
|
604
|
-
console.log(pc3.yellow(" \u26A0 seeds.ts \u2014
|
|
751
|
+
console.log(pc3.yellow(" \u26A0 seeds.ts \u2014 not found (optional: needed only for the one-time code \u2192 DB load; after `beech seed:load`, the DB is canonical)"));
|
|
605
752
|
} else {
|
|
606
753
|
console.log(pc3.green(" \u2713 seeds.ts"));
|
|
607
754
|
}
|
|
@@ -618,7 +765,7 @@ function printNextSteps(local) {
|
|
|
618
765
|
const localFlag = local ? " --local" : "";
|
|
619
766
|
console.log(pc3.dim(" Next steps:"));
|
|
620
767
|
console.log(pc3.cyan(` 1. npx beech seed:load${localFlag}`));
|
|
621
|
-
console.log(pc3.dim(" \u2192 create content tables
|
|
768
|
+
console.log(pc3.dim(" \u2192 create content tables and register seed definitions in D1"));
|
|
622
769
|
console.log(pc3.cyan(" 2. npx wrangler dev"));
|
|
623
770
|
console.log(pc3.dim(" \u2192 start API + dashboard"));
|
|
624
771
|
console.log(pc3.dim(" 3. Open http://localhost:8789/admin\n"));
|
|
@@ -663,7 +810,7 @@ async function init(args) {
|
|
|
663
810
|
for (const issue of placeholders) {
|
|
664
811
|
console.log(pc3.yellow(` - ${issue}`));
|
|
665
812
|
}
|
|
666
|
-
if (process.stdin.isTTY) {
|
|
813
|
+
if (process.stdin.isTTY && !args.nonInteractive) {
|
|
667
814
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
668
815
|
let autoCreate = false;
|
|
669
816
|
try {
|
|
@@ -721,7 +868,14 @@ async function init(args) {
|
|
|
721
868
|
printManualDbInstructions();
|
|
722
869
|
process.exit(1);
|
|
723
870
|
}
|
|
871
|
+
} else if (args.nonInteractive && args.local) {
|
|
872
|
+
console.log(pc3.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
|
|
724
873
|
} else {
|
|
874
|
+
if (args.nonInteractive) {
|
|
875
|
+
console.log(pc3.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
|
|
876
|
+
console.log(pc3.dim(" Set a real database_id in wrangler.jsonc, then retry."));
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
725
879
|
printManualDbInstructions();
|
|
726
880
|
process.exit(1);
|
|
727
881
|
}
|
|
@@ -1039,50 +1193,65 @@ async function deploy(args) {
|
|
|
1039
1193
|
}
|
|
1040
1194
|
}
|
|
1041
1195
|
|
|
1042
|
-
// src/commands/
|
|
1196
|
+
// src/commands/onboard.ts
|
|
1043
1197
|
import pc6 from "picocolors";
|
|
1198
|
+
async function onboard(args) {
|
|
1199
|
+
console.log(pc6.cyan("\n beech onboard \u2014 full provisioning\n"));
|
|
1200
|
+
await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes });
|
|
1201
|
+
await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null });
|
|
1202
|
+
console.log(pc6.cyan("\n Provisioning complete.\n"));
|
|
1203
|
+
console.log(pc6.dim(" Next steps:"));
|
|
1204
|
+
console.log(pc6.cyan(" 1. npx wrangler dev"));
|
|
1205
|
+
console.log(pc6.dim(" \u2192 start API + dashboard"));
|
|
1206
|
+
console.log(pc6.dim(" 2. Open http://localhost:8789/admin"));
|
|
1207
|
+
console.log(pc6.dim(" \u2192 complete setup wizard to create admin user\n"));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/commands/update.ts
|
|
1211
|
+
import pc7 from "picocolors";
|
|
1044
1212
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1045
1213
|
async function update(_args) {
|
|
1046
|
-
console.log(
|
|
1047
|
-
console.log(
|
|
1214
|
+
console.log(pc7.cyan("\n beech update\n"));
|
|
1215
|
+
console.log(pc7.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
|
|
1048
1216
|
const installResult = spawnSync4(
|
|
1049
1217
|
"npm",
|
|
1050
1218
|
["install", "@beechcms/api@latest", "@beechcms/core@latest"],
|
|
1051
1219
|
{ stdio: "inherit", cwd: process.cwd(), shell: true }
|
|
1052
1220
|
);
|
|
1053
1221
|
if (installResult.status !== 0) {
|
|
1054
|
-
console.log(
|
|
1055
|
-
console.log(
|
|
1056
|
-
console.log(
|
|
1057
|
-
console.log(
|
|
1222
|
+
console.log(pc7.red("\n \u2717 npm install failed\n"));
|
|
1223
|
+
console.log(pc7.dim(" Check the output above for details."));
|
|
1224
|
+
console.log(pc7.dim(" You may need to resolve version conflicts manually."));
|
|
1225
|
+
console.log(pc7.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
|
|
1058
1226
|
process.exit(1);
|
|
1059
1227
|
}
|
|
1060
|
-
console.log(
|
|
1061
|
-
console.log(
|
|
1228
|
+
console.log(pc7.green("\n \u2713 Packages updated"));
|
|
1229
|
+
console.log(pc7.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
|
|
1062
1230
|
const initResult = spawnSync4(
|
|
1063
1231
|
"npx",
|
|
1064
1232
|
["beech", "init", "--db", "--local"],
|
|
1065
1233
|
{ stdio: "inherit", cwd: process.cwd(), shell: true }
|
|
1066
1234
|
);
|
|
1067
1235
|
if (initResult.status !== 0) {
|
|
1068
|
-
console.log(
|
|
1069
|
-
console.log(
|
|
1070
|
-
console.log(
|
|
1236
|
+
console.log(pc7.yellow("\n \u26A0 Local DB update failed\n"));
|
|
1237
|
+
console.log(pc7.dim(" Apply system migrations manually:"));
|
|
1238
|
+
console.log(pc7.cyan(" \u2192 Run: npx beech init --db --local\n"));
|
|
1071
1239
|
} else {
|
|
1072
|
-
console.log(
|
|
1240
|
+
console.log(pc7.green("\n \u2713 Local database updated"));
|
|
1073
1241
|
}
|
|
1074
|
-
console.log(
|
|
1075
|
-
console.log(
|
|
1076
|
-
console.log(
|
|
1077
|
-
console.log(
|
|
1078
|
-
console.log(
|
|
1079
|
-
console.log(
|
|
1080
|
-
console.log(
|
|
1081
|
-
console.log(
|
|
1242
|
+
console.log(pc7.dim("\n Local update complete.\n"));
|
|
1243
|
+
console.log(pc7.dim(" Next steps:"));
|
|
1244
|
+
console.log(pc7.cyan(" 1. npx beech seed:load --local"));
|
|
1245
|
+
console.log(pc7.dim(" \u2192 sync content schema to local DB"));
|
|
1246
|
+
console.log(pc7.cyan(" 2. npm run deploy"));
|
|
1247
|
+
console.log(pc7.dim(" \u2192 deploy updated API + dashboard"));
|
|
1248
|
+
console.log(pc7.cyan(" 3. npx beech seed:load"));
|
|
1249
|
+
console.log(pc7.dim(" \u2192 sync remote schema\n"));
|
|
1082
1250
|
}
|
|
1083
1251
|
export {
|
|
1084
1252
|
deploy,
|
|
1085
1253
|
init,
|
|
1254
|
+
onboard,
|
|
1086
1255
|
seedCreate,
|
|
1087
1256
|
seedLoad,
|
|
1088
1257
|
update,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Seed } from '@beech/core';
|
|
2
|
+
import type { WranglerOptions } from './wrangler.js';
|
|
3
|
+
export interface ColumnDiff {
|
|
4
|
+
name: string;
|
|
5
|
+
status: 'ok' | 'missing' | 'extra' | 'type_mismatch';
|
|
6
|
+
expectedType?: string;
|
|
7
|
+
actualType?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SeedDiff {
|
|
10
|
+
slug: string;
|
|
11
|
+
tableExists: boolean;
|
|
12
|
+
columns: ColumnDiff[];
|
|
13
|
+
}
|
|
14
|
+
export declare function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff>;
|
|
15
|
+
//# sourceMappingURL=schema-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-diff.d.ts","sourceRoot":"","sources":["../../src/lib/schema-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAS,MAAM,eAAe,CAAA;AAU3D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,CAAA;IACpD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,UAAU,EAAE,CAAA;CACtB;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsCtF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getExpectedColumns } from '@beech/core';
|
|
2
|
+
import { queryD1 } from './wrangler.js';
|
|
3
|
+
export async function diffSeed(seed, options) {
|
|
4
|
+
const tableName = `content_${seed.slug}`;
|
|
5
|
+
const expected = getExpectedColumns(seed);
|
|
6
|
+
let actual;
|
|
7
|
+
try {
|
|
8
|
+
actual = queryD1(`PRAGMA table_info(${tableName})`, options);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) };
|
|
12
|
+
}
|
|
13
|
+
if (actual.length === 0) {
|
|
14
|
+
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) };
|
|
15
|
+
}
|
|
16
|
+
const actualMap = new Map(actual.map(r => [r.name, r]));
|
|
17
|
+
const expectedSet = new Set(expected.map(c => c.name));
|
|
18
|
+
const columns = [];
|
|
19
|
+
for (const col of expected) {
|
|
20
|
+
const actual = actualMap.get(col.name);
|
|
21
|
+
if (!actual) {
|
|
22
|
+
columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType });
|
|
23
|
+
}
|
|
24
|
+
else if (actual.type.toUpperCase() !== col.sqlType) {
|
|
25
|
+
columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actual.type });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
columns.push({ name: col.name, status: 'ok' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const row of actual) {
|
|
32
|
+
if (!expectedSet.has(row.name)) {
|
|
33
|
+
columns.push({ name: row.name, status: 'extra', actualType: row.type });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { slug: seed.slug, tableExists: true, columns };
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface WranglerOptions {
|
|
2
|
+
db: string;
|
|
3
|
+
local: boolean;
|
|
4
|
+
configPath: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface D1Row {
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. */
|
|
10
|
+
export declare function executeD1File(sql: string, options: WranglerOptions): void;
|
|
11
|
+
/** Esegue una query SQL e ritorna i risultati come array di oggetti (--json). */
|
|
12
|
+
export declare function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[];
|
|
13
|
+
/** Trova il path di wrangler.jsonc in apps/api/ relativo a cwd. */
|
|
14
|
+
export declare function findWranglerConfig(): string | null;
|
|
15
|
+
/** Risolve il nome del database D1 da wrangler.jsonc (grepping 'database_name'). */
|
|
16
|
+
export declare function resolveDbName(configPath: string | null): string;
|
|
17
|
+
//# sourceMappingURL=wrangler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrangler.d.ts","sourceRoot":"","sources":["../../src/lib/wrangler.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAgBD,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAYzE;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,EAAE,CAc3F;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAGlD;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAS/D"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
function buildArgs(options) {
|
|
6
|
+
const args = [];
|
|
7
|
+
if (options.configPath)
|
|
8
|
+
args.push('--config', options.configPath);
|
|
9
|
+
if (options.local)
|
|
10
|
+
args.push('--local');
|
|
11
|
+
else
|
|
12
|
+
args.push('--remote');
|
|
13
|
+
return args;
|
|
14
|
+
}
|
|
15
|
+
/** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. */
|
|
16
|
+
export function executeD1File(sql, options) {
|
|
17
|
+
const tmpFile = join(tmpdir(), `beech-seed-${Date.now()}.sql`);
|
|
18
|
+
try {
|
|
19
|
+
writeFileSync(tmpFile, sql, 'utf-8');
|
|
20
|
+
const args = ['d1', 'execute', options.db, '--file', tmpFile, ...buildArgs(options)];
|
|
21
|
+
const result = spawnSync('npx', ['wrangler', ...args], { stdio: 'inherit', cwd: process.cwd() });
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
process.exit(result.status ?? 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
try {
|
|
28
|
+
rmSync(tmpFile);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Esegue una query SQL e ritorna i risultati come array di oggetti (--json). */
|
|
34
|
+
export function queryD1(sql, options) {
|
|
35
|
+
const args = ['d1', 'execute', options.db, '--command', sql, '--json', ...buildArgs(options)];
|
|
36
|
+
const result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd() });
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
throw new Error(`wrangler d1 execute failed:\n${result.stderr}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(result.stdout);
|
|
42
|
+
return (parsed[0]?.results ?? []);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(`Failed to parse wrangler JSON output:\n${result.stdout}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Trova il path di wrangler.jsonc in apps/api/ relativo a cwd. */
|
|
49
|
+
export function findWranglerConfig() {
|
|
50
|
+
const candidate = resolve(process.cwd(), 'apps', 'api', 'wrangler.jsonc');
|
|
51
|
+
return existsSync(candidate) ? candidate : null;
|
|
52
|
+
}
|
|
53
|
+
/** Risolve il nome del database D1 da wrangler.jsonc (grepping 'database_name'). */
|
|
54
|
+
export function resolveDbName(configPath) {
|
|
55
|
+
if (!configPath)
|
|
56
|
+
return 'beech-db';
|
|
57
|
+
try {
|
|
58
|
+
const content = execSync(`cat "${configPath}"`, { encoding: 'utf-8' });
|
|
59
|
+
const match = content.match(/"database_name"\s*:\s*"([^"]+)"/);
|
|
60
|
+
return match?.[1] ?? 'beech-db';
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 'beech-db';
|
|
64
|
+
}
|
|
65
|
+
}
|