@flaredev/cli 0.3.0 → 0.4.1
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/dist/index.js +498 -220
- package/package.json +2 -2
- package/templates/app/app/dashboard/account/layout.tsx +30 -0
- package/templates/app/app/dashboard/account/page.tsx +11 -33
- package/templates/app/app/dashboard/account/password/page.tsx +22 -0
- package/templates/app/app/dashboard/account/security/page.tsx +34 -0
- package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
- package/templates/app/app/dashboard/actions.ts +73 -5
- package/templates/app/app/dashboard/layout.tsx +28 -4
- package/templates/app/app/dashboard/observability/page.tsx +4 -3
- package/templates/app/app/dashboard/page.tsx +2 -2
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/components/account/account-nav.tsx +39 -0
- package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -522
- package/templates/app/components/dashboard/column-menu.tsx +66 -0
- package/templates/app/components/dashboard/dashboard-header.tsx +115 -78
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +86 -24
- package/templates/app/components/dashboard/dropzone.tsx +92 -0
- package/templates/app/components/dashboard/editable.ts +12 -0
- package/templates/app/components/dashboard/fields/file-field.tsx +310 -302
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +62 -9
- package/templates/app/components/dashboard/inline-cell.tsx +165 -0
- package/templates/app/components/dashboard/local-time.tsx +77 -0
- package/templates/app/components/dashboard/query.ts +2 -0
- package/templates/app/components/dashboard/record-actions.tsx +113 -0
- package/templates/app/components/dashboard/record-detail.tsx +203 -0
- package/templates/app/components/dashboard/resource-chart.tsx +91 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +3 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form.tsx +165 -165
- package/templates/app/components/dashboard/resource-stats.tsx +6 -11
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +408 -331
- package/templates/app/components/dashboard/row-actions.tsx +156 -144
- package/templates/app/components/dashboard/save-view-button.tsx +86 -0
- package/templates/app/components/dashboard/user-menu.tsx +91 -0
- package/templates/app/components/ui/sonner.tsx +33 -13
- package/templates/app/db/flare-schema.ts +22 -0
- package/templates/app/lib/api.ts +12 -0
- package/templates/app/lib/dashboard.ts +165 -138
- package/templates/app/lib/views.ts +29 -0
- package/templates/app/migrations/0000_init.sql +11 -0
- package/templates/app/migrations/meta/0000_snapshot.json +63 -1
- package/templates/app/vite.config.ts +41 -31
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- package/templates/locks/yarn.lock +3548 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import pc24 from "picocolors";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { cac } from "cac";
|
|
6
|
+
import pc23 from "picocolors";
|
|
6
7
|
import * as prompts3 from "@clack/prompts";
|
|
7
8
|
import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
|
|
8
9
|
|
|
@@ -138,6 +139,7 @@ function parseTheme(value) {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// src/versions.ts
|
|
142
|
+
var COMPATIBILITY_DATE = "2026-09-15";
|
|
141
143
|
var APP_DEPENDENCIES = {
|
|
142
144
|
"@better-auth/drizzle-adapter": "1.7.5",
|
|
143
145
|
"@better-auth/passkey": "1.7.5",
|
|
@@ -185,43 +187,6 @@ var APP_DEV_DEPENDENCIES = {
|
|
|
185
187
|
wrangler: "^4.132.0"
|
|
186
188
|
};
|
|
187
189
|
|
|
188
|
-
// src/terminal.ts
|
|
189
|
-
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
190
|
-
function formatDuration(ms) {
|
|
191
|
-
const seconds = Math.round(ms / 1e3);
|
|
192
|
-
if (seconds < 60) return `${seconds}s`;
|
|
193
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
194
|
-
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
195
|
-
}
|
|
196
|
-
var formatCount = (value) => value.toLocaleString();
|
|
197
|
-
function formatRate(count, ms, unit = "rows") {
|
|
198
|
-
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
199
|
-
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
200
|
-
}
|
|
201
|
-
function progressLine(everyMs = 120) {
|
|
202
|
-
if (!isTTY()) return { update: () => {
|
|
203
|
-
}, done: () => {
|
|
204
|
-
} };
|
|
205
|
-
let last = 0;
|
|
206
|
-
let width = 0;
|
|
207
|
-
const write = (message) => {
|
|
208
|
-
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
209
|
-
width = Math.max(width, message.length);
|
|
210
|
-
};
|
|
211
|
-
return {
|
|
212
|
-
update(message) {
|
|
213
|
-
const now = Date.now();
|
|
214
|
-
if (now - last < everyMs) return;
|
|
215
|
-
last = now;
|
|
216
|
-
write(message);
|
|
217
|
-
},
|
|
218
|
-
done() {
|
|
219
|
-
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
220
|
-
width = 0;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
190
|
// src/utils/fs.ts
|
|
226
191
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
227
192
|
import { dirname, join, resolve } from "path";
|
|
@@ -281,11 +246,6 @@ function childEnv(env = process.env) {
|
|
|
281
246
|
}
|
|
282
247
|
return next;
|
|
283
248
|
}
|
|
284
|
-
function runQuiet(command, args, cwd) {
|
|
285
|
-
const env = childEnv();
|
|
286
|
-
const result = process.platform === "win32" ? spawnSync([command, ...args].join(" "), { cwd, env, shell: true, encoding: "utf8" }) : spawnSync(command, args, { cwd, env, encoding: "utf8" });
|
|
287
|
-
return { code: result.status ?? 1, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
|
288
|
-
}
|
|
289
249
|
function installArgs(pm) {
|
|
290
250
|
if (pm === "npm") return ["install", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
291
251
|
return ["install"];
|
|
@@ -321,7 +281,7 @@ function createApp(target, options2 = {}) {
|
|
|
321
281
|
const authMethods = parseAuthMethods(options2.auth);
|
|
322
282
|
const theme = parseTheme(options2.theme);
|
|
323
283
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
324
|
-
const compatibilityDate = options2.compatibilityDate ??
|
|
284
|
+
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
325
285
|
const files = copyTemplate(join2(templatesDir, "app"), dir, {
|
|
326
286
|
APP_NAME: name,
|
|
327
287
|
COMPAT_DATE: compatibilityDate,
|
|
@@ -361,16 +321,10 @@ ${devVarsEntries(authProviders)}`
|
|
|
361
321
|
if (pinned) copyLockfile(packageManager, dir);
|
|
362
322
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
363
323
|
log2(`${pc.green("\u2714")} Wrote ${files} files to ${relative(process.cwd(), dir) || "."}`);
|
|
364
|
-
if (options2.install !== false) {
|
|
365
|
-
const started = Date.now();
|
|
366
|
-
const code = (options2.installer ?? run)(packageManager, installArgs(packageManager), dir);
|
|
367
|
-
if (code !== 0) throw new Error(`${packageManager} install failed (exit code ${code}).`);
|
|
368
|
-
if (!options2.installer) log2(`${pc.green("\u2714")} Installed dependencies ${pc.dim(`(${formatDuration(Date.now() - started)})`)}`);
|
|
369
|
-
}
|
|
370
324
|
return { dir, name, packageManager, inWorkspace };
|
|
371
325
|
}
|
|
372
326
|
function copyLockfile(packageManager, dir) {
|
|
373
|
-
const file = packageManager === "
|
|
327
|
+
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
374
328
|
if (!file) return false;
|
|
375
329
|
const source = join2(templatesDir, "locks", file);
|
|
376
330
|
if (!existsSync2(source)) return false;
|
|
@@ -426,6 +380,20 @@ function splitTopLevel(input) {
|
|
|
426
380
|
parts.push(current);
|
|
427
381
|
return parts.map((part) => part.trim()).filter(Boolean);
|
|
428
382
|
}
|
|
383
|
+
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
384
|
+
var SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
|
|
385
|
+
function parseSize(text, spec) {
|
|
386
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i.exec(text.trim());
|
|
387
|
+
if (!match) {
|
|
388
|
+
throw new FieldGrammarError(`"${spec}": "${text}" isn't a size. Write it like "file:[image]:5mb".`);
|
|
389
|
+
}
|
|
390
|
+
const bytes = Math.round(Number(match[1]) * SIZE_UNITS[match[2].toLowerCase()]);
|
|
391
|
+
if (bytes < 1) throw new FieldGrammarError(`"${spec}": a size limit has to be at least one byte.`);
|
|
392
|
+
if (bytes > MAX_UPLOAD_BYTES) {
|
|
393
|
+
throw new FieldGrammarError(`"${spec}": ${text} is more than a Worker will carry in one request; the most is 100mb.`);
|
|
394
|
+
}
|
|
395
|
+
return bytes;
|
|
396
|
+
}
|
|
429
397
|
function editDistance(a, b) {
|
|
430
398
|
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
431
399
|
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
@@ -486,11 +454,14 @@ function parseField(spec) {
|
|
|
486
454
|
}
|
|
487
455
|
if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
|
|
488
456
|
field.options = options2;
|
|
489
|
-
} else if (
|
|
457
|
+
} else if (/^file\[/.test(type)) {
|
|
458
|
+
throw new FieldGrammarError(`"${spec}": file fields need a colon before the list, e.g. "${field.key}:file:[${type.slice(5, -1) || "image"}]".`);
|
|
459
|
+
} else if (match = /^file(?::\[(.*)\])?(?::([^:]*))?$/.exec(type)) {
|
|
490
460
|
field.kind = "file";
|
|
491
461
|
if (match[1] === void 0) {
|
|
492
462
|
throw new FieldGrammarError(`"${spec}": file fields list accepted types, e.g. "file:[image,pdf]".`);
|
|
493
463
|
}
|
|
464
|
+
if (match[2] !== void 0) field.maxBytes = parseSize(match[2], spec);
|
|
494
465
|
const categories = Object.keys(FILE_CATEGORIES);
|
|
495
466
|
const accept = list(match[1], "file type", spec).map((category) => category.toLowerCase());
|
|
496
467
|
for (const category of accept) {
|
|
@@ -639,6 +610,7 @@ function options(field) {
|
|
|
639
610
|
if (field.format) opts.format = field.format;
|
|
640
611
|
if (field.widget) opts.widget = field.widget;
|
|
641
612
|
if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
|
|
613
|
+
if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
|
|
642
614
|
return opts;
|
|
643
615
|
}
|
|
644
616
|
function toField(field) {
|
|
@@ -685,15 +657,21 @@ function renderFieldsBlock(fields) {
|
|
|
685
657
|
return fields.map((f) => `${renderField(f)}
|
|
686
658
|
`).join("");
|
|
687
659
|
}
|
|
688
|
-
function renderDescriptor(name, fields) {
|
|
660
|
+
function renderDescriptor(name, fields, options2 = {}) {
|
|
689
661
|
defineResource({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
|
|
690
662
|
const block = renderFieldsBlock(fields);
|
|
663
|
+
const heading = [
|
|
664
|
+
options2.icon ? ` icon: ${literal(options2.icon)},
|
|
665
|
+
` : "",
|
|
666
|
+
options2.group ? ` group: ${literal(options2.group)},
|
|
667
|
+
` : ""
|
|
668
|
+
].join("");
|
|
691
669
|
return joinMarkers({
|
|
692
670
|
before: `import { defineResource, field } from "@flaredev/core";
|
|
693
671
|
|
|
694
672
|
export default defineResource({
|
|
695
673
|
name: ${literal(name)},
|
|
696
|
-
fields: {
|
|
674
|
+
${heading} fields: {
|
|
697
675
|
`,
|
|
698
676
|
block,
|
|
699
677
|
after: ` },
|
|
@@ -1375,7 +1353,7 @@ async function loadResources(root) {
|
|
|
1375
1353
|
const appRoot = resolve3(root);
|
|
1376
1354
|
const dir = join3(appRoot, "resources");
|
|
1377
1355
|
if (!existsSync4(dir)) return [];
|
|
1378
|
-
const jiti = createJiti(join3(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1356
|
+
const jiti = createJiti(join3(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1379
1357
|
const loaded = [];
|
|
1380
1358
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1381
1359
|
const path = join3(dir, file);
|
|
@@ -1575,6 +1553,7 @@ function parseDeployArgs(args) {
|
|
|
1575
1553
|
if (arg === "--skip-migrations") skipMigrations = true;
|
|
1576
1554
|
else if (arg === "--skip-secrets") skipSecrets = true;
|
|
1577
1555
|
else if (arg === "--skip-security") skipSecurity = true;
|
|
1556
|
+
else if (arg === "--yes" || arg === "-y") continue;
|
|
1578
1557
|
else {
|
|
1579
1558
|
forwarded.push(arg);
|
|
1580
1559
|
if (arg === "--env" && args[i + 1]) env = args[i + 1];
|
|
@@ -2007,13 +1986,24 @@ function renderTableModule(entry, all) {
|
|
|
2007
1986
|
const name = tableExport(resource);
|
|
2008
1987
|
const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
|
|
2009
1988
|
const extras = [];
|
|
1989
|
+
const indexed = /* @__PURE__ */ new Set();
|
|
1990
|
+
const addIndex = (key, column) => {
|
|
1991
|
+
if (indexed.has(key)) return;
|
|
1992
|
+
indexed.add(key);
|
|
1993
|
+
extras.push(` index(${q(`${resource.table}_${columnName(column)}_idx`)}).on(table.${key}),`);
|
|
1994
|
+
};
|
|
2010
1995
|
for (const [key, def] of fields) {
|
|
2011
1996
|
if (def.kind === "enum") {
|
|
2012
1997
|
const options2 = def.options.map(sqlString).join(", ");
|
|
2013
1998
|
extras.push(` check(${q(`${resource.table}_${columnName(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
|
|
1999
|
+
if (def.filterable !== false) addIndex(key, key);
|
|
2014
2000
|
}
|
|
2015
|
-
if (def.kind === "belongsTo")
|
|
2001
|
+
if (def.kind === "belongsTo") addIndex(key, key);
|
|
2016
2002
|
}
|
|
2003
|
+
const sortKey = resource.defaultSort.field;
|
|
2004
|
+
if (sortKey === "createdAt" || sortKey === "updatedAt") addIndex(sortKey, sortKey === "createdAt" ? "created_at" : "updated_at");
|
|
2005
|
+
else if (fields.some(([key]) => key === sortKey)) addIndex(sortKey, sortKey);
|
|
2006
|
+
addIndex("createdAt", "created_at");
|
|
2017
2007
|
const targets = [...new Set(fields.flatMap(([, def]) => def.kind === "belongsTo" ? [resources.get(def.target)] : []))].filter((target) => target.name !== resource.name).sort((a, b) => a.table.localeCompare(b.table));
|
|
2018
2008
|
const coreImports = ["integer", "sqliteTable", "text"];
|
|
2019
2009
|
if (fields.some(([, def]) => def.kind === "float")) coreImports.push("real");
|
|
@@ -2089,7 +2079,7 @@ var handlerImports = (entry) => [
|
|
|
2089
2079
|
`import { createResourceHandlers } from "@flaredev/core/server";`,
|
|
2090
2080
|
`import { getDb } from "@/db";`,
|
|
2091
2081
|
`import { ${tableExport(entry.resource)} } from "@/db/schema";`,
|
|
2092
|
-
`import { authorize } from "@/lib/api";`,
|
|
2082
|
+
`import { authorize, currentUser } from "@/lib/api";`,
|
|
2093
2083
|
`import { revalidateResource } from "@/lib/cache";`,
|
|
2094
2084
|
`import ${resourceLocal(entry.stem)} from "@/resources/${entry.stem}.resource";`,
|
|
2095
2085
|
"",
|
|
@@ -2098,6 +2088,7 @@ var handlerImports = (entry) => [
|
|
|
2098
2088
|
` table: ${tableExport(entry.resource)},`,
|
|
2099
2089
|
` getDb,`,
|
|
2100
2090
|
` authorize,`,
|
|
2091
|
+
` currentUser,`,
|
|
2101
2092
|
` onChange: revalidateResource,`,
|
|
2102
2093
|
`});`,
|
|
2103
2094
|
""
|
|
@@ -2124,6 +2115,7 @@ function renderAdminListPage(entry) {
|
|
|
2124
2115
|
const local = resourceLocal(entry.stem);
|
|
2125
2116
|
return [
|
|
2126
2117
|
`import { PageHeader } from "@/components/dashboard/page-header";`,
|
|
2118
|
+
`import { ResourceChart } from "@/components/dashboard/resource-chart";`,
|
|
2127
2119
|
`import { ResourceStats } from "@/components/dashboard/resource-stats";`,
|
|
2128
2120
|
`import { ResourceTable } from "@/components/dashboard/resource-table";`,
|
|
2129
2121
|
`import type { SearchParams } from "@/components/dashboard/query";`,
|
|
@@ -2139,6 +2131,7 @@ function renderAdminListPage(entry) {
|
|
|
2139
2131
|
` crumbs={[{ label: "Dashboard", href: "/dashboard" }, { label: ${local}.pluralLabel }]}`,
|
|
2140
2132
|
" />",
|
|
2141
2133
|
` <ResourceStats resource={${local}} />`,
|
|
2134
|
+
` <ResourceChart resource={${local}} />`,
|
|
2142
2135
|
` <ResourceTable resource={${local}} searchParams={await searchParams} />`,
|
|
2143
2136
|
" </>",
|
|
2144
2137
|
" );",
|
|
@@ -2146,6 +2139,17 @@ function renderAdminListPage(entry) {
|
|
|
2146
2139
|
""
|
|
2147
2140
|
].join("\n");
|
|
2148
2141
|
}
|
|
2142
|
+
function renderAdminDetailPage(entry) {
|
|
2143
|
+
return [
|
|
2144
|
+
`import { RecordDetail } from "@/components/dashboard/record-detail";`,
|
|
2145
|
+
...adminImports(entry),
|
|
2146
|
+
"",
|
|
2147
|
+
`export default async function ${entry.resource.name}DetailPage({ params }: { params: Promise<{ id: string }> }) {`,
|
|
2148
|
+
` return <RecordDetail resource={${resourceLocal(entry.stem)}} id={(await params).id} />;`,
|
|
2149
|
+
"}",
|
|
2150
|
+
""
|
|
2151
|
+
].join("\n");
|
|
2152
|
+
}
|
|
2149
2153
|
function renderAdminNewPage(entry) {
|
|
2150
2154
|
return [
|
|
2151
2155
|
`import { ResourceFormPage } from "@/components/dashboard/resource-form-page";`,
|
|
@@ -2228,6 +2232,7 @@ function resourceFiles(entry, all) {
|
|
|
2228
2232
|
{ path: `resources/${stem}.client.ts`, content: renderClient(entry) },
|
|
2229
2233
|
{ path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
|
|
2230
2234
|
{ path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
|
|
2235
|
+
{ path: `app/dashboard/${resource.slug}/[id]/page.tsx`, content: renderAdminDetailPage(entry) },
|
|
2231
2236
|
{ path: `app/dashboard/${resource.slug}/new/page.tsx`, content: renderAdminNewPage(entry) },
|
|
2232
2237
|
{ path: `app/dashboard/${resource.slug}/[id]/edit/page.tsx`, content: renderAdminEditPage(entry) }
|
|
2233
2238
|
];
|
|
@@ -2290,7 +2295,7 @@ function findOrphans(appRoot, all) {
|
|
|
2290
2295
|
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync11(join10(appRoot, file))) candidates.push(file);
|
|
2291
2296
|
}
|
|
2292
2297
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2293
|
-
for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
|
|
2298
|
+
for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
|
|
2294
2299
|
if (existsSync11(join10(appRoot, file))) candidates.push(file);
|
|
2295
2300
|
}
|
|
2296
2301
|
}
|
|
@@ -3168,7 +3173,7 @@ async function genResource(rawName, options2) {
|
|
|
3168
3173
|
validateFields(name, fields, others, log2);
|
|
3169
3174
|
if (!exists) {
|
|
3170
3175
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
3171
|
-
writeFileSync9(path, renderDescriptor(name, fields));
|
|
3176
|
+
writeFileSync9(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
|
|
3172
3177
|
log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
|
|
3173
3178
|
} else {
|
|
3174
3179
|
try {
|
|
@@ -3216,11 +3221,74 @@ Next: ${pc10.bold("flare migrate")} to apply it locally.`);
|
|
|
3216
3221
|
return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
|
|
3217
3222
|
}
|
|
3218
3223
|
|
|
3219
|
-
// src/commands/gen-
|
|
3224
|
+
// src/commands/gen-endpoint.ts
|
|
3220
3225
|
import { existsSync as existsSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
3221
|
-
import {
|
|
3222
|
-
import {
|
|
3226
|
+
import { join as join15 } from "path";
|
|
3227
|
+
import { kebabCase as kebabCase3, pascalCase as pascalCase2 } from "@flaredev/core";
|
|
3223
3228
|
import pc11 from "picocolors";
|
|
3229
|
+
var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
3230
|
+
var ACTIONS = ["read", "create", "update", "delete"];
|
|
3231
|
+
async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
3232
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3233
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3234
|
+
const method = (options2.method ?? "GET").toUpperCase();
|
|
3235
|
+
if (!METHODS.includes(method)) throw new Error(`Unknown method "${method}". Use one of ${METHODS.join(", ")}.`);
|
|
3236
|
+
const name = kebabCase3(rawName);
|
|
3237
|
+
if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
|
|
3238
|
+
const resources = await loadResources(appRoot);
|
|
3239
|
+
const wanted = pascalCase2(resourceName2);
|
|
3240
|
+
const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
|
|
3241
|
+
if (!entry) {
|
|
3242
|
+
throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
|
|
3243
|
+
}
|
|
3244
|
+
const action = options2.action ?? (method === "GET" ? "read" : "update");
|
|
3245
|
+
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3246
|
+
const { resource, stem } = entry;
|
|
3247
|
+
const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
|
|
3248
|
+
const path = join15(appRoot, relative2);
|
|
3249
|
+
if (existsSync16(path)) throw new Error(`${relative2} already exists.`);
|
|
3250
|
+
mkdirSync8(join15(path, ".."), { recursive: true });
|
|
3251
|
+
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3252
|
+
writeFileSync10(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3253
|
+
log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
|
|
3254
|
+
log2(`
|
|
3255
|
+
It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
|
|
3256
|
+
return relative2;
|
|
3257
|
+
}
|
|
3258
|
+
function renderEndpoint({ resource, stem, method, action, record, route }) {
|
|
3259
|
+
const body = method === "GET" || method === "DELETE" ? "" : " const body = (await request.json()) as Record<string, unknown>;\n";
|
|
3260
|
+
const params = record ? ", { params }: { params: Promise<{ id: string }> }" : "";
|
|
3261
|
+
const id = record ? " const { id } = await params;\n" : "";
|
|
3262
|
+
return `import { authorize, currentUser } from "@/lib/api";
|
|
3263
|
+
import { dashboardStore } from "@/lib/dashboard";
|
|
3264
|
+
import ${stem.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())}Resource from "@/resources/${stem}.resource";
|
|
3265
|
+
|
|
3266
|
+
/**
|
|
3267
|
+
* ${method} ${route}
|
|
3268
|
+
*
|
|
3269
|
+
* Yours to change. The session and the policy are checked the same way the generated
|
|
3270
|
+
* CRUD routes check them, and \`store\` is the same store they use \u2014 so hooks, validation
|
|
3271
|
+
* and cache invalidation all still apply.
|
|
3272
|
+
*/
|
|
3273
|
+
export async function ${method}(request: Request${params}) {
|
|
3274
|
+
${id} const denied = await authorize({ request, resource: ${stem.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())}Resource, action: "${action}" });
|
|
3275
|
+
if (denied) return denied;
|
|
3276
|
+
|
|
3277
|
+
const user = await currentUser();
|
|
3278
|
+
const store = dashboardStore("${resource}");
|
|
3279
|
+
${body}
|
|
3280
|
+
// Your logic here. \`store.list\`, \`store.get\`, \`store.create\`, \`store.update\` and
|
|
3281
|
+
// \`store.delete\` are available, or reach for Drizzle directly with \`getDb()\`.
|
|
3282
|
+
return Response.json({ ok: true, ${record ? "id, " : ""}${method === "GET" || method === "DELETE" ? "" : "received: body, "}user: user?.email ?? null });
|
|
3283
|
+
}
|
|
3284
|
+
`;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// src/commands/gen-migration.ts
|
|
3288
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
3289
|
+
import { basename as basename2, join as join16 } from "path";
|
|
3290
|
+
import { snakeCase } from "@flaredev/core";
|
|
3291
|
+
import pc12 from "picocolors";
|
|
3224
3292
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
3225
3293
|
-- Write SQL that undoes the up migration, e.g.:
|
|
3226
3294
|
-- DROP INDEX IF EXISTS \`contacts_phone_idx\`;
|
|
@@ -3236,31 +3304,31 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
3236
3304
|
log2(options2.fromSchema ? "No schema changes, so no migration was created." : "drizzle-kit didn't create a migration.");
|
|
3237
3305
|
return [];
|
|
3238
3306
|
}
|
|
3239
|
-
for (const file of files) log2(`${
|
|
3240
|
-
for (const repair of repairs) log2(
|
|
3307
|
+
for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
|
|
3308
|
+
for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
|
|
3241
3309
|
if (!options2.fromSchema) {
|
|
3242
|
-
const downDir =
|
|
3243
|
-
|
|
3310
|
+
const downDir = join16(appRoot, "migrations", "down");
|
|
3311
|
+
mkdirSync9(downDir, { recursive: true });
|
|
3244
3312
|
for (const file of files) {
|
|
3245
|
-
const down =
|
|
3246
|
-
if (
|
|
3247
|
-
|
|
3248
|
-
log2(`${
|
|
3313
|
+
const down = join16(downDir, basename2(file));
|
|
3314
|
+
if (existsSync17(down)) continue;
|
|
3315
|
+
writeFileSync11(down, DOWN_TEMPLATE(basename2(file)));
|
|
3316
|
+
log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3249
3317
|
}
|
|
3250
3318
|
log2(`
|
|
3251
|
-
Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${
|
|
3319
|
+
Write your SQL in ${files[0]} (and its rollback in migrations/down/), then run ${pc12.bold("flare migrate")}.`);
|
|
3252
3320
|
} else {
|
|
3253
3321
|
log2(`
|
|
3254
|
-
Next: ${
|
|
3322
|
+
Next: ${pc12.bold("flare migrate")}.`);
|
|
3255
3323
|
}
|
|
3256
3324
|
return files;
|
|
3257
3325
|
}
|
|
3258
3326
|
|
|
3259
3327
|
// src/commands/gen-policy.ts
|
|
3260
|
-
import { existsSync as
|
|
3261
|
-
import { dirname as dirname9, join as
|
|
3328
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12 } from "fs";
|
|
3329
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
3262
3330
|
import { definePolicy } from "@flaredev/core";
|
|
3263
|
-
import
|
|
3331
|
+
import pc13 from "picocolors";
|
|
3264
3332
|
function parseRoles(input, what = "--roles") {
|
|
3265
3333
|
const roles = (input ?? "").split(",").map((role) => role.trim()).filter(Boolean);
|
|
3266
3334
|
if (roles.length === 0) throw new Error(`${what} is required, e.g. ${what} admin,staff`);
|
|
@@ -3276,8 +3344,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3276
3344
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3277
3345
|
}
|
|
3278
3346
|
const relative2 = policyPath(name);
|
|
3279
|
-
const path =
|
|
3280
|
-
const exists =
|
|
3347
|
+
const path = join17(appRoot, relative2);
|
|
3348
|
+
const exists = existsSync18(path);
|
|
3281
3349
|
let roles;
|
|
3282
3350
|
let deleteRoles;
|
|
3283
3351
|
if (options2.roles || !exists) {
|
|
@@ -3288,13 +3356,13 @@ async function genPolicy(rawName, options2) {
|
|
|
3288
3356
|
if (roles && deleteRoles) {
|
|
3289
3357
|
const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
|
|
3290
3358
|
if (!exists) {
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
log2(`${
|
|
3359
|
+
mkdirSync10(dirname9(path), { recursive: true });
|
|
3360
|
+
writeFileSync12(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3361
|
+
log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
|
|
3294
3362
|
} else {
|
|
3295
3363
|
try {
|
|
3296
3364
|
const outcome = writeGenerated(path, block, { force: options2.force });
|
|
3297
|
-
log2(`${(outcome === "identical" ?
|
|
3365
|
+
log2(`${(outcome === "identical" ? pc13.dim : pc13.yellow)(outcome.padEnd(9))} ${relative2}`);
|
|
3298
3366
|
} catch (error) {
|
|
3299
3367
|
if (error instanceof DriftError) {
|
|
3300
3368
|
throw new Error(`The roles in ${relative2} were edited by hand, so --roles won't overwrite them. Edit the file directly, or pass --force.`);
|
|
@@ -3306,15 +3374,15 @@ async function genPolicy(rawName, options2) {
|
|
|
3306
3374
|
const policies = await loadPolicies(appRoot);
|
|
3307
3375
|
logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
|
|
3308
3376
|
log2(`
|
|
3309
|
-
${
|
|
3377
|
+
${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3310
3378
|
return { name, path: relative2 };
|
|
3311
3379
|
}
|
|
3312
3380
|
|
|
3313
3381
|
// src/commands/migrate.ts
|
|
3314
|
-
import { existsSync as
|
|
3382
|
+
import { existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync13 } from "fs";
|
|
3315
3383
|
import { tmpdir } from "os";
|
|
3316
|
-
import { join as
|
|
3317
|
-
import
|
|
3384
|
+
import { join as join18 } from "path";
|
|
3385
|
+
import pc14 from "picocolors";
|
|
3318
3386
|
|
|
3319
3387
|
// src/generator/down.ts
|
|
3320
3388
|
var IDENT = '(`[^`]+`|"[^"]+"|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
|
|
@@ -3431,7 +3499,7 @@ async function rollback(options2 = {}) {
|
|
|
3431
3499
|
const steps = options2.steps ?? 1;
|
|
3432
3500
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3433
3501
|
const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
|
|
3434
|
-
const migrationsDir =
|
|
3502
|
+
const migrationsDir = join18(appRoot, db.migrations_dir ?? "migrations");
|
|
3435
3503
|
const applied = await appliedMigrations(options2, steps);
|
|
3436
3504
|
if (applied.length === 0) {
|
|
3437
3505
|
log2("Nothing to roll back.");
|
|
@@ -3440,8 +3508,8 @@ async function rollback(options2 = {}) {
|
|
|
3440
3508
|
const plan = [];
|
|
3441
3509
|
for (const { name } of applied) {
|
|
3442
3510
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3443
|
-
const handWritten =
|
|
3444
|
-
if (
|
|
3511
|
+
const handWritten = join18(migrationsDir, "down", name);
|
|
3512
|
+
if (existsSync19(handWritten)) {
|
|
3445
3513
|
const sql2 = readFileSync11(handWritten, "utf8");
|
|
3446
3514
|
if (splitStatements(sql2).length === 0) {
|
|
3447
3515
|
throw new Error(`Can't roll back ${name}: migrations/down/${name} has no SQL statements yet. Write the rollback SQL, then run the rollback again. Nothing was changed.`);
|
|
@@ -3449,8 +3517,8 @@ async function rollback(options2 = {}) {
|
|
|
3449
3517
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3450
3518
|
continue;
|
|
3451
3519
|
}
|
|
3452
|
-
const upPath =
|
|
3453
|
-
if (!
|
|
3520
|
+
const upPath = join18(migrationsDir, name);
|
|
3521
|
+
if (!existsSync19(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3454
3522
|
const down = deriveDown(readFileSync11(upPath, "utf8"));
|
|
3455
3523
|
if (!down.reversible) {
|
|
3456
3524
|
throw new Error(
|
|
@@ -3460,37 +3528,37 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3460
3528
|
}
|
|
3461
3529
|
plan.push({ name, sql: down.sql, source: "derived" });
|
|
3462
3530
|
}
|
|
3463
|
-
log2(`${
|
|
3531
|
+
log2(`${pc14.bold(`Rolling back ${plan.length} migration(s) on ${db.database_name} (${where}):`)}`);
|
|
3464
3532
|
for (const step of plan) {
|
|
3465
|
-
log2(` ${step.name} ${
|
|
3466
|
-
for (const line of step.sql.trim().split("\n")) log2(
|
|
3533
|
+
log2(` ${step.name} ${pc14.dim(`(${step.source})`)}`);
|
|
3534
|
+
for (const line of step.sql.trim().split("\n")) log2(pc14.dim(` ${line}`));
|
|
3467
3535
|
}
|
|
3468
3536
|
if (options2.remote && !options2.yes) {
|
|
3469
|
-
log2(
|
|
3537
|
+
log2(pc14.yellow("\nThis changes the remote database. Re-run with --yes to proceed."));
|
|
3470
3538
|
return 1;
|
|
3471
3539
|
}
|
|
3472
3540
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3473
3541
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3474
|
-
const dir = mkdtempSync(
|
|
3542
|
+
const dir = mkdtempSync(join18(tmpdir(), "flare-rollback-"));
|
|
3475
3543
|
try {
|
|
3476
|
-
const file =
|
|
3477
|
-
|
|
3544
|
+
const file = join18(dir, "rollback.sql");
|
|
3545
|
+
writeFileSync13(file, script);
|
|
3478
3546
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3479
3547
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
3480
3548
|
${result.output.trim()}`);
|
|
3481
3549
|
} finally {
|
|
3482
3550
|
rmSync2(dir, { recursive: true, force: true });
|
|
3483
3551
|
}
|
|
3484
|
-
log2(
|
|
3552
|
+
log2(pc14.green(`
|
|
3485
3553
|
Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
3486
3554
|
return 0;
|
|
3487
3555
|
}
|
|
3488
3556
|
|
|
3489
3557
|
// src/commands/rm.ts
|
|
3490
|
-
import { existsSync as
|
|
3491
|
-
import { dirname as dirname10, join as
|
|
3558
|
+
import { existsSync as existsSync20, readdirSync as readdirSync7, readFileSync as readFileSync12, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3559
|
+
import { dirname as dirname10, join as join19 } from "path";
|
|
3492
3560
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
3493
|
-
import
|
|
3561
|
+
import pc15 from "picocolors";
|
|
3494
3562
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
3495
3563
|
function handWrittenReason(source, header, options2 = {}) {
|
|
3496
3564
|
const parts = splitMarkers(source);
|
|
@@ -3507,10 +3575,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
3507
3575
|
return null;
|
|
3508
3576
|
}
|
|
3509
3577
|
function removeEmptyDirs(appRoot, start) {
|
|
3510
|
-
let dir = dirname10(
|
|
3511
|
-
const roots = [
|
|
3578
|
+
let dir = dirname10(join19(appRoot, start));
|
|
3579
|
+
const roots = [join19(appRoot, "app", "api"), join19(appRoot, "app", "dashboard")];
|
|
3512
3580
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
3513
|
-
while (inside() &&
|
|
3581
|
+
while (inside() && existsSync20(dir) && readdirSync7(dir).length === 0) {
|
|
3514
3582
|
rmdirSync(dir);
|
|
3515
3583
|
dir = dirname10(dir);
|
|
3516
3584
|
}
|
|
@@ -3539,7 +3607,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3539
3607
|
if (entry) {
|
|
3540
3608
|
owned.add(entry.file);
|
|
3541
3609
|
for (const file of planFiles(all)) {
|
|
3542
|
-
if (
|
|
3610
|
+
if (existsSync20(join19(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
3543
3611
|
}
|
|
3544
3612
|
}
|
|
3545
3613
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -3548,7 +3616,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3548
3616
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
3549
3617
|
const findings = [];
|
|
3550
3618
|
for (const path of owned) {
|
|
3551
|
-
const source = readFileSync12(
|
|
3619
|
+
const source = readFileSync12(join19(appRoot, path), "utf8");
|
|
3552
3620
|
let wrapper;
|
|
3553
3621
|
if (entry && path === entry.file) {
|
|
3554
3622
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -3569,34 +3637,34 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
3569
3637
|
}
|
|
3570
3638
|
const removed = [...owned].sort();
|
|
3571
3639
|
for (const path of removed) {
|
|
3572
|
-
unlinkSync2(
|
|
3640
|
+
unlinkSync2(join19(appRoot, path));
|
|
3573
3641
|
removeEmptyDirs(appRoot, path);
|
|
3574
|
-
log2(`${
|
|
3642
|
+
log2(`${pc15.red("remove".padEnd(9))} ${path}`);
|
|
3575
3643
|
}
|
|
3576
3644
|
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
|
|
3577
3645
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
3578
|
-
if (table &&
|
|
3579
|
-
const seeds = readdirSync7(
|
|
3580
|
-
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(
|
|
3646
|
+
if (table && existsSync20(join19(appRoot, "seeds"))) {
|
|
3647
|
+
const seeds = readdirSync7(join19(appRoot, "seeds")).filter(
|
|
3648
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync12(join19(appRoot, "seeds", file), "utf8"))
|
|
3581
3649
|
);
|
|
3582
|
-
if (seeds.length) log2(
|
|
3650
|
+
if (seeds.length) log2(pc15.yellow(`
|
|
3583
3651
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
3584
3652
|
}
|
|
3585
3653
|
let migrations = [];
|
|
3586
3654
|
if (!options2.skipMigration && entry) {
|
|
3587
3655
|
const result = await generateSchemaMigration(appRoot, `drop_${entry.resource.table}`);
|
|
3588
3656
|
migrations = result.files;
|
|
3589
|
-
for (const file of migrations) log2(`${
|
|
3590
|
-
for (const repair of result.repairs) log2(
|
|
3657
|
+
for (const file of migrations) log2(`${pc15.green("create".padEnd(9))} ${file}`);
|
|
3658
|
+
for (const repair of result.repairs) log2(pc15.dim(`repaired ${repair}`));
|
|
3591
3659
|
if (migrations.length) log2(`
|
|
3592
|
-
Next: ${
|
|
3660
|
+
Next: ${pc15.bold("flare migrate")} to drop the table locally.`);
|
|
3593
3661
|
}
|
|
3594
3662
|
return { removed, migrations };
|
|
3595
3663
|
}
|
|
3596
3664
|
|
|
3597
3665
|
// src/commands/role.ts
|
|
3598
3666
|
import { humanize } from "@flaredev/core";
|
|
3599
|
-
import
|
|
3667
|
+
import pc16 from "picocolors";
|
|
3600
3668
|
var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
3601
3669
|
function roleInsertSql(name, label) {
|
|
3602
3670
|
if (!ROLE_NAME.test(name)) throw new Error(`Invalid role "${name}". Use lowercase letters, digits, _ or -, e.g. admin or support-agent.`);
|
|
@@ -3641,17 +3709,17 @@ ${result.output.trim()}`);
|
|
|
3641
3709
|
}
|
|
3642
3710
|
const [first] = extractJson(result.output);
|
|
3643
3711
|
const added = first?.results?.[0];
|
|
3644
|
-
log2(
|
|
3645
|
-
log2(
|
|
3712
|
+
log2(pc16.green(`Role "${added?.name ?? name}" registered (${options2.remote ? "remote" : "local"}).`));
|
|
3713
|
+
log2(pc16.dim(`Assign it with: flare user:role <email> ${added?.name ?? name}${options2.remote ? " --remote" : ""}`));
|
|
3646
3714
|
return 0;
|
|
3647
3715
|
}
|
|
3648
3716
|
|
|
3649
3717
|
// src/commands/seed.ts
|
|
3650
|
-
import { existsSync as
|
|
3651
|
-
import { join as
|
|
3652
|
-
import { createFake, kebabCase as
|
|
3718
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync11, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "fs";
|
|
3719
|
+
import { join as join21 } from "path";
|
|
3720
|
+
import { createFake, kebabCase as kebabCase4, storedFields as storedFields2 } from "@flaredev/core";
|
|
3653
3721
|
import { createJiti as createJiti4 } from "jiti";
|
|
3654
|
-
import
|
|
3722
|
+
import pc17 from "picocolors";
|
|
3655
3723
|
|
|
3656
3724
|
// src/seed/bulk.ts
|
|
3657
3725
|
var MAX_STATEMENT_BYTES = 8e4;
|
|
@@ -3746,16 +3814,16 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
3746
3814
|
|
|
3747
3815
|
// src/seed/local-d1.ts
|
|
3748
3816
|
import { createRequire } from "module";
|
|
3749
|
-
import { join as
|
|
3817
|
+
import { join as join20 } from "path";
|
|
3750
3818
|
import { pathToFileURL } from "url";
|
|
3751
3819
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
3752
3820
|
async function openLocalD1(appRoot, binding) {
|
|
3753
3821
|
process.env.WRANGLER_LOG ??= "error";
|
|
3754
|
-
const appRequire = createRequire(
|
|
3822
|
+
const appRequire = createRequire(join20(appRoot, "package.json"));
|
|
3755
3823
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
3756
3824
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3757
|
-
configPath:
|
|
3758
|
-
persist: { path:
|
|
3825
|
+
configPath: join20(appRoot, "wrangler.jsonc"),
|
|
3826
|
+
persist: { path: join20(appRoot, LOCAL_STATE) }
|
|
3759
3827
|
});
|
|
3760
3828
|
const bindings = env;
|
|
3761
3829
|
const name = binding ?? "DB";
|
|
@@ -3770,8 +3838,8 @@ async function openLocalD1(appRoot, binding) {
|
|
|
3770
3838
|
// src/commands/seed.ts
|
|
3771
3839
|
var SEEDS_DIR = "seeds";
|
|
3772
3840
|
function seedFiles(appRoot, names) {
|
|
3773
|
-
const dir =
|
|
3774
|
-
const all =
|
|
3841
|
+
const dir = join21(appRoot, SEEDS_DIR);
|
|
3842
|
+
const all = existsSync21(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
3775
3843
|
if (!names?.length) return all;
|
|
3776
3844
|
return names.map((name) => {
|
|
3777
3845
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -3786,24 +3854,24 @@ async function runSeeds(options2 = {}) {
|
|
|
3786
3854
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3787
3855
|
const files = seedFiles(appRoot, options2.names);
|
|
3788
3856
|
if (files.length === 0) {
|
|
3789
|
-
log2(`No seeds found. Create one with ${
|
|
3857
|
+
log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
|
|
3790
3858
|
return;
|
|
3791
3859
|
}
|
|
3792
|
-
const jiti = createJiti4(
|
|
3860
|
+
const jiti = createJiti4(join21(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
3793
3861
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
3794
3862
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
3795
|
-
const schema = await jiti.import(
|
|
3863
|
+
const schema = await jiti.import(join21(appRoot, "db/schema.ts"));
|
|
3796
3864
|
const local = await openLocalD1(appRoot);
|
|
3797
3865
|
try {
|
|
3798
3866
|
const db = drizzle(local.db, { schema });
|
|
3799
3867
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
3800
3868
|
for (const file of files) {
|
|
3801
|
-
const mod = await jiti.import(
|
|
3869
|
+
const mod = await jiti.import(join21(appRoot, SEEDS_DIR, file));
|
|
3802
3870
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
3803
|
-
log2(`${
|
|
3871
|
+
log2(`${pc17.cyan("seed")} ${file}`);
|
|
3804
3872
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
3805
3873
|
}
|
|
3806
|
-
log2(
|
|
3874
|
+
log2(pc17.green(`
|
|
3807
3875
|
Ran ${files.length} seed(s) against the local database.`));
|
|
3808
3876
|
} finally {
|
|
3809
3877
|
await local.dispose();
|
|
@@ -3885,32 +3953,141 @@ ${[...lines, ...todos].join("\n")}
|
|
|
3885
3953
|
async function makeSeed(rawName, options2 = {}) {
|
|
3886
3954
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3887
3955
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3888
|
-
const name =
|
|
3956
|
+
const name = kebabCase4(rawName);
|
|
3889
3957
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
3890
3958
|
const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
3891
|
-
const path =
|
|
3892
|
-
if (
|
|
3959
|
+
const path = join21(appRoot, relative2);
|
|
3960
|
+
if (existsSync21(path)) throw new Error(`${relative2} already exists.`);
|
|
3893
3961
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
3894
3962
|
let resource;
|
|
3895
3963
|
if (options2.resource) {
|
|
3896
3964
|
resource = resources.find((r) => r.name.toLowerCase() === options2.resource.toLowerCase());
|
|
3897
3965
|
if (!resource) throw new Error(`No resource "${options2.resource}". Available: ${resources.map((r) => r.name).join(", ") || "none"}.`);
|
|
3898
3966
|
} else {
|
|
3899
|
-
resource = resources.find((r) => [
|
|
3967
|
+
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
3900
3968
|
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
log2(`${
|
|
3969
|
+
mkdirSync11(join21(appRoot, SEEDS_DIR), { recursive: true });
|
|
3970
|
+
writeFileSync14(path, renderSeed(resource));
|
|
3971
|
+
log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
|
|
3904
3972
|
log2(`
|
|
3905
|
-
Run it with ${
|
|
3973
|
+
Run it with ${pc17.bold(`flare seed ${name}`)}.`);
|
|
3906
3974
|
return relative2;
|
|
3907
3975
|
}
|
|
3908
3976
|
|
|
3909
|
-
// src/commands/
|
|
3910
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as
|
|
3977
|
+
// src/commands/db-push.ts
|
|
3978
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync15 } from "fs";
|
|
3911
3979
|
import { tmpdir as tmpdir2 } from "os";
|
|
3912
|
-
import { join as
|
|
3913
|
-
import
|
|
3980
|
+
import { join as join22 } from "path";
|
|
3981
|
+
import pc18 from "picocolors";
|
|
3982
|
+
|
|
3983
|
+
// src/terminal.ts
|
|
3984
|
+
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
3985
|
+
function formatDuration(ms) {
|
|
3986
|
+
const seconds = Math.round(ms / 1e3);
|
|
3987
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3988
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
3989
|
+
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
3990
|
+
}
|
|
3991
|
+
var formatCount = (value) => value.toLocaleString();
|
|
3992
|
+
function formatRate(count, ms, unit = "rows") {
|
|
3993
|
+
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
3994
|
+
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
3995
|
+
}
|
|
3996
|
+
function progressLine(everyMs = 120) {
|
|
3997
|
+
if (!isTTY()) return { update: () => {
|
|
3998
|
+
}, done: () => {
|
|
3999
|
+
} };
|
|
4000
|
+
let last = 0;
|
|
4001
|
+
let width = 0;
|
|
4002
|
+
const write = (message) => {
|
|
4003
|
+
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
4004
|
+
width = Math.max(width, message.length);
|
|
4005
|
+
};
|
|
4006
|
+
return {
|
|
4007
|
+
update(message) {
|
|
4008
|
+
const now = Date.now();
|
|
4009
|
+
if (now - last < everyMs) return;
|
|
4010
|
+
last = now;
|
|
4011
|
+
write(message);
|
|
4012
|
+
},
|
|
4013
|
+
done() {
|
|
4014
|
+
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
4015
|
+
width = 0;
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
// src/commands/db-push.ts
|
|
4021
|
+
var PAGE = 2e3;
|
|
4022
|
+
function pickDatabase(databases, wanted) {
|
|
4023
|
+
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4024
|
+
if (wanted) {
|
|
4025
|
+
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
4026
|
+
if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
|
|
4027
|
+
return match;
|
|
4028
|
+
}
|
|
4029
|
+
if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
|
|
4030
|
+
return databases[0];
|
|
4031
|
+
}
|
|
4032
|
+
async function dbPush(tables, options2 = {}) {
|
|
4033
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4034
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4035
|
+
if (tables.length === 0) throw new Error("Name at least one table, e.g. flare db:push products categories --yes");
|
|
4036
|
+
const bad = tables.find((table) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(table));
|
|
4037
|
+
if (bad) throw new Error(`"${bad}" isn't a table name.`);
|
|
4038
|
+
if (!options2.yes) {
|
|
4039
|
+
throw new Error(`This writes ${tables.join(", ")} to the deployed database. Re-run with --yes to confirm.`);
|
|
4040
|
+
}
|
|
4041
|
+
const db = pickDatabase(readD1Databases(appRoot), options2.database);
|
|
4042
|
+
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4043
|
+
const started = Date.now();
|
|
4044
|
+
const local = await openLocalD1(appRoot, options2.database);
|
|
4045
|
+
const dir = mkdtempSync2(join22(tmpdir2(), "flare-push-"));
|
|
4046
|
+
try {
|
|
4047
|
+
const statements = [];
|
|
4048
|
+
const counts = [];
|
|
4049
|
+
for (const table of tables) {
|
|
4050
|
+
if (options2.truncate) statements.push(`DELETE FROM "${table}";`);
|
|
4051
|
+
let offset = 0;
|
|
4052
|
+
let rows = 0;
|
|
4053
|
+
for (; ; ) {
|
|
4054
|
+
const page = await local.db.prepare(`SELECT * FROM "${table}" LIMIT ${PAGE} OFFSET ${offset}`).all();
|
|
4055
|
+
const results = page.results ?? [];
|
|
4056
|
+
if (results.length === 0) break;
|
|
4057
|
+
const columns = Object.keys(results[0]);
|
|
4058
|
+
for (const statement of insertStatements(table, columns, results)) statements.push(statement.sql);
|
|
4059
|
+
rows += results.length;
|
|
4060
|
+
if (results.length < PAGE) break;
|
|
4061
|
+
offset += PAGE;
|
|
4062
|
+
}
|
|
4063
|
+
counts.push({ table, rows });
|
|
4064
|
+
log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
|
|
4065
|
+
}
|
|
4066
|
+
const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
|
|
4067
|
+
if (total === 0) {
|
|
4068
|
+
log2("Nothing to push: those tables are empty locally.");
|
|
4069
|
+
return { tables: counts, ms: Date.now() - started };
|
|
4070
|
+
}
|
|
4071
|
+
const file = join22(dir, "push.sql");
|
|
4072
|
+
writeFileSync15(file, statements.join("\n"));
|
|
4073
|
+
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4074
|
+
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4075
|
+
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
4076
|
+
${result.output.trim()}`);
|
|
4077
|
+
const ms = Date.now() - started;
|
|
4078
|
+
log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
|
|
4079
|
+
return { tables: counts, ms };
|
|
4080
|
+
} finally {
|
|
4081
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
4082
|
+
await local.dispose();
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
// src/commands/seed-resource.ts
|
|
4087
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, writeFileSync as writeFileSync16 } from "fs";
|
|
4088
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
4089
|
+
import { join as join23 } from "path";
|
|
4090
|
+
import pc19 from "picocolors";
|
|
3914
4091
|
|
|
3915
4092
|
// src/seed/rows.ts
|
|
3916
4093
|
import { columnName as columnName2, createFake as createFake2, storedFields as storedFields3 } from "@flaredev/core";
|
|
@@ -3933,7 +4110,9 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3933
4110
|
return fake.slug();
|
|
3934
4111
|
}
|
|
3935
4112
|
const name = key.toLowerCase();
|
|
3936
|
-
const
|
|
4113
|
+
const resourceName2 = resource.name.toLowerCase();
|
|
4114
|
+
const organisation = has(resourceName2, "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
|
|
4115
|
+
const catalogue = has(resourceName2, "product", "item", "article", "sku", "listing", "merch", "book", "course", "plan", "service");
|
|
3937
4116
|
let value;
|
|
3938
4117
|
if (has(name, "email")) value = fake.email();
|
|
3939
4118
|
else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
|
|
@@ -3948,7 +4127,7 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3948
4127
|
else if (has(name, "colour", "color")) value = fake.color();
|
|
3949
4128
|
else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
|
|
3950
4129
|
else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
|
|
3951
|
-
else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
|
|
4130
|
+
else if (has(name, "name")) value = organisation ? fake.company() : catalogue ? fake.product(digitalish(resource)) : fake.fullName();
|
|
3952
4131
|
else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
|
|
3953
4132
|
else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
|
|
3954
4133
|
if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
|
|
@@ -4010,6 +4189,11 @@ function uniquify(value, row) {
|
|
|
4010
4189
|
if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
|
|
4011
4190
|
return `${value} ${row}`;
|
|
4012
4191
|
}
|
|
4192
|
+
function digitalish(resource) {
|
|
4193
|
+
return Object.entries(resource.fields).some(
|
|
4194
|
+
([key, def]) => def.kind === "enum" && /kind|type/.test(key) && def.options.some((option) => /digital|download|licence|license/i.test(option))
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4013
4197
|
function seedColumns(resource) {
|
|
4014
4198
|
return ["id", ...storedFields3(resource).map(([key]) => columnName2(key)), "created_at", "updated_at"];
|
|
4015
4199
|
}
|
|
@@ -4018,11 +4202,13 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4018
4202
|
const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
|
|
4019
4203
|
const fields = storedFields3(resource);
|
|
4020
4204
|
const now = options2.now ?? Date.now();
|
|
4205
|
+
const start = options2.startIndex ?? 0;
|
|
4021
4206
|
for (let i = 0; i < count; i++) {
|
|
4207
|
+
const number = start + i;
|
|
4022
4208
|
const row = { id: fake.id() };
|
|
4023
4209
|
for (const [key, def] of fields) {
|
|
4024
|
-
let value = fieldValue(key, def, fake, resource, parentIds,
|
|
4025
|
-
if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value,
|
|
4210
|
+
let value = fieldValue(key, def, fake, resource, parentIds, number);
|
|
4211
|
+
if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
|
|
4026
4212
|
row[columnName2(key)] = value;
|
|
4027
4213
|
}
|
|
4028
4214
|
const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
|
|
@@ -4033,6 +4219,10 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4033
4219
|
}
|
|
4034
4220
|
|
|
4035
4221
|
// src/commands/seed-resource.ts
|
|
4222
|
+
async function countRows(db, table) {
|
|
4223
|
+
const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
|
|
4224
|
+
return row?.total ?? 0;
|
|
4225
|
+
}
|
|
4036
4226
|
var PARENT_SAMPLE = 5e3;
|
|
4037
4227
|
function parseCount(value, fallback = 25) {
|
|
4038
4228
|
if (value === void 0 || value === "") return fallback;
|
|
@@ -4052,7 +4242,7 @@ function pickResource(resources, name) {
|
|
|
4052
4242
|
}
|
|
4053
4243
|
return match;
|
|
4054
4244
|
}
|
|
4055
|
-
function
|
|
4245
|
+
function pickDatabase2(databases, wanted) {
|
|
4056
4246
|
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4057
4247
|
if (wanted) {
|
|
4058
4248
|
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
@@ -4081,13 +4271,13 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4081
4271
|
...options2,
|
|
4082
4272
|
onProgress: (written, total) => {
|
|
4083
4273
|
const elapsed = Date.now() - started;
|
|
4084
|
-
progress.update(` ${
|
|
4274
|
+
progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
|
|
4085
4275
|
}
|
|
4086
4276
|
});
|
|
4087
4277
|
progress.done();
|
|
4088
4278
|
const { rows, ms, where } = result;
|
|
4089
4279
|
console.log(
|
|
4090
|
-
`${
|
|
4280
|
+
`${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
|
|
4091
4281
|
);
|
|
4092
4282
|
return result;
|
|
4093
4283
|
} catch (error) {
|
|
@@ -4108,9 +4298,10 @@ async function seedResource(name, options2 = {}) {
|
|
|
4108
4298
|
const columns = seedColumns(resource);
|
|
4109
4299
|
const started = Date.now();
|
|
4110
4300
|
let parents = /* @__PURE__ */ new Map();
|
|
4111
|
-
|
|
4301
|
+
let startIndex = 0;
|
|
4302
|
+
const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
|
|
4112
4303
|
if (options2.remote) {
|
|
4113
|
-
const db =
|
|
4304
|
+
const db = pickDatabase2(readD1Databases(appRoot), options2.database);
|
|
4114
4305
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4115
4306
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4116
4307
|
const query = async (sql2) => {
|
|
@@ -4125,8 +4316,12 @@ ${result.output.trim()}`);
|
|
|
4125
4316
|
const found = await query(`SELECT id FROM "${table}" LIMIT ${PARENT_SAMPLE}`);
|
|
4126
4317
|
return found.map((row) => row.id);
|
|
4127
4318
|
});
|
|
4128
|
-
|
|
4129
|
-
|
|
4319
|
+
if (!options2.truncate) {
|
|
4320
|
+
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4321
|
+
startIndex = counted?.total ?? 0;
|
|
4322
|
+
}
|
|
4323
|
+
const dir = mkdtempSync3(join23(tmpdir3(), "flare-seed-"));
|
|
4324
|
+
const file = join23(dir, `${resource.table}.sql`);
|
|
4130
4325
|
try {
|
|
4131
4326
|
const statements = [];
|
|
4132
4327
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4136,13 +4331,13 @@ ${result.output.trim()}`);
|
|
|
4136
4331
|
written += statement.rows;
|
|
4137
4332
|
options2.onProgress?.(written, total);
|
|
4138
4333
|
}
|
|
4139
|
-
|
|
4140
|
-
log2(`${
|
|
4334
|
+
writeFileSync16(file, statements.join("\n"));
|
|
4335
|
+
log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
|
|
4141
4336
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4142
4337
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4143
4338
|
${result.output.trim()}`);
|
|
4144
4339
|
} finally {
|
|
4145
|
-
|
|
4340
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
4146
4341
|
}
|
|
4147
4342
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4148
4343
|
}
|
|
@@ -4153,6 +4348,7 @@ ${result.output.trim()}`);
|
|
|
4153
4348
|
return (found.results ?? []).map((row) => row.id);
|
|
4154
4349
|
});
|
|
4155
4350
|
if (options2.truncate) await local.db.exec(`DELETE FROM "${resource.table}";`);
|
|
4351
|
+
else startIndex = await countRows(local.db, resource.table);
|
|
4156
4352
|
const sink = { exec: (sql2) => local.db.exec(sql2) };
|
|
4157
4353
|
const written = await bulkInsert(sink, {
|
|
4158
4354
|
table: resource.table,
|
|
@@ -4168,16 +4364,16 @@ ${result.output.trim()}`);
|
|
|
4168
4364
|
|
|
4169
4365
|
// src/commands/billing-sync.ts
|
|
4170
4366
|
import { createRequire as createRequire2 } from "module";
|
|
4171
|
-
import { existsSync as
|
|
4172
|
-
import { tmpdir as
|
|
4173
|
-
import { join as
|
|
4367
|
+
import { existsSync as existsSync22, mkdtempSync as mkdtempSync4, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync17 } from "fs";
|
|
4368
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
4369
|
+
import { join as join24 } from "path";
|
|
4174
4370
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4175
|
-
import { kebabCase as
|
|
4176
|
-
import
|
|
4371
|
+
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4372
|
+
import pc20 from "picocolors";
|
|
4177
4373
|
function stripeKeys(appRoot) {
|
|
4178
4374
|
const map = {};
|
|
4179
|
-
const path =
|
|
4180
|
-
if (
|
|
4375
|
+
const path = join24(appRoot, ".dev.vars");
|
|
4376
|
+
if (existsSync22(path)) {
|
|
4181
4377
|
for (const line of readFileSync13(path, "utf8").split(/\r?\n/)) {
|
|
4182
4378
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
4183
4379
|
if (match) map[match[1]] = match[2].trim();
|
|
@@ -4198,7 +4394,7 @@ function planRows(catalog) {
|
|
|
4198
4394
|
return ok;
|
|
4199
4395
|
});
|
|
4200
4396
|
const currencies = new Set(usable.map((price) => price.currency));
|
|
4201
|
-
const base = product.metadata?.slug ||
|
|
4397
|
+
const base = product.metadata?.slug || kebabCase5(product.name);
|
|
4202
4398
|
for (const price of usable) {
|
|
4203
4399
|
const interval = price.recurring?.interval ?? null;
|
|
4204
4400
|
const parts = [base];
|
|
@@ -4261,10 +4457,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4261
4457
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4262
4458
|
const db = readD1Databases(appRoot)[0];
|
|
4263
4459
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4264
|
-
const appRequire = createRequire2(
|
|
4460
|
+
const appRequire = createRequire2(join24(appRoot, "package.json"));
|
|
4265
4461
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4266
4462
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4267
|
-
const appName = JSON.parse(readFileSync13(
|
|
4463
|
+
const appName = JSON.parse(readFileSync13(join24(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4268
4464
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4269
4465
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4270
4466
|
if (products.length === 0) {
|
|
@@ -4285,41 +4481,41 @@ async function syncPlans(options2 = {}) {
|
|
|
4285
4481
|
${existingResult.output.trim()}`);
|
|
4286
4482
|
}
|
|
4287
4483
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4288
|
-
const dir =
|
|
4484
|
+
const dir = mkdtempSync4(join24(tmpdir4(), "flare-sync-"));
|
|
4289
4485
|
try {
|
|
4290
|
-
const file =
|
|
4291
|
-
|
|
4486
|
+
const file = join24(dir, "sync-plans.sql");
|
|
4487
|
+
writeFileSync17(file, renderSyncSql(rows));
|
|
4292
4488
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4293
4489
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4294
4490
|
${result.output.trim()}`);
|
|
4295
4491
|
} finally {
|
|
4296
|
-
|
|
4492
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
4297
4493
|
}
|
|
4298
4494
|
let created = 0;
|
|
4299
4495
|
for (const row of rows) {
|
|
4300
4496
|
const isNew = !known.has(row.stripePriceId);
|
|
4301
4497
|
if (isNew) created++;
|
|
4302
4498
|
const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
|
|
4303
|
-
log2(`${(isNew ?
|
|
4499
|
+
log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
|
|
4304
4500
|
}
|
|
4305
|
-
for (const name of skipped) log2(
|
|
4501
|
+
for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
|
|
4306
4502
|
const configuration = await syncPortalConfiguration(stripe, rows, appName);
|
|
4307
|
-
if (configuration) log2(`${
|
|
4308
|
-
log2(
|
|
4503
|
+
if (configuration) log2(`${pc20.dim("portal".padEnd(9))} plan changes enabled for ${new Set(rows.filter((row) => row.interval).map((row) => row.stripeProductId)).size} product(s) (${configuration})`);
|
|
4504
|
+
log2(pc20.green(`
|
|
4309
4505
|
${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
|
|
4310
4506
|
return { created, updated: rows.length - created, skipped };
|
|
4311
4507
|
}
|
|
4312
4508
|
|
|
4313
4509
|
// src/commands/sync.ts
|
|
4314
|
-
import { existsSync as
|
|
4315
|
-
import { join as
|
|
4316
|
-
import
|
|
4510
|
+
import { existsSync as existsSync23 } from "fs";
|
|
4511
|
+
import { join as join25 } from "path";
|
|
4512
|
+
import pc21 from "picocolors";
|
|
4317
4513
|
async function syncTypes(options2 = {}) {
|
|
4318
4514
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4319
4515
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4320
4516
|
const all = await loadResources(appRoot);
|
|
4321
4517
|
const policies = await loadPolicies(appRoot);
|
|
4322
|
-
const tracksPolicies = policies.length > 0 ||
|
|
4518
|
+
const tracksPolicies = policies.length > 0 || existsSync23(join25(appRoot, "policies/index.ts"));
|
|
4323
4519
|
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
4324
4520
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4325
4521
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
@@ -4331,42 +4527,42 @@ async function syncTypes(options2 = {}) {
|
|
|
4331
4527
|
if (drift.length) {
|
|
4332
4528
|
log2(
|
|
4333
4529
|
`
|
|
4334
|
-
${
|
|
4530
|
+
${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
|
|
4335
4531
|
Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
|
|
4336
4532
|
);
|
|
4337
4533
|
}
|
|
4338
4534
|
if (unmarked.length) {
|
|
4339
4535
|
log2(`
|
|
4340
|
-
${
|
|
4536
|
+
${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
|
|
4341
4537
|
}
|
|
4342
4538
|
if (orphans.length) {
|
|
4343
4539
|
const resources = [...new Set(orphans.map((o) => o.resource))];
|
|
4344
4540
|
log2(`
|
|
4345
|
-
${
|
|
4541
|
+
${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
|
|
4346
4542
|
for (const orphan of orphans) log2(` ${orphan.path}`);
|
|
4347
4543
|
log2(
|
|
4348
|
-
`If the resource was deleted, remove them with ${
|
|
4544
|
+
`If the resource was deleted, remove them with ${pc21.bold(`flare rm resource ${resources[0]}`)}; if it was renamed (table or slug), delete the old files.`
|
|
4349
4545
|
);
|
|
4350
4546
|
}
|
|
4351
4547
|
const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
|
|
4352
4548
|
if (tablesChanged && !options2.check) {
|
|
4353
4549
|
log2(`
|
|
4354
|
-
${
|
|
4550
|
+
${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
|
|
4355
4551
|
}
|
|
4356
4552
|
const problems = drift.length + unmarked.length + orphans.length;
|
|
4357
4553
|
if (options2.check) {
|
|
4358
4554
|
const stale = changed.length;
|
|
4359
|
-
if (stale === 0 && problems === 0) log2(
|
|
4555
|
+
if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
|
|
4360
4556
|
else log2(`
|
|
4361
|
-
${
|
|
4557
|
+
${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
|
|
4362
4558
|
return stale + problems > 0 ? 1 : 0;
|
|
4363
4559
|
}
|
|
4364
|
-
if (changed.length === 0 && problems === 0) log2(
|
|
4560
|
+
if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
|
|
4365
4561
|
return problems > 0 ? 1 : 0;
|
|
4366
4562
|
}
|
|
4367
4563
|
|
|
4368
4564
|
// src/commands/user-role.ts
|
|
4369
|
-
import
|
|
4565
|
+
import pc22 from "picocolors";
|
|
4370
4566
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
4371
4567
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
4372
4568
|
function userRoleSql(email, role) {
|
|
@@ -4380,8 +4576,8 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
4380
4576
|
const sql2 = userRoleSql(email, role);
|
|
4381
4577
|
const roles = await listRoles(options2);
|
|
4382
4578
|
if (roles && !roles.includes(role)) {
|
|
4383
|
-
log2(
|
|
4384
|
-
log2(
|
|
4579
|
+
log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
4580
|
+
log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
|
|
4385
4581
|
return 1;
|
|
4386
4582
|
}
|
|
4387
4583
|
const db = readD1Databases(appRoot)[0];
|
|
@@ -4395,51 +4591,130 @@ ${result.output.trim()}`);
|
|
|
4395
4591
|
const [first] = extractJson(result.output);
|
|
4396
4592
|
const updated = first?.results ?? [];
|
|
4397
4593
|
if (updated.length === 0) {
|
|
4398
|
-
log2(
|
|
4594
|
+
log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
|
|
4399
4595
|
return 1;
|
|
4400
4596
|
}
|
|
4401
|
-
log2(
|
|
4597
|
+
log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
4598
|
+
return 0;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
// src/utils/install.ts
|
|
4602
|
+
import { spawn as spawn5 } from "child_process";
|
|
4603
|
+
import { existsSync as existsSync24, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "fs";
|
|
4604
|
+
import { join as join26 } from "path";
|
|
4605
|
+
function expectedPackages(dir) {
|
|
4606
|
+
const npmLock = join26(dir, "package-lock.json");
|
|
4607
|
+
if (existsSync24(npmLock)) {
|
|
4608
|
+
try {
|
|
4609
|
+
const lock = JSON.parse(readFileSync14(npmLock, "utf8"));
|
|
4610
|
+
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
4611
|
+
} catch {
|
|
4612
|
+
return 0;
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
const pnpmLock = join26(dir, "pnpm-lock.yaml");
|
|
4616
|
+
if (existsSync24(pnpmLock)) {
|
|
4617
|
+
try {
|
|
4618
|
+
const text = readFileSync14(pnpmLock, "utf8");
|
|
4619
|
+
const packages = text.slice(text.indexOf("\npackages:"));
|
|
4620
|
+
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
4621
|
+
} catch {
|
|
4622
|
+
return 0;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4402
4625
|
return 0;
|
|
4403
4626
|
}
|
|
4627
|
+
function installedPackages(dir) {
|
|
4628
|
+
const root = join26(dir, "node_modules");
|
|
4629
|
+
if (!existsSync24(root)) return 0;
|
|
4630
|
+
let total = 0;
|
|
4631
|
+
try {
|
|
4632
|
+
for (const entry of readdirSync9(root, { withFileTypes: true })) {
|
|
4633
|
+
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4634
|
+
if (entry.name.startsWith("@")) {
|
|
4635
|
+
try {
|
|
4636
|
+
total += readdirSync9(join26(root, entry.name)).length;
|
|
4637
|
+
} catch {
|
|
4638
|
+
}
|
|
4639
|
+
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
4640
|
+
total += 1;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
} catch {
|
|
4644
|
+
return total;
|
|
4645
|
+
}
|
|
4646
|
+
return total;
|
|
4647
|
+
}
|
|
4648
|
+
function install(packageManager, dir, onProgress) {
|
|
4649
|
+
const args = installArgs(packageManager);
|
|
4650
|
+
const started = Date.now();
|
|
4651
|
+
const expected = expectedPackages(dir);
|
|
4652
|
+
return new Promise((resolve6, reject) => {
|
|
4653
|
+
const child = process.platform === "win32" ? spawn5([packageManager, ...args].join(" "), { cwd: dir, shell: true, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] }) : spawn5(packageManager, args, { cwd: dir, env: childEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
4654
|
+
let output = "";
|
|
4655
|
+
child.stdout?.on("data", (chunk) => output += chunk);
|
|
4656
|
+
child.stderr?.on("data", (chunk) => output += chunk);
|
|
4657
|
+
const timer = onProgress ? setInterval(() => {
|
|
4658
|
+
const installed = installedPackages(dir);
|
|
4659
|
+
onProgress({
|
|
4660
|
+
installed,
|
|
4661
|
+
expected,
|
|
4662
|
+
percent: expected > 0 ? Math.min(99, Math.round(installed / expected * 100)) : void 0,
|
|
4663
|
+
elapsedMs: Date.now() - started
|
|
4664
|
+
});
|
|
4665
|
+
}, 500) : void 0;
|
|
4666
|
+
child.on("error", (error) => {
|
|
4667
|
+
clearInterval(timer);
|
|
4668
|
+
reject(error);
|
|
4669
|
+
});
|
|
4670
|
+
child.on("close", (code) => {
|
|
4671
|
+
clearInterval(timer);
|
|
4672
|
+
resolve6({ code: code ?? 1, output, ms: Date.now() - started });
|
|
4673
|
+
});
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4404
4676
|
|
|
4405
4677
|
// src/cli.ts
|
|
4406
4678
|
function createCli() {
|
|
4407
4679
|
const cli = cac("flare");
|
|
4408
4680
|
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--theme <name>", "Look of the app: default, coral, amber, sky, mono or emerald").option("--auth <list>", "Sign-in methods: magic-link, email-otp, passkeys, 2fa-app, 2fa-email, all or none (default: all)").option("--auth-providers <list>", "Social sign-in, comma-separated: google, github, apple, microsoft").option("-y, --yes", "Don't ask: use the flags given and the defaults for the rest").option("--skip-install", "Write files without installing dependencies").example("flare create shop").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(async (dir, options2) => {
|
|
4409
|
-
const
|
|
4681
|
+
const wanted = !options2.skipInstall;
|
|
4410
4682
|
let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
4411
4683
|
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
|
|
4412
|
-
const result = createApp(dir, {
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
return code;
|
|
4684
|
+
const result = createApp(dir, { pm: options2.pm, install: false, ...answers, log: (message) => prompts3.log.message(message) });
|
|
4685
|
+
let installed = false;
|
|
4686
|
+
if (wanted) {
|
|
4687
|
+
const spin = prompts3.spinner({ indicator: "timer" });
|
|
4688
|
+
spin.start(`Installing dependencies with ${result.packageManager}`);
|
|
4689
|
+
const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
|
|
4690
|
+
spin.message(
|
|
4691
|
+
percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
|
|
4692
|
+
);
|
|
4693
|
+
});
|
|
4694
|
+
installed = outcome.code === 0;
|
|
4695
|
+
if (installed) {
|
|
4696
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
4697
|
+
} else {
|
|
4698
|
+
spin.error(`${result.packageManager} install failed`);
|
|
4699
|
+
process.stderr.write(outcome.output);
|
|
4700
|
+
process.exitCode = 1;
|
|
4430
4701
|
}
|
|
4431
|
-
}
|
|
4432
|
-
printNextSteps(result,
|
|
4702
|
+
}
|
|
4703
|
+
printNextSteps(result, installed);
|
|
4433
4704
|
});
|
|
4434
|
-
cli.command("gen <generator> [name]", "Generate code. Generators: resource, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
|
|
4435
|
-
async (generator, name, options2) => {
|
|
4705
|
+
cli.command("gen <generator> [name] [second]", "Generate code. Generators: resource, endpoint, migration, policy, billing, security").option("--fields <fields>", "resource: fields, e.g. 'name:string, email:string!, status:enum(lead,customer)' (single quotes: bash treats ! in double quotes as history)").option("--group <name>", "resource: sidebar heading to file it under, e.g. Sales").option("--icon <name>", "resource: lucide icon for the sidebar, e.g. users").option("--method <verb>", "endpoint: GET (default), POST, PATCH, PUT or DELETE").option("--action <action>", "endpoint: policy action to require (read, create, update, delete)").option("--record", "endpoint: put it under one record, /api/<resource>/[id]/<name>").option("--force", "resource/policy/billing/security: overwrite hand-edited generated blocks").option("--from-schema", "migration: diff the current tables instead of a blank migration").option("--roles <roles>", "policy: roles allowed to read, create and update, e.g. admin,staff").option("--delete-roles <roles>", "policy: roles allowed to delete (default: the first --roles entry)").option("--provider <provider>", "billing: payment provider (default: stripe)").option("--mode <mode>", "billing: subscriptions (the default; includes one-time checkout)").option("--skip-install", "billing: write files without installing the stripe dependency").option("--skip-migration", "billing/security: skip generating the schema migration").example("flare gen resource Contact --fields 'name:string, email:string!, company:belongsTo(Company)?'").example("flare gen endpoint Order publish --method POST --record").example("flare gen migration backfill_contact_status").example("flare gen migration add_phone_to_contacts --from-schema").example("flare gen policy Invoice --roles admin,staff --delete-roles admin").example("flare gen billing --provider stripe --mode subscriptions").example("flare gen security").action(
|
|
4706
|
+
async (generator, name, second, options2) => {
|
|
4436
4707
|
if (generator === "billing")
|
|
4437
4708
|
return genBilling({ provider: options2.provider, mode: options2.mode, force: options2.force, skipInstall: options2.skipInstall, skipMigration: options2.skipMigration });
|
|
4438
4709
|
if (generator === "security") return genSecurity({ force: options2.force, skipMigration: options2.skipMigration });
|
|
4439
4710
|
if (["resource", "migration", "policy"].includes(generator) && !name) {
|
|
4440
4711
|
throw new Error(`flare gen ${generator} needs a name, e.g. flare gen ${generator} ${generator === "migration" ? "add_phone_to_contacts" : "Contact"}`);
|
|
4441
4712
|
}
|
|
4442
|
-
if (generator === "resource") return genResource(name, { fields: options2.fields, force: options2.force });
|
|
4713
|
+
if (generator === "resource") return genResource(name, { fields: options2.fields, force: options2.force, group: options2.group, icon: options2.icon });
|
|
4714
|
+
if (generator === "endpoint") {
|
|
4715
|
+
if (!name || !second) throw new Error("flare gen endpoint needs a resource and a name, e.g. flare gen endpoint Order publish");
|
|
4716
|
+
return genEndpoint(name, second, { method: options2.method, action: options2.action, record: options2.record });
|
|
4717
|
+
}
|
|
4443
4718
|
if (generator === "migration") return genMigration(name, { fromSchema: options2.fromSchema });
|
|
4444
4719
|
if (generator === "policy") return genPolicy(name, { roles: options2.roles, deleteRoles: options2.deleteRoles, force: options2.force });
|
|
4445
4720
|
throw new Error(`Unknown generator "${generator}". Available: resource, migration, policy, billing, security.`);
|
|
@@ -4467,6 +4742,9 @@ function createCli() {
|
|
|
4467
4742
|
cli.command("seed:resource <resource> [count]", "Fill a resource's table with sample rows built from its descriptor").option("--count <rows>", "How many rows: 1000, 25k, 1m (default: 25)").option("--remote", "Seed the deployed database instead of the local one (needs --yes)").option("--truncate", "Delete the table's rows first").option("--seed <number>", "Same number, same rows").option("--env <name>", "Wrangler environment").option("--database <binding>", "D1 binding, when the app has several").option("-y, --yes", "Confirm writing to the remote database").example("flare seed:resource Contact --count 1000").example("flare seed:resource Contact 1m # a million rows").example("flare seed:resource Contact 5k --remote --yes").action(async (resource, count, options2) => {
|
|
4468
4743
|
await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
|
|
4469
4744
|
});
|
|
4745
|
+
cli.command("db:push [...tables]", "Copy rows from the local database to the deployed one (seeded catalogues, reference data)").option("--truncate", "Delete the remote rows of those tables first").option("--database <binding>", "D1 binding, when the app has several").option("--env <name>", "Wrangler environment").option("-y, --yes", "Confirm writing to the deployed database").example("flare db:push products categories --yes").example("flare db:push plans --truncate --yes").action(async (tables, options2) => {
|
|
4746
|
+
await dbPush(tables, options2);
|
|
4747
|
+
});
|
|
4470
4748
|
cli.command("seed:make <name>", "Create seeds/<name>.seed.ts (with example rows for a matching resource)").option("--resource <name>", "Resource to write example rows for").action(async (name, options2) => {
|
|
4471
4749
|
await makeSeed(name, options2);
|
|
4472
4750
|
});
|
|
@@ -4512,6 +4790,6 @@ try {
|
|
|
4512
4790
|
}
|
|
4513
4791
|
}
|
|
4514
4792
|
} catch (error) {
|
|
4515
|
-
console.error(
|
|
4793
|
+
console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
4516
4794
|
process.exitCode = 1;
|
|
4517
4795
|
}
|