@flaredev/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +851 -405
- package/package.json +2 -2
- package/templates/app/app/dashboard/actions.ts +38 -5
- package/templates/app/app/dashboard/costs/page.tsx +93 -0
- package/templates/app/app/dashboard/page.tsx +4 -7
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/app/page.tsx +10 -0
- package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
- package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
- package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +2 -2
- package/templates/app/components/dashboard/record-actions.tsx +9 -9
- package/templates/app/components/dashboard/record-detail.tsx +6 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
- package/templates/app/components/dashboard/resource-form.tsx +261 -165
- package/templates/app/components/dashboard/resource-icon.tsx +87 -85
- package/templates/app/components/dashboard/resource-stats.tsx +10 -9
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +12 -9
- package/templates/app/components/dashboard/row-actions.tsx +5 -5
- package/templates/app/lib/costs.ts +138 -0
- package/templates/app/lib/dashboard-nav.ts +5 -1
- package/templates/app/lib/dashboard.ts +66 -61
- package/templates/app/lib/number.ts +33 -0
- package/templates/app/lib/site.ts +56 -56
- package/templates/app/lib/suggest.ts +35 -0
- package/templates/app/lib/usage.ts +32 -0
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- package/templates/locks/yarn.lock +3 -3
- package/templates/next/.env.example +21 -0
- package/templates/next/README.md +16 -0
- package/templates/next/app/api/health/route.ts +7 -0
- package/templates/next/app/dashboard/account/password/page.tsx +19 -0
- package/templates/next/app/dashboard/account/security/page.tsx +31 -0
- package/templates/next/app/dashboard/actions.ts +277 -0
- package/templates/next/app/dashboard/layout.tsx +64 -0
- package/templates/next/app/dashboard/page.tsx +127 -0
- package/templates/next/components/dashboard/resource-chart.tsx +86 -0
- package/templates/next/lib/analytics.ts +71 -0
- package/templates/next/lib/audit.ts +80 -0
- package/templates/next/lib/auth-emails.ts +92 -0
- package/templates/next/lib/auth.ts +156 -0
- package/templates/next/lib/dashboard.ts +161 -0
- package/templates/next/lib/db.ts +24 -0
- package/templates/next/lib/mail.ts +35 -0
- package/templates/next/lib/realtime.ts +23 -0
- package/templates/next/lib/redis.ts +17 -0
- package/templates/next/lib/storage.ts +57 -0
- package/templates/next/lib/theme.ts +83 -0
- package/templates/next/lib/views.ts +23 -0
- package/templates/next/next.config.ts +13 -0
- package/templates/next/prisma/schema/base.prisma +162 -0
- package/templates/next/prisma.config.ts +15 -0
- package/templates/next/tsconfig.json +24 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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
|
|
6
|
+
import pc23 from "picocolors";
|
|
7
7
|
import * as prompts3 from "@clack/prompts";
|
|
8
8
|
import { FLARE_VERSION as FLARE_VERSION3 } from "@flaredev/core";
|
|
9
9
|
|
|
10
10
|
// src/commands/create.ts
|
|
11
11
|
import { randomBytes } from "crypto";
|
|
12
|
-
import { appendFileSync, copyFileSync, existsSync as
|
|
13
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
12
|
+
import { appendFileSync, copyFileSync, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
13
|
+
import { basename, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
|
|
14
14
|
import * as prompts from "@clack/prompts";
|
|
15
15
|
import pc from "picocolors";
|
|
16
16
|
import { FLARE_VERSION } from "@flaredev/core";
|
|
@@ -139,6 +139,7 @@ function parseTheme(value) {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// src/versions.ts
|
|
142
|
+
var COMPATIBILITY_DATE = "2026-09-15";
|
|
142
143
|
var APP_DEPENDENCIES = {
|
|
143
144
|
"@better-auth/drizzle-adapter": "1.7.5",
|
|
144
145
|
"@better-auth/passkey": "1.7.5",
|
|
@@ -185,42 +186,90 @@ var APP_DEV_DEPENDENCIES = {
|
|
|
185
186
|
vite: "^8.3.0",
|
|
186
187
|
wrangler: "^4.132.0"
|
|
187
188
|
};
|
|
189
|
+
var NEXT_DEPENDENCIES = {
|
|
190
|
+
next: "16.0.4",
|
|
191
|
+
"@prisma/client": "7.10.0",
|
|
192
|
+
"@prisma/adapter-pg": "7.10.0",
|
|
193
|
+
pg: "^8.16.3",
|
|
194
|
+
"@upstash/redis": "^1.36.0",
|
|
195
|
+
"@better-auth/prisma-adapter": "1.7.5",
|
|
196
|
+
// Signs S3 requests in a few kilobytes, where the AWS SDK is megabytes.
|
|
197
|
+
aws4fetch: "^1.0.20",
|
|
198
|
+
dotenv: "^17.2.3"
|
|
199
|
+
};
|
|
200
|
+
var NEXT_DEV_DEPENDENCIES = {
|
|
201
|
+
prisma: "7.10.0",
|
|
202
|
+
// Runs seeds: the generated Prisma client is TypeScript a bundler compiles.
|
|
203
|
+
tsx: "^4.23.13",
|
|
204
|
+
"@types/node": "^22.20.3",
|
|
205
|
+
"@types/react": "^19.3.0",
|
|
206
|
+
"@types/react-dom": "^19.3.0",
|
|
207
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
208
|
+
tailwindcss: "^4.3.3",
|
|
209
|
+
typescript: "~5.9.3"
|
|
210
|
+
};
|
|
211
|
+
var CLOUDFLARE_ONLY = [
|
|
212
|
+
"wrangler.jsonc",
|
|
213
|
+
"vite.config.ts",
|
|
214
|
+
"worker",
|
|
215
|
+
"db",
|
|
216
|
+
"migrations",
|
|
217
|
+
"drizzle.config.ts",
|
|
218
|
+
".dev.vars.example",
|
|
219
|
+
"worker-configuration.d.ts"
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
// src/stack.ts
|
|
223
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
224
|
+
import { join } from "path";
|
|
225
|
+
var STACKS = ["cloudflare", "next"];
|
|
226
|
+
var isStack = (value) => STACKS.includes(value);
|
|
227
|
+
function readStack(appRoot) {
|
|
228
|
+
const file = join(appRoot, "package.json");
|
|
229
|
+
if (!existsSync(file)) return "cloudflare";
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
232
|
+
return isStack(parsed.flare?.stack) ? parsed.flare.stack : "cloudflare";
|
|
233
|
+
} catch {
|
|
234
|
+
return "cloudflare";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
188
237
|
|
|
189
238
|
// src/utils/fs.ts
|
|
190
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
191
|
-
import { dirname, join, resolve } from "path";
|
|
239
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
240
|
+
import { dirname, join as join2, resolve } from "path";
|
|
192
241
|
import { fileURLToPath } from "url";
|
|
193
242
|
var RENAMES = { _gitignore: ".gitignore" };
|
|
194
243
|
function copyTemplate(from, to, tokens) {
|
|
195
244
|
mkdirSync(to, { recursive: true });
|
|
196
245
|
let files = 0;
|
|
197
246
|
for (const entry of readdirSync(from)) {
|
|
198
|
-
const src =
|
|
199
|
-
const dest =
|
|
247
|
+
const src = join2(from, entry);
|
|
248
|
+
const dest = join2(to, RENAMES[entry] ?? entry);
|
|
200
249
|
if (statSync(src).isDirectory()) {
|
|
201
250
|
files += copyTemplate(src, dest, tokens);
|
|
202
251
|
continue;
|
|
203
252
|
}
|
|
204
|
-
const content =
|
|
205
|
-
|
|
253
|
+
const content = readFileSync2(src, "utf8").replace(/__([A-Z_]+)__/g, (match, key) => tokens[key] ?? match);
|
|
254
|
+
writeFileSync2(dest, content);
|
|
206
255
|
files++;
|
|
207
256
|
}
|
|
208
257
|
return files;
|
|
209
258
|
}
|
|
210
259
|
function writeJson(path, value) {
|
|
211
260
|
mkdirSync(dirname(path), { recursive: true });
|
|
212
|
-
|
|
261
|
+
writeFileSync2(path, JSON.stringify(value, null, 2) + "\n");
|
|
213
262
|
}
|
|
214
263
|
function findUp(file, start) {
|
|
215
264
|
let dir = resolve(start);
|
|
216
265
|
while (true) {
|
|
217
|
-
if (
|
|
266
|
+
if (existsSync2(join2(dir, file))) return dir;
|
|
218
267
|
const parent = dirname(dir);
|
|
219
268
|
if (parent === dir) return void 0;
|
|
220
269
|
dir = parent;
|
|
221
270
|
}
|
|
222
271
|
}
|
|
223
|
-
var templatesDir =
|
|
272
|
+
var templatesDir = join2(findUp("templates", dirname(fileURLToPath(import.meta.url))), "templates");
|
|
224
273
|
|
|
225
274
|
// src/utils/pm.ts
|
|
226
275
|
import { spawnSync } from "child_process";
|
|
@@ -259,7 +308,7 @@ function toAppName(dir) {
|
|
|
259
308
|
function flarePackageSpec(pkg, appDir, packageManager) {
|
|
260
309
|
const repoPackage = resolve2(templatesDir, "../..", pkg);
|
|
261
310
|
const repoRoot = resolve2(repoPackage, "../..");
|
|
262
|
-
const fromCheckout =
|
|
311
|
+
const fromCheckout = existsSync3(join3(repoPackage, "package.json")) && existsSync3(join3(repoRoot, "pnpm-workspace.yaml")) && !repoPackage.split(/[\\/]/).includes("node_modules");
|
|
263
312
|
if (!fromCheckout) return `^${FLARE_VERSION}`;
|
|
264
313
|
const rel = relative(repoRoot, appDir);
|
|
265
314
|
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return "workspace:*";
|
|
@@ -269,32 +318,54 @@ function flarePackageSpec(pkg, appDir, packageManager) {
|
|
|
269
318
|
function createApp(target, options2 = {}) {
|
|
270
319
|
const dir = resolve2(target);
|
|
271
320
|
const name = toAppName(dir);
|
|
272
|
-
if (
|
|
321
|
+
if (existsSync3(dir) && readdirSync2(dir).length > 0) {
|
|
273
322
|
throw new Error(`Directory ${dir} already exists and is not empty.`);
|
|
274
323
|
}
|
|
275
324
|
if (options2.pm !== void 0 && !isPackageManager(options2.pm)) {
|
|
276
325
|
throw new Error(`Unknown package manager "${options2.pm}". Use pnpm, npm, yarn, or bun.`);
|
|
277
326
|
}
|
|
278
327
|
const packageManager = options2.pm ?? detectPackageManager();
|
|
328
|
+
if (options2.stack !== void 0 && !isStack(options2.stack)) {
|
|
329
|
+
throw new Error(`Unknown stack "${options2.stack}". Use ${STACKS.join(" or ")}.`);
|
|
330
|
+
}
|
|
331
|
+
const stack = options2.stack ?? "cloudflare";
|
|
279
332
|
const authProviders = parseAuthProviders(options2.authProviders);
|
|
280
333
|
const authMethods = parseAuthMethods(options2.auth);
|
|
281
334
|
const theme = parseTheme(options2.theme);
|
|
282
335
|
const inWorkspace = findUp("pnpm-workspace.yaml", dirname2(dir)) !== void 0;
|
|
283
|
-
const compatibilityDate = options2.compatibilityDate ??
|
|
284
|
-
const files = copyTemplate(
|
|
336
|
+
const compatibilityDate = options2.compatibilityDate ?? COMPATIBILITY_DATE;
|
|
337
|
+
const files = copyTemplate(join3(templatesDir, "app"), dir, {
|
|
285
338
|
APP_NAME: name,
|
|
286
339
|
COMPAT_DATE: compatibilityDate,
|
|
287
340
|
PM: packageManager,
|
|
288
341
|
THEME: theme
|
|
289
342
|
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
343
|
+
if (stack === "next") {
|
|
344
|
+
copyTemplate(join3(templatesDir, "next"), dir, { APP_NAME: name, PM: packageManager, THEME: theme });
|
|
345
|
+
for (const path of CLOUDFLARE_ONLY) rmSync(join3(dir, path), { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
writeFileSync3(join3(dir, "lib", "auth-config.ts"), renderAuthConfig(authMethods, authProviders));
|
|
348
|
+
appendFileSync(join3(dir, stack === "next" ? ".env.example" : ".dev.vars.example"), devVarsExampleEntries(authProviders));
|
|
349
|
+
const shared = { react: APP_DEPENDENCIES.react, "react-dom": APP_DEPENDENCIES["react-dom"] };
|
|
350
|
+
const uiOnly = Object.fromEntries(
|
|
351
|
+
Object.entries(APP_DEPENDENCIES).filter(([key]) => !["vinext", "@vinext/cloudflare", "drizzle-orm", "react-server-dom-webpack"].includes(key))
|
|
352
|
+
);
|
|
353
|
+
writeJson(join3(dir, "package.json"), {
|
|
293
354
|
name,
|
|
294
355
|
version: "0.1.0",
|
|
295
356
|
private: true,
|
|
296
357
|
type: "module",
|
|
297
|
-
|
|
358
|
+
// Which stack this app targets. `flare gen` reads it; don't change it by hand.
|
|
359
|
+
flare: { stack },
|
|
360
|
+
scripts: stack === "next" ? {
|
|
361
|
+
dev: "next dev",
|
|
362
|
+
build: "prisma generate && next build",
|
|
363
|
+
start: "next start",
|
|
364
|
+
deploy: "vercel deploy --prod",
|
|
365
|
+
"db:generate": "prisma generate",
|
|
366
|
+
"db:migrate": "prisma migrate dev",
|
|
367
|
+
"db:studio": "prisma studio"
|
|
368
|
+
} : {
|
|
298
369
|
dev: "flare dev",
|
|
299
370
|
build: "flare build",
|
|
300
371
|
start: "flare start",
|
|
@@ -303,18 +374,25 @@ function createApp(target, options2 = {}) {
|
|
|
303
374
|
"db:generate": "drizzle-kit generate",
|
|
304
375
|
"db:migrate:local": "wrangler d1 migrations apply DB --local"
|
|
305
376
|
},
|
|
306
|
-
dependencies: {
|
|
307
|
-
|
|
377
|
+
dependencies: {
|
|
378
|
+
"@flaredev/core": flarePackageSpec("core", dir, packageManager),
|
|
379
|
+
...stack === "next" ? { ...uiOnly, ...shared, ...NEXT_DEPENDENCIES } : APP_DEPENDENCIES
|
|
380
|
+
},
|
|
381
|
+
devDependencies: {
|
|
382
|
+
"@flaredev/cli": flarePackageSpec("cli", dir, packageManager),
|
|
383
|
+
...stack === "next" ? NEXT_DEV_DEPENDENCIES : APP_DEV_DEPENDENCIES
|
|
384
|
+
}
|
|
308
385
|
});
|
|
309
|
-
|
|
310
|
-
|
|
386
|
+
writeFileSync3(
|
|
387
|
+
join3(dir, stack === "next" ? ".env" : ".dev.vars"),
|
|
311
388
|
`BETTER_AUTH_SECRET=${randomBytes(32).toString("base64")}
|
|
312
389
|
RESEND_API_KEY=
|
|
313
390
|
MAIL_FROM=
|
|
314
391
|
${devVarsEntries(authProviders)}`
|
|
315
392
|
);
|
|
316
393
|
if (packageManager === "pnpm" && !inWorkspace) {
|
|
317
|
-
|
|
394
|
+
const allowBuilds = stack === "next" ? "allowBuilds:\n '@prisma/engines': true\n prisma: true\n sharp: false\n" : "allowBuilds:\n esbuild: true\n workerd: true\n sharp: false\n";
|
|
395
|
+
writeFileSync3(join3(dir, "pnpm-workspace.yaml"), allowBuilds);
|
|
318
396
|
}
|
|
319
397
|
const pinned = flarePackageSpec("core", dir, packageManager).startsWith("^");
|
|
320
398
|
if (pinned) copyLockfile(packageManager, dir);
|
|
@@ -325,9 +403,9 @@ ${devVarsEntries(authProviders)}`
|
|
|
325
403
|
function copyLockfile(packageManager, dir) {
|
|
326
404
|
const file = packageManager === "pnpm" ? "pnpm-lock.yaml" : packageManager === "yarn" ? "yarn.lock" : "package-lock.json";
|
|
327
405
|
if (!file) return false;
|
|
328
|
-
const source =
|
|
329
|
-
if (!
|
|
330
|
-
copyFileSync(source,
|
|
406
|
+
const source = join3(templatesDir, "locks", file);
|
|
407
|
+
if (!existsSync3(source)) return false;
|
|
408
|
+
copyFileSync(source, join3(dir, file));
|
|
331
409
|
return true;
|
|
332
410
|
}
|
|
333
411
|
function printNextSteps(result, installed) {
|
|
@@ -347,8 +425,8 @@ function printNextSteps(result, installed) {
|
|
|
347
425
|
}
|
|
348
426
|
|
|
349
427
|
// src/commands/gen-billing.ts
|
|
350
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
351
|
-
import { dirname as dirname6, join as
|
|
428
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
|
|
429
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
352
430
|
import pc6 from "picocolors";
|
|
353
431
|
|
|
354
432
|
// src/generator/grammar.ts
|
|
@@ -379,6 +457,20 @@ function splitTopLevel(input) {
|
|
|
379
457
|
parts.push(current);
|
|
380
458
|
return parts.map((part) => part.trim()).filter(Boolean);
|
|
381
459
|
}
|
|
460
|
+
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
461
|
+
var SIZE_UNITS = { b: 1, kb: 1024, mb: 1024 * 1024, gb: 1024 * 1024 * 1024 };
|
|
462
|
+
function parseSize(text, spec) {
|
|
463
|
+
const match = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i.exec(text.trim());
|
|
464
|
+
if (!match) {
|
|
465
|
+
throw new FieldGrammarError(`"${spec}": "${text}" isn't a size. Write it like "file:[image]:5mb".`);
|
|
466
|
+
}
|
|
467
|
+
const bytes = Math.round(Number(match[1]) * SIZE_UNITS[match[2].toLowerCase()]);
|
|
468
|
+
if (bytes < 1) throw new FieldGrammarError(`"${spec}": a size limit has to be at least one byte.`);
|
|
469
|
+
if (bytes > MAX_UPLOAD_BYTES) {
|
|
470
|
+
throw new FieldGrammarError(`"${spec}": ${text} is more than a Worker will carry in one request; the most is 100mb.`);
|
|
471
|
+
}
|
|
472
|
+
return bytes;
|
|
473
|
+
}
|
|
382
474
|
function editDistance(a, b) {
|
|
383
475
|
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
384
476
|
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
@@ -439,11 +531,14 @@ function parseField(spec) {
|
|
|
439
531
|
}
|
|
440
532
|
if (new Set(options2).size !== options2.length) throw new FieldGrammarError(`"${spec}": duplicate ${choice} options.`);
|
|
441
533
|
field.options = options2;
|
|
442
|
-
} else if (
|
|
534
|
+
} else if (/^file\[/.test(type)) {
|
|
535
|
+
throw new FieldGrammarError(`"${spec}": file fields need a colon before the list, e.g. "${field.key}:file:[${type.slice(5, -1) || "image"}]".`);
|
|
536
|
+
} else if (match = /^file(?::\[(.*)\])?(?::([^:]*))?$/.exec(type)) {
|
|
443
537
|
field.kind = "file";
|
|
444
538
|
if (match[1] === void 0) {
|
|
445
539
|
throw new FieldGrammarError(`"${spec}": file fields list accepted types, e.g. "file:[image,pdf]".`);
|
|
446
540
|
}
|
|
541
|
+
if (match[2] !== void 0) field.maxBytes = parseSize(match[2], spec);
|
|
447
542
|
const categories = Object.keys(FILE_CATEGORIES);
|
|
448
543
|
const accept = list(match[1], "file type", spec).map((category) => category.toLowerCase());
|
|
449
544
|
for (const category of accept) {
|
|
@@ -495,7 +590,7 @@ import { defineResource, field as builders, kebabCase, pascalCase } from "@flare
|
|
|
495
590
|
|
|
496
591
|
// src/generator/markers.ts
|
|
497
592
|
import { createHash } from "crypto";
|
|
498
|
-
import { existsSync as
|
|
593
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
499
594
|
import { dirname as dirname3 } from "path";
|
|
500
595
|
var START = "// generated:start";
|
|
501
596
|
var END = "// generated:end";
|
|
@@ -545,8 +640,8 @@ ${indentBlock(body, indent)}${indent}${END}
|
|
|
545
640
|
${after}`;
|
|
546
641
|
}
|
|
547
642
|
function inspectGenerated(path, content) {
|
|
548
|
-
if (!
|
|
549
|
-
const parts = splitMarkers(
|
|
643
|
+
if (!existsSync4(path)) return "missing";
|
|
644
|
+
const parts = splitMarkers(readFileSync3(path, "utf8"));
|
|
550
645
|
if (!parts) return "unmarked";
|
|
551
646
|
if (parts.hash && hashBlock(parts.block) !== parts.hash) return "drift";
|
|
552
647
|
if (normalize(parts.block) !== normalize(content)) return "outdated";
|
|
@@ -562,18 +657,18 @@ var DriftError = class extends Error {
|
|
|
562
657
|
};
|
|
563
658
|
function writeGenerated(path, content, options2 = {}) {
|
|
564
659
|
const hash = hashBlock(content);
|
|
565
|
-
if (!
|
|
660
|
+
if (!existsSync4(path)) {
|
|
566
661
|
mkdirSync2(dirname3(path), { recursive: true });
|
|
567
|
-
|
|
662
|
+
writeFileSync4(path, joinMarkers({ before: options2.header ?? "", block: content, after: "", indent: "", hash }));
|
|
568
663
|
return "create";
|
|
569
664
|
}
|
|
570
|
-
const source =
|
|
665
|
+
const source = readFileSync3(path, "utf8");
|
|
571
666
|
const parts = splitMarkers(source);
|
|
572
667
|
if (!parts) throw new Error(`${path} exists but has no generated block; refusing to overwrite hand-written code.`);
|
|
573
668
|
if (parts.hash && hashBlock(parts.block) !== parts.hash && !options2.force) throw new DriftError(path);
|
|
574
669
|
const next = joinMarkers({ ...parts, block: content, hash });
|
|
575
670
|
if (next === source) return "identical";
|
|
576
|
-
|
|
671
|
+
writeFileSync4(path, next);
|
|
577
672
|
return "update";
|
|
578
673
|
}
|
|
579
674
|
|
|
@@ -592,6 +687,7 @@ function options(field) {
|
|
|
592
687
|
if (field.format) opts.format = field.format;
|
|
593
688
|
if (field.widget) opts.widget = field.widget;
|
|
594
689
|
if (field.kind === "belongsTo" && !field.required) opts.onDelete = "set null";
|
|
690
|
+
if (field.maxBytes !== void 0) opts.maxBytes = field.maxBytes;
|
|
595
691
|
return opts;
|
|
596
692
|
}
|
|
597
693
|
function toField(field) {
|
|
@@ -1327,17 +1423,17 @@ var billingDevVarsExampleEntries = () => [
|
|
|
1327
1423
|
].join("\n");
|
|
1328
1424
|
|
|
1329
1425
|
// src/generator/load.ts
|
|
1330
|
-
import { existsSync as
|
|
1331
|
-
import { join as
|
|
1426
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
1427
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1332
1428
|
import { createJiti } from "jiti";
|
|
1333
1429
|
async function loadResources(root) {
|
|
1334
1430
|
const appRoot = resolve3(root);
|
|
1335
|
-
const dir =
|
|
1336
|
-
if (!
|
|
1337
|
-
const jiti = createJiti(
|
|
1431
|
+
const dir = join4(appRoot, "resources");
|
|
1432
|
+
if (!existsSync5(dir)) return [];
|
|
1433
|
+
const jiti = createJiti(join4(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
1338
1434
|
const loaded = [];
|
|
1339
1435
|
for (const file of readdirSync3(dir).filter((name) => name.endsWith(".resource.ts")).sort()) {
|
|
1340
|
-
const path =
|
|
1436
|
+
const path = join4(dir, file);
|
|
1341
1437
|
const mod = await jiti.import(path);
|
|
1342
1438
|
const resource = mod.default;
|
|
1343
1439
|
if (!resource || typeof resource !== "object" || !("fields" in resource) || !("table" in resource)) {
|
|
@@ -1355,20 +1451,20 @@ async function loadResources(root) {
|
|
|
1355
1451
|
|
|
1356
1452
|
// src/generator/migrations.ts
|
|
1357
1453
|
import { spawn as spawn4 } from "child_process";
|
|
1358
|
-
import { existsSync as
|
|
1359
|
-
import { join as
|
|
1454
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
1455
|
+
import { join as join9 } from "path";
|
|
1360
1456
|
|
|
1361
1457
|
// src/commands/run.ts
|
|
1362
1458
|
import { spawn as spawn3 } from "child_process";
|
|
1363
|
-
import { existsSync as
|
|
1364
|
-
import { dirname as dirname4, join as
|
|
1459
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
1460
|
+
import { dirname as dirname4, join as join8, resolve as resolve4 } from "path";
|
|
1365
1461
|
import pc4 from "picocolors";
|
|
1366
1462
|
|
|
1367
1463
|
// src/tunnel.ts
|
|
1368
1464
|
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1369
|
-
import { chmodSync, existsSync as
|
|
1465
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1370
1466
|
import { homedir } from "os";
|
|
1371
|
-
import { join as
|
|
1467
|
+
import { join as join5 } from "path";
|
|
1372
1468
|
import pc2 from "picocolors";
|
|
1373
1469
|
var TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
1374
1470
|
var LOCAL_URL = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0):\d+/;
|
|
@@ -1393,7 +1489,7 @@ function releaseAsset(platform = process.platform, arch = process.arch) {
|
|
|
1393
1489
|
throw new Error(`No cloudflared build for ${platform}/${arch}. Install it yourself: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`);
|
|
1394
1490
|
}
|
|
1395
1491
|
var binName = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
1396
|
-
var cacheDir = () =>
|
|
1492
|
+
var cacheDir = () => join5(homedir(), ".flare", "bin");
|
|
1397
1493
|
function onPath() {
|
|
1398
1494
|
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["cloudflared"], { encoding: "utf8" });
|
|
1399
1495
|
return probe.status === 0 ? probe.stdout.split(/\r?\n/)[0]?.trim() ?? null : null;
|
|
@@ -1402,19 +1498,19 @@ async function download(log2) {
|
|
|
1402
1498
|
const asset = releaseAsset();
|
|
1403
1499
|
const dir = cacheDir();
|
|
1404
1500
|
mkdirSync3(dir, { recursive: true });
|
|
1405
|
-
const target =
|
|
1501
|
+
const target = join5(dir, binName);
|
|
1406
1502
|
log2(pc2.dim(`Downloading cloudflared (one time) into ${dir}\u2026`));
|
|
1407
1503
|
const response = await fetch(`https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`);
|
|
1408
1504
|
if (!response.ok) throw new Error(`Downloading cloudflared failed: HTTP ${response.status}.`);
|
|
1409
1505
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1410
1506
|
const partial = `${target}.download`;
|
|
1411
1507
|
if (asset.endsWith(".tgz")) {
|
|
1412
|
-
|
|
1508
|
+
writeFileSync5(`${partial}.tgz`, bytes);
|
|
1413
1509
|
const untar = spawnSync2("tar", ["-xzf", `${partial}.tgz`, "-C", dir], { encoding: "utf8" });
|
|
1414
|
-
|
|
1510
|
+
rmSync2(`${partial}.tgz`, { force: true });
|
|
1415
1511
|
if (untar.status !== 0) throw new Error(`Unpacking cloudflared failed: ${untar.stderr}`);
|
|
1416
1512
|
} else {
|
|
1417
|
-
|
|
1513
|
+
writeFileSync5(partial, bytes);
|
|
1418
1514
|
renameSync(partial, target);
|
|
1419
1515
|
}
|
|
1420
1516
|
if (process.platform !== "win32") chmodSync(target, 493);
|
|
@@ -1423,8 +1519,8 @@ async function download(log2) {
|
|
|
1423
1519
|
async function ensureCloudflared(log2 = console.log) {
|
|
1424
1520
|
const found = onPath();
|
|
1425
1521
|
if (found) return found;
|
|
1426
|
-
const cached =
|
|
1427
|
-
if (
|
|
1522
|
+
const cached = join5(cacheDir(), binName);
|
|
1523
|
+
if (existsSync6(cached)) return cached;
|
|
1428
1524
|
return download(log2);
|
|
1429
1525
|
}
|
|
1430
1526
|
async function openTunnel(localUrl, options2 = {}) {
|
|
@@ -1480,16 +1576,16 @@ function tunnelBanner(publicUrl, localUrl) {
|
|
|
1480
1576
|
|
|
1481
1577
|
// src/commands/deploy.ts
|
|
1482
1578
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1483
|
-
import { existsSync as
|
|
1484
|
-
import { join as
|
|
1579
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
1580
|
+
import { join as join7 } from "path";
|
|
1485
1581
|
import { createZoneClient } from "@flaredev/core/security";
|
|
1486
1582
|
import { createJiti as createJiti2 } from "jiti";
|
|
1487
1583
|
import pc3 from "picocolors";
|
|
1488
1584
|
|
|
1489
1585
|
// src/utils/wrangler.ts
|
|
1490
1586
|
import { spawn as spawn2 } from "child_process";
|
|
1491
|
-
import { existsSync as
|
|
1492
|
-
import { join as
|
|
1587
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
1588
|
+
import { join as join6 } from "path";
|
|
1493
1589
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
1494
1590
|
function runWrangler(wranglerBin, args, cwd, options2 = {}) {
|
|
1495
1591
|
return new Promise((resolve6, reject) => {
|
|
@@ -1514,9 +1610,9 @@ ${output.trim()}`);
|
|
|
1514
1610
|
return JSON.parse(output.slice(start, end + 1));
|
|
1515
1611
|
}
|
|
1516
1612
|
function readD1Databases(appRoot) {
|
|
1517
|
-
const path =
|
|
1518
|
-
if (!
|
|
1519
|
-
const config = parseJsonc(
|
|
1613
|
+
const path = join6(appRoot, "wrangler.jsonc");
|
|
1614
|
+
if (!existsSync7(path)) return [];
|
|
1615
|
+
const config = parseJsonc(readFileSync4(path, "utf8"));
|
|
1520
1616
|
return (config?.d1_databases ?? []).filter((db) => db.binding && db.database_name);
|
|
1521
1617
|
}
|
|
1522
1618
|
|
|
@@ -1534,6 +1630,7 @@ function parseDeployArgs(args) {
|
|
|
1534
1630
|
if (arg === "--skip-migrations") skipMigrations = true;
|
|
1535
1631
|
else if (arg === "--skip-secrets") skipSecrets = true;
|
|
1536
1632
|
else if (arg === "--skip-security") skipSecurity = true;
|
|
1633
|
+
else if (arg === "--yes" || arg === "-y") continue;
|
|
1537
1634
|
else {
|
|
1538
1635
|
forwarded.push(arg);
|
|
1539
1636
|
if (arg === "--env" && args[i + 1]) env = args[i + 1];
|
|
@@ -1555,15 +1652,15 @@ function declaredSecretNames(devVarsExample) {
|
|
|
1555
1652
|
return devVarsExample.split(/\r?\n/).map((line) => /^([A-Z][A-Z0-9_]*)=/.exec(line.trim())?.[1]).filter((name) => Boolean(name));
|
|
1556
1653
|
}
|
|
1557
1654
|
async function loadSecurityConfig(appRoot) {
|
|
1558
|
-
const path =
|
|
1559
|
-
if (!
|
|
1560
|
-
const jiti = createJiti2(
|
|
1655
|
+
const path = join7(appRoot, "security.config.ts");
|
|
1656
|
+
if (!existsSync8(path)) return null;
|
|
1657
|
+
const jiti = createJiti2(join7(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1561
1658
|
const mod = await jiti.import(path);
|
|
1562
1659
|
if (!mod.default) throw new Error("security.config.ts must `export default defineSecurity({ ... })`.");
|
|
1563
1660
|
return mod.default;
|
|
1564
1661
|
}
|
|
1565
1662
|
async function provisionZoneSecurity(appRoot, config, credentials, options2) {
|
|
1566
|
-
const app = JSON.parse(
|
|
1663
|
+
const app = JSON.parse(readFileSync5(join7(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
1567
1664
|
const zone = createZoneClient({ ...credentials, app, fetch: options2.fetch });
|
|
1568
1665
|
if (config.zone?.rateLimits) {
|
|
1569
1666
|
const result = await zone.syncRateLimits(config.zone.rateLimits);
|
|
@@ -1583,7 +1680,7 @@ async function runDeploy(args, context3) {
|
|
|
1583
1680
|
const envArgs = plan.env ? ["--env", plan.env] : [];
|
|
1584
1681
|
const step = (message) => log2(`
|
|
1585
1682
|
${pc3.bold(pc3.cyan("flare"))} ${message}`);
|
|
1586
|
-
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) =>
|
|
1683
|
+
const databases = plan.skipMigrations ? [] : readD1Databases(appRoot).filter((db) => existsSync8(join7(appRoot, db.migrations_dir ?? "migrations")));
|
|
1587
1684
|
const migrateAfterDeploy = [];
|
|
1588
1685
|
const migrate2 = async (db) => {
|
|
1589
1686
|
step(`Applying migrations to ${db.database_name} (remote)`);
|
|
@@ -1642,8 +1739,8 @@ ${put.output.trim()}`);
|
|
|
1642
1739
|
existing.add(name);
|
|
1643
1740
|
}
|
|
1644
1741
|
}
|
|
1645
|
-
const examplePath =
|
|
1646
|
-
const optional =
|
|
1742
|
+
const examplePath = join7(appRoot, ".dev.vars.example");
|
|
1743
|
+
const optional = existsSync8(examplePath) ? declaredSecretNames(readFileSync5(examplePath, "utf8")).filter((name) => !existing.has(name)) : [];
|
|
1647
1744
|
if (optional.length > 0) {
|
|
1648
1745
|
step("Optional secrets not set in production (features using them stay off):");
|
|
1649
1746
|
for (const name of optional) log2(` wrangler secret put ${name}${plan.env ? ` --env ${plan.env}` : ""}`);
|
|
@@ -1678,25 +1775,30 @@ function findAppRoot(cwd) {
|
|
|
1678
1775
|
while (dir) {
|
|
1679
1776
|
const root = findUp("package.json", dir);
|
|
1680
1777
|
if (!root) break;
|
|
1681
|
-
const pkg = JSON.parse(
|
|
1682
|
-
|
|
1778
|
+
const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
|
|
1779
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1780
|
+
if (deps["@flaredev/core"] || deps.vinext) return root;
|
|
1683
1781
|
const parent = dirname4(root);
|
|
1684
1782
|
dir = parent === root ? void 0 : parent;
|
|
1685
1783
|
}
|
|
1686
|
-
throw new Error("Not inside a Flare app (no package.json depending on
|
|
1784
|
+
throw new Error("Not inside a Flare app (no package.json depending on @flaredev/core found). Run this from your app directory.");
|
|
1687
1785
|
}
|
|
1688
1786
|
function resolveBin(appRoot, pkgName, binName2) {
|
|
1689
|
-
const pkgDir = findUp(
|
|
1787
|
+
const pkgDir = findUp(join8("node_modules", pkgName, "package.json"), appRoot);
|
|
1690
1788
|
if (!pkgDir) {
|
|
1691
1789
|
throw new Error(`Cannot find ${pkgName} in ${appRoot}. Install dependencies first (e.g. pnpm install).`);
|
|
1692
1790
|
}
|
|
1693
|
-
const manifestPath =
|
|
1694
|
-
const { bin } = JSON.parse(
|
|
1791
|
+
const manifestPath = join8(pkgDir, "node_modules", pkgName, "package.json");
|
|
1792
|
+
const { bin } = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
1695
1793
|
const relativeBin = typeof bin === "string" ? bin : bin?.[binName2];
|
|
1696
1794
|
if (!relativeBin) throw new Error(`${pkgName} does not provide a "${binName2}" executable.`);
|
|
1697
|
-
return
|
|
1795
|
+
return join8(dirname4(manifestPath), relativeBin);
|
|
1698
1796
|
}
|
|
1699
1797
|
function delegatedArgv(appRoot, command, forwarded) {
|
|
1798
|
+
if (readStack(appRoot) === "next") {
|
|
1799
|
+
const script = { dev: "npm run dev", build: "npm run build", start: "npm start", deploy: "npm run deploy" }[command];
|
|
1800
|
+
throw new Error(`\`flare ${command}\` is for the Cloudflare stack. This app targets Next.js \u2014 run \`${script}\` instead.`);
|
|
1801
|
+
}
|
|
1700
1802
|
const spec = DELEGATED_COMMANDS[command];
|
|
1701
1803
|
return [resolveBin(appRoot, spec.pkg, spec.bin), ...spec.args, ...forwarded];
|
|
1702
1804
|
}
|
|
@@ -1755,7 +1857,7 @@ async function runDelegated(command, forwarded, cwd = process.cwd()) {
|
|
|
1755
1857
|
const tunnel = (command === "dev" || command === "start") && forwarded.includes("--tunnel");
|
|
1756
1858
|
if (tunnel) forwarded = forwarded.filter((arg) => arg !== "--tunnel");
|
|
1757
1859
|
const wantsHelp = forwarded.includes("--help") || forwarded.includes("-h");
|
|
1758
|
-
if (command === "start" && !wantsHelp && !
|
|
1860
|
+
if (command === "start" && !wantsHelp && !existsSync9(join8(appRoot, "dist/server/wrangler.json"))) {
|
|
1759
1861
|
console.log("No production build found; running `flare build` first.\n");
|
|
1760
1862
|
const code = await runNode(delegatedArgv(appRoot, "build", []), appRoot);
|
|
1761
1863
|
if (code !== 0) return code;
|
|
@@ -1805,9 +1907,9 @@ function snapshotColumns(snapshot) {
|
|
|
1805
1907
|
}
|
|
1806
1908
|
|
|
1807
1909
|
// src/generator/migrations.ts
|
|
1808
|
-
var listSql = (dir) =>
|
|
1910
|
+
var listSql = (dir) => existsSync10(dir) ? readdirSync4(dir).filter((file) => file.endsWith(".sql")) : [];
|
|
1809
1911
|
async function generateSchemaMigration(appRoot, name, options2 = {}) {
|
|
1810
|
-
const dir =
|
|
1912
|
+
const dir = join9(appRoot, "migrations");
|
|
1811
1913
|
const before = new Set(listSql(dir));
|
|
1812
1914
|
const bin = resolveBin(appRoot, "drizzle-kit", "drizzle-kit");
|
|
1813
1915
|
const { code, output } = await new Promise((resolve6, reject) => {
|
|
@@ -1835,10 +1937,10 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1835
1937
|
for (const file of created) {
|
|
1836
1938
|
const previous = previousSnapshot(dir, file);
|
|
1837
1939
|
if (!previous) continue;
|
|
1838
|
-
const path =
|
|
1839
|
-
const { sql: sql2, changes } = repairRebuildMigration(
|
|
1940
|
+
const path = join9(dir, file);
|
|
1941
|
+
const { sql: sql2, changes } = repairRebuildMigration(readFileSync7(path, "utf8"), snapshotColumns(previous));
|
|
1840
1942
|
if (changes.length) {
|
|
1841
|
-
|
|
1943
|
+
writeFileSync6(path, sql2);
|
|
1842
1944
|
repairs.push(...changes.map((change) => `${file}: ${change}`));
|
|
1843
1945
|
}
|
|
1844
1946
|
}
|
|
@@ -1846,32 +1948,32 @@ If it asked whether a column was renamed, run \`pnpm exec drizzle-kit generate\`
|
|
|
1846
1948
|
}
|
|
1847
1949
|
function previousSnapshot(dir, migrationFile) {
|
|
1848
1950
|
const prefix = /^(\d+)_/.exec(migrationFile)?.[1];
|
|
1849
|
-
const metaDir =
|
|
1850
|
-
if (!prefix || !
|
|
1951
|
+
const metaDir = join9(dir, "meta");
|
|
1952
|
+
if (!prefix || !existsSync10(metaDir)) return void 0;
|
|
1851
1953
|
const earlier = readdirSync4(metaDir).filter((file) => /^\d+_snapshot\.json$/.test(file) && file < `${prefix}_snapshot.json`).sort().pop();
|
|
1852
|
-
return earlier ? JSON.parse(
|
|
1954
|
+
return earlier ? JSON.parse(readFileSync7(join9(metaDir, earlier), "utf8")) : void 0;
|
|
1853
1955
|
}
|
|
1854
1956
|
|
|
1855
1957
|
// src/generator/plan.ts
|
|
1856
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1857
|
-
import { dirname as dirname5, join as
|
|
1958
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync8 } from "fs";
|
|
1959
|
+
import { dirname as dirname5, join as join11 } from "path";
|
|
1858
1960
|
import pc5 from "picocolors";
|
|
1859
1961
|
|
|
1860
1962
|
// src/generator/policy.ts
|
|
1861
|
-
import { existsSync as
|
|
1862
|
-
import { join as
|
|
1963
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
|
|
1964
|
+
import { join as join10, resolve as resolve5 } from "path";
|
|
1863
1965
|
import { camelCase as camelCase2, kebabCase as kebabCase2 } from "@flaredev/core";
|
|
1864
1966
|
import { createJiti as createJiti3 } from "jiti";
|
|
1865
1967
|
var policyPath = (name) => `policies/${kebabCase2(name)}.policy.ts`;
|
|
1866
1968
|
var policyLocal = (stem) => `${camelCase2(stem)}Policy`;
|
|
1867
1969
|
async function loadPolicies(root) {
|
|
1868
1970
|
const appRoot = resolve5(root);
|
|
1869
|
-
const dir =
|
|
1870
|
-
if (!
|
|
1871
|
-
const jiti = createJiti3(
|
|
1971
|
+
const dir = join10(appRoot, "policies");
|
|
1972
|
+
if (!existsSync11(dir)) return [];
|
|
1973
|
+
const jiti = createJiti3(join10(appRoot, "package.json"), { moduleCache: false, fsCache: false });
|
|
1872
1974
|
const loaded = [];
|
|
1873
1975
|
for (const file of readdirSync5(dir).filter((name) => name.endsWith(".policy.ts")).sort()) {
|
|
1874
|
-
const mod = await jiti.import(
|
|
1976
|
+
const mod = await jiti.import(join10(dir, file));
|
|
1875
1977
|
const policy = mod.default;
|
|
1876
1978
|
if (!policy || typeof policy !== "object" || !("resource" in policy) || !Array.isArray(policy.read)) {
|
|
1877
1979
|
throw new Error(`policies/${file} must \`export default definePolicy({ ... })\`.`);
|
|
@@ -1916,15 +2018,136 @@ function renderPolicyRegistry(all) {
|
|
|
1916
2018
|
].join("\n");
|
|
1917
2019
|
}
|
|
1918
2020
|
|
|
1919
|
-
// src/generator/
|
|
1920
|
-
import { camelCase as camelCase3, columnName,
|
|
1921
|
-
var resourceLocal = (stem) => `${camelCase3(stem)}Resource`;
|
|
1922
|
-
var tableExport = (resource) => camelCase3(resource.table);
|
|
2021
|
+
// src/generator/prisma.ts
|
|
2022
|
+
import { camelCase as camelCase3, columnName, pascalCase as pascalCase2, pluralize, storedFields } from "@flaredev/core";
|
|
1923
2023
|
var q = (value) => JSON.stringify(value);
|
|
2024
|
+
var enumTypeName = (resource, key) => `${pascalCase2(resource)}${pascalCase2(key)}`;
|
|
2025
|
+
var enumMember = (option) => {
|
|
2026
|
+
const safe = option.replace(/[^A-Za-z0-9_]/g, "_").replace(/^(?=\d)/, "v_");
|
|
2027
|
+
return safe === option ? ` ${safe}` : ` ${safe} @map(${q(option)})`;
|
|
2028
|
+
};
|
|
2029
|
+
function prismaType(resource, key, def) {
|
|
2030
|
+
switch (def.kind) {
|
|
2031
|
+
case "int":
|
|
2032
|
+
return "Int";
|
|
2033
|
+
case "float":
|
|
2034
|
+
return "Float";
|
|
2035
|
+
case "boolean":
|
|
2036
|
+
return "Boolean";
|
|
2037
|
+
case "date":
|
|
2038
|
+
case "datetime":
|
|
2039
|
+
return "DateTime";
|
|
2040
|
+
case "enum":
|
|
2041
|
+
return enumTypeName(resource.name, key);
|
|
2042
|
+
case "multiselect":
|
|
2043
|
+
return "String[]";
|
|
2044
|
+
default:
|
|
2045
|
+
return "String";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
function attributes(key, def) {
|
|
2049
|
+
const parts = [];
|
|
2050
|
+
const column = columnName(key);
|
|
2051
|
+
if (column !== key) parts.push(`@map(${q(column)})`);
|
|
2052
|
+
if ("unique" in def && def.unique) parts.push("@unique");
|
|
2053
|
+
if (def.kind === "text") parts.push("@db.Text");
|
|
2054
|
+
if (def.kind === "date") parts.push("@db.Date");
|
|
2055
|
+
if ("default" in def && def.default !== void 0) {
|
|
2056
|
+
const value = def.default;
|
|
2057
|
+
if (def.kind === "enum") parts.push(`@default(${String(value).replace(/[^A-Za-z0-9_]/g, "_")})`);
|
|
2058
|
+
else if (def.kind === "multiselect") parts.push(`@default([${value.map(q).join(", ")}])`);
|
|
2059
|
+
else parts.push(`@default(${typeof value === "string" ? q(value) : String(value)})`);
|
|
2060
|
+
}
|
|
2061
|
+
return parts;
|
|
2062
|
+
}
|
|
2063
|
+
var onDeleteAction = (onDelete) => onDelete === "set null" ? "SetNull" : onDelete === "cascade" ? "Cascade" : "Restrict";
|
|
2064
|
+
function renderPrismaEnums(resource) {
|
|
2065
|
+
return storedFields(resource).filter(([, def]) => def.kind === "enum").map(([key, def]) => {
|
|
2066
|
+
if (def.kind !== "enum") return "";
|
|
2067
|
+
return [`enum ${enumTypeName(resource.name, key)} {`, ...def.options.map(enumMember), "}"].join("\n");
|
|
2068
|
+
}).filter(Boolean);
|
|
2069
|
+
}
|
|
2070
|
+
function backReferences(target, all) {
|
|
2071
|
+
const found = [];
|
|
2072
|
+
for (const { resource: source } of all) {
|
|
2073
|
+
const pointing = storedFields(source).filter(([, def]) => def.kind === "belongsTo" && def.target === target.name);
|
|
2074
|
+
const ambiguous = pointing.length > 1;
|
|
2075
|
+
for (const [key, def] of pointing) {
|
|
2076
|
+
if (def.kind !== "belongsTo") continue;
|
|
2077
|
+
const declared = Object.entries(target.fields).find(([, other]) => other.kind === "hasMany" && other.target === source.name)?.[0];
|
|
2078
|
+
const base = ambiguous ? camelCase3(`${key.replace(/Id$/, "")} ${pluralize(source.name)}`) : declared ?? camelCase3(pluralize(source.name));
|
|
2079
|
+
found.push({ field: base, from: source.name, relationName: ambiguous ? `${source.name}_${key}` : void 0 });
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return found;
|
|
2083
|
+
}
|
|
2084
|
+
function renderPrismaModel(entry, all) {
|
|
2085
|
+
const resource = entry.resource;
|
|
2086
|
+
const byName = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
2087
|
+
const lines = [`model ${resource.name} {`, ` id String @id @default(uuid())`];
|
|
2088
|
+
const indexes = [];
|
|
2089
|
+
for (const [key, def] of Object.entries(resource.fields)) {
|
|
2090
|
+
if (def.kind === "hasMany") {
|
|
2091
|
+
if (!byName.get(def.target)) {
|
|
2092
|
+
throw new Error(`${resource.name}.${key} has many "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2093
|
+
}
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (def.kind === "belongsTo") {
|
|
2097
|
+
const target = byName.get(def.target);
|
|
2098
|
+
if (!target) throw new Error(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
2099
|
+
const optional2 = def.required ? "" : "?";
|
|
2100
|
+
const relationField = key.replace(/Id$/, "");
|
|
2101
|
+
const named = backReferences(target, all).find((back) => back.from === resource.name && back.relationName?.endsWith(`_${key}`));
|
|
2102
|
+
const relationArgs = [
|
|
2103
|
+
...named?.relationName ? [q(named.relationName)] : [],
|
|
2104
|
+
`fields: [${key}]`,
|
|
2105
|
+
"references: [id]",
|
|
2106
|
+
`onDelete: ${onDeleteAction(def.onDelete)}`
|
|
2107
|
+
];
|
|
2108
|
+
lines.push(` ${key} String${optional2} @map(${q(columnName(key))})`);
|
|
2109
|
+
lines.push(` ${relationField} ${target.name}${optional2} @relation(${relationArgs.join(", ")})`);
|
|
2110
|
+
indexes.push(` @@index([${key}])`);
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
const optional = def.required || def.kind === "multiselect" ? "" : "?";
|
|
2114
|
+
lines.push(` ${key} ${prismaType(resource, key, def)}${optional} ${attributes(key, def).join(" ")}`.trimEnd());
|
|
2115
|
+
}
|
|
2116
|
+
for (const back of backReferences(resource, all)) {
|
|
2117
|
+
const args = back.relationName ? `@relation(${q(back.relationName)})` : "";
|
|
2118
|
+
lines.push(` ${back.field} ${back.from}[] ${args}`.trimEnd());
|
|
2119
|
+
}
|
|
2120
|
+
lines.push(` createdAt DateTime @default(now()) @map("created_at")`);
|
|
2121
|
+
lines.push(` updatedAt DateTime @updatedAt @map("updated_at")`);
|
|
2122
|
+
indexes.push(` @@index([createdAt])`);
|
|
2123
|
+
const sortField = resource.defaultSort.field;
|
|
2124
|
+
if (!["createdAt", "updatedAt"].includes(sortField) && resource.fields[sortField]) indexes.push(` @@index([${sortField}])`);
|
|
2125
|
+
for (const [key, def] of storedFields(resource)) {
|
|
2126
|
+
if (def.kind === "enum" && def.filterable !== false) indexes.push(` @@index([${key}])`);
|
|
2127
|
+
}
|
|
2128
|
+
lines.push("", ...[...new Set(indexes)], ` @@map(${q(resource.table)})`, "}");
|
|
2129
|
+
return lines.join("\n");
|
|
2130
|
+
}
|
|
2131
|
+
function renderPrismaSchema(all) {
|
|
2132
|
+
const ordered = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2133
|
+
const blocks = [];
|
|
2134
|
+
for (const entry of ordered) {
|
|
2135
|
+
blocks.push(...renderPrismaEnums(entry.resource));
|
|
2136
|
+
blocks.push(renderPrismaModel(entry, all));
|
|
2137
|
+
}
|
|
2138
|
+
return `${blocks.join("\n\n")}
|
|
2139
|
+
`;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/generator/render.ts
|
|
2143
|
+
import { camelCase as camelCase4, columnName as columnName2, relationGraph, storedFields as storedFields2 } from "@flaredev/core";
|
|
2144
|
+
var resourceLocal = (stem) => `${camelCase4(stem)}Resource`;
|
|
2145
|
+
var tableExport = (resource) => camelCase4(resource.table);
|
|
2146
|
+
var q2 = (value) => JSON.stringify(value);
|
|
1924
2147
|
var sqlString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
1925
2148
|
var TIMESTAMP_DEFAULT = "sql`(cast(unixepoch('subsecond') * 1000 as integer))`";
|
|
1926
2149
|
function columnExpression(key, def, resources, selfName) {
|
|
1927
|
-
const col =
|
|
2150
|
+
const col = q2(columnName2(key));
|
|
1928
2151
|
let expr;
|
|
1929
2152
|
switch (def.kind) {
|
|
1930
2153
|
case "int":
|
|
@@ -1937,10 +2160,10 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1937
2160
|
expr = `integer(${col}, { mode: "boolean" })`;
|
|
1938
2161
|
break;
|
|
1939
2162
|
case "enum":
|
|
1940
|
-
expr = `text(${col}, { enum: ${
|
|
2163
|
+
expr = `text(${col}, { enum: ${q2(def.options)} })`;
|
|
1941
2164
|
break;
|
|
1942
2165
|
case "multiselect":
|
|
1943
|
-
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) =>
|
|
2166
|
+
expr = `text(${col}, { mode: "json" }).$type<(${def.options.map((option) => q2(option)).join(" | ")})[]>()`;
|
|
1944
2167
|
break;
|
|
1945
2168
|
case "belongsTo": {
|
|
1946
2169
|
const target = resources.get(def.target);
|
|
@@ -1948,7 +2171,7 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1948
2171
|
throw new Error(`${selfName}.${key} belongs to "${def.target}", but there is no ${def.target} resource. Generate it first.`);
|
|
1949
2172
|
}
|
|
1950
2173
|
const onDelete = def.onDelete ?? "restrict";
|
|
1951
|
-
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${
|
|
2174
|
+
expr = `text(${col}).references(() => ${tableExport(target)}.id, { onDelete: ${q2(onDelete)} })`;
|
|
1952
2175
|
break;
|
|
1953
2176
|
}
|
|
1954
2177
|
default:
|
|
@@ -1956,13 +2179,13 @@ function columnExpression(key, def, resources, selfName) {
|
|
|
1956
2179
|
}
|
|
1957
2180
|
if (def.required) expr += ".notNull()";
|
|
1958
2181
|
if ("unique" in def && def.unique) expr += ".unique()";
|
|
1959
|
-
if ("default" in def && def.default !== void 0) expr += `.default(${
|
|
2182
|
+
if ("default" in def && def.default !== void 0) expr += `.default(${q2(def.default)})`;
|
|
1960
2183
|
return expr;
|
|
1961
2184
|
}
|
|
1962
2185
|
function renderTableModule(entry, all) {
|
|
1963
2186
|
const { resource } = entry;
|
|
1964
2187
|
const resources = new Map(all.map((item) => [item.resource.name, item.resource]));
|
|
1965
|
-
const fields =
|
|
2188
|
+
const fields = storedFields2(resource);
|
|
1966
2189
|
const name = tableExport(resource);
|
|
1967
2190
|
const columns = fields.map(([key, def]) => ` ${key}: ${columnExpression(key, def, resources, resource.name)},`);
|
|
1968
2191
|
const extras = [];
|
|
@@ -1970,12 +2193,12 @@ function renderTableModule(entry, all) {
|
|
|
1970
2193
|
const addIndex = (key, column) => {
|
|
1971
2194
|
if (indexed.has(key)) return;
|
|
1972
2195
|
indexed.add(key);
|
|
1973
|
-
extras.push(` index(${
|
|
2196
|
+
extras.push(` index(${q2(`${resource.table}_${columnName2(column)}_idx`)}).on(table.${key}),`);
|
|
1974
2197
|
};
|
|
1975
2198
|
for (const [key, def] of fields) {
|
|
1976
2199
|
if (def.kind === "enum") {
|
|
1977
2200
|
const options2 = def.options.map(sqlString).join(", ");
|
|
1978
|
-
extras.push(` check(${
|
|
2201
|
+
extras.push(` check(${q2(`${resource.table}_${columnName2(key)}_check`)}, sql\`\${table.${key}} in (${options2})\`),`);
|
|
1979
2202
|
if (def.filterable !== false) addIndex(key, key);
|
|
1980
2203
|
}
|
|
1981
2204
|
if (def.kind === "belongsTo") addIndex(key, key);
|
|
@@ -1997,7 +2220,7 @@ function renderTableModule(entry, all) {
|
|
|
1997
2220
|
...targets.map((target) => `import { ${tableExport(target)} } from "./${target.table}";`),
|
|
1998
2221
|
"",
|
|
1999
2222
|
`export const ${name} = sqliteTable(`,
|
|
2000
|
-
` ${
|
|
2223
|
+
` ${q2(resource.table)},`,
|
|
2001
2224
|
" {",
|
|
2002
2225
|
` id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),`,
|
|
2003
2226
|
...columns.map(
|
|
@@ -2037,12 +2260,12 @@ function renderRelations(all) {
|
|
|
2037
2260
|
const target = byName.get(relation.target);
|
|
2038
2261
|
used.add(target);
|
|
2039
2262
|
const other = tableExport(target);
|
|
2040
|
-
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${
|
|
2263
|
+
return ` ${relation.name}: one(${other}, { fields: [${self}.${relation.key}], references: [${other}.id], relationName: ${q2(relation.relationName)} }),`;
|
|
2041
2264
|
}),
|
|
2042
2265
|
...hasMany.map((relation) => {
|
|
2043
2266
|
const target = byName.get(relation.target);
|
|
2044
2267
|
used.add(target);
|
|
2045
|
-
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${
|
|
2268
|
+
return ` ${relation.key}: many(${tableExport(target)}, { relationName: ${q2(relation.relationName)} }),`;
|
|
2046
2269
|
})
|
|
2047
2270
|
];
|
|
2048
2271
|
const helpers = [belongsTo.length ? "one" : "", hasMany.length ? "many" : ""].filter(Boolean).join(", ");
|
|
@@ -2055,32 +2278,40 @@ ${lines.join("\n")}
|
|
|
2055
2278
|
const imports = [...used].sort((a, b) => a.table.localeCompare(b.table)).map((resource) => `import { ${tableExport(resource)} } from "./schema/${resource.table}";`);
|
|
2056
2279
|
return [`import { relations } from "drizzle-orm";`, ...imports, "", blocks.join("\n")].join("\n");
|
|
2057
2280
|
}
|
|
2058
|
-
var handlerImports = (entry) =>
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2281
|
+
var handlerImports = (entry, stack = "cloudflare") => {
|
|
2282
|
+
const local = resourceLocal(entry.stem);
|
|
2283
|
+
const source = stack === "next" ? {
|
|
2284
|
+
imports: [`import { prismaRows } from "@flaredev/core/server";`, `import { prisma } from "@/lib/db";`],
|
|
2285
|
+
wiring: [` rows: prismaRows(prisma.${camelCase4(entry.resource.name)}, prisma),`]
|
|
2286
|
+
} : {
|
|
2287
|
+
imports: [`import { getDb } from "@/db";`, `import { ${tableExport(entry.resource)} } from "@/db/schema";`],
|
|
2288
|
+
wiring: [` table: ${tableExport(entry.resource)},`, ` getDb,`]
|
|
2289
|
+
};
|
|
2290
|
+
return [
|
|
2291
|
+
`import { createResourceHandlers } from "@flaredev/core/server";`,
|
|
2292
|
+
...source.imports,
|
|
2293
|
+
`import { authorize, currentUser } from "@/lib/api";`,
|
|
2294
|
+
`import { revalidateResource } from "@/lib/cache";`,
|
|
2295
|
+
`import ${local} from "@/resources/${entry.stem}.resource";`,
|
|
2296
|
+
"",
|
|
2297
|
+
`const handlers = createResourceHandlers({`,
|
|
2298
|
+
` resource: ${local},`,
|
|
2299
|
+
...source.wiring,
|
|
2300
|
+
` authorize,`,
|
|
2301
|
+
` currentUser,`,
|
|
2302
|
+
` onChange: revalidateResource,`,
|
|
2303
|
+
`});`,
|
|
2304
|
+
""
|
|
2305
|
+
];
|
|
2306
|
+
};
|
|
2307
|
+
function renderCollectionRoute(entry, stack = "cloudflare") {
|
|
2308
|
+
return [...handlerImports(entry, stack), "export const GET = handlers.collection.GET;", "export const POST = handlers.collection.POST;", ""].join(
|
|
2078
2309
|
"\n"
|
|
2079
2310
|
);
|
|
2080
2311
|
}
|
|
2081
|
-
function renderItemRoute(entry) {
|
|
2312
|
+
function renderItemRoute(entry, stack = "cloudflare") {
|
|
2082
2313
|
return [
|
|
2083
|
-
...handlerImports(entry),
|
|
2314
|
+
...handlerImports(entry, stack),
|
|
2084
2315
|
"export const GET = handlers.item.GET;",
|
|
2085
2316
|
"export const PATCH = handlers.item.PATCH;",
|
|
2086
2317
|
"export const PUT = handlers.item.PUT;",
|
|
@@ -2159,7 +2390,7 @@ function renderClient(entry) {
|
|
|
2159
2390
|
`import { createResourceClient } from "@flaredev/core/client";`,
|
|
2160
2391
|
`import type ${local} from "./${stem}.resource";`,
|
|
2161
2392
|
"",
|
|
2162
|
-
`export const ${
|
|
2393
|
+
`export const ${camelCase4(stem)}Client = createResourceClient<typeof ${local}>(${q2(`/api/${resource.slug}`)});`,
|
|
2163
2394
|
"",
|
|
2164
2395
|
`export type ${resource.name} = typeof ${local}.$types.record;`,
|
|
2165
2396
|
`export type ${resource.name}Create = typeof ${local}.$types.create;`,
|
|
@@ -2173,7 +2404,7 @@ function renderValidators(entry) {
|
|
|
2173
2404
|
`import { createValidators } from "@flaredev/core";`,
|
|
2174
2405
|
`import ${local} from "./${entry.stem}.resource";`,
|
|
2175
2406
|
"",
|
|
2176
|
-
`export const ${
|
|
2407
|
+
`export const ${camelCase4(entry.stem)}Validators = createValidators(${local});`,
|
|
2177
2408
|
""
|
|
2178
2409
|
].join("\n");
|
|
2179
2410
|
}
|
|
@@ -2203,12 +2434,31 @@ function renderServerRegistry(all) {
|
|
|
2203
2434
|
""
|
|
2204
2435
|
].join("\n");
|
|
2205
2436
|
}
|
|
2206
|
-
function
|
|
2437
|
+
function renderPrismaServerRegistry(all) {
|
|
2438
|
+
if (all.length === 0) return "export const resourceTables = {} as const;\n";
|
|
2439
|
+
const sorted = [...all].sort((a, b) => a.resource.name.localeCompare(b.resource.name));
|
|
2440
|
+
return [
|
|
2441
|
+
`import { prisma } from "@/lib/db";`,
|
|
2442
|
+
`import { ${sorted.map(({ stem }) => resourceLocal(stem)).join(", ")} } from "./index";`,
|
|
2443
|
+
"",
|
|
2444
|
+
"export const resourceTables = {",
|
|
2445
|
+
...sorted.map(
|
|
2446
|
+
({ resource, stem }) => ` ${resource.name}: { resource: ${resourceLocal(stem)}, delegate: prisma.${camelCase4(resource.name)} },`
|
|
2447
|
+
),
|
|
2448
|
+
"} as const;",
|
|
2449
|
+
"",
|
|
2450
|
+
"export type ResourceName = keyof typeof resourceTables;",
|
|
2451
|
+
""
|
|
2452
|
+
].join("\n");
|
|
2453
|
+
}
|
|
2454
|
+
function resourceFiles(entry, all, stack = "cloudflare") {
|
|
2207
2455
|
const { resource, stem } = entry;
|
|
2208
2456
|
return [
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
{ path: `
|
|
2457
|
+
// The schema is the one file that belongs to a stack. On Next.js every model lives
|
|
2458
|
+
// in one prisma/schema.prisma, written by planFiles rather than per resource.
|
|
2459
|
+
...stack === "cloudflare" ? [{ path: `db/schema/${resource.table}.ts`, content: renderTableModule(entry, all) }] : [],
|
|
2460
|
+
{ path: `app/api/${resource.slug}/route.ts`, content: renderCollectionRoute(entry, stack) },
|
|
2461
|
+
{ path: `app/api/${resource.slug}/[id]/route.ts`, content: renderItemRoute(entry, stack) },
|
|
2212
2462
|
{ path: `resources/${stem}.client.ts`, content: renderClient(entry) },
|
|
2213
2463
|
{ path: `resources/${stem}.validators.ts`, content: renderValidators(entry) },
|
|
2214
2464
|
{ path: `app/dashboard/${resource.slug}/page.tsx`, content: renderAdminListPage(entry) },
|
|
@@ -2223,21 +2473,42 @@ var resourceHeader = (name) => `// Generated by \`flare gen resource ${name}\`.
|
|
|
2223
2473
|
// rewritten on regeneration; add your own code outside it.
|
|
2224
2474
|
`;
|
|
2225
2475
|
var HEADER_PATTERN = /^\/\/ Generated by `flare gen resource (\w+)`/;
|
|
2226
|
-
function planFiles(all, policies) {
|
|
2476
|
+
function planFiles(all, policies, stack = "cloudflare") {
|
|
2227
2477
|
const files = [];
|
|
2228
2478
|
for (const entry of all) {
|
|
2229
|
-
for (const file of resourceFiles(entry, all)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
|
|
2230
|
-
}
|
|
2231
|
-
files.push(
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2479
|
+
for (const file of resourceFiles(entry, all, stack)) files.push({ ...file, header: resourceHeader(entry.resource.name) });
|
|
2480
|
+
}
|
|
2481
|
+
files.push({
|
|
2482
|
+
path: "resources/index.ts",
|
|
2483
|
+
content: renderRegistry(all),
|
|
2484
|
+
header: "// Registry of every resource descriptor (maintained by flare gen).\n"
|
|
2485
|
+
});
|
|
2486
|
+
if (stack === "next") {
|
|
2487
|
+
files.push(
|
|
2488
|
+
// Prisma keeps every model in one schema and writes its own migrations, so there
|
|
2489
|
+
// is no per-table module and no relations file to maintain.
|
|
2490
|
+
{
|
|
2491
|
+
path: "prisma/schema/resources.prisma",
|
|
2492
|
+
content: renderPrismaSchema(all),
|
|
2493
|
+
header: "// Generated from the descriptors in resources/. Edit a descriptor, not this.\n"
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
path: "resources/server.ts",
|
|
2497
|
+
content: renderPrismaServerRegistry(all),
|
|
2498
|
+
header: "// Server-only: resource name \u2192 descriptor and Prisma delegate (maintained by flare gen).\n"
|
|
2499
|
+
}
|
|
2500
|
+
);
|
|
2501
|
+
} else {
|
|
2502
|
+
files.push(
|
|
2503
|
+
{ path: "db/relations.ts", content: renderRelations(all), header: "// Drizzle relations for every resource (maintained by flare gen).\n" },
|
|
2504
|
+
{
|
|
2505
|
+
path: "resources/server.ts",
|
|
2506
|
+
content: renderServerRegistry(all),
|
|
2507
|
+
header: "// Server-only: resource name \u2192 descriptor and table (maintained by flare gen).\n"
|
|
2508
|
+
},
|
|
2509
|
+
{ path: "db/schema.ts", content: renderSchemaIndex(all) }
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2241
2512
|
if (policies) {
|
|
2242
2513
|
files.push({
|
|
2243
2514
|
path: "policies/index.ts",
|
|
@@ -2249,7 +2520,7 @@ function planFiles(all, policies) {
|
|
|
2249
2520
|
}
|
|
2250
2521
|
function applyPlan(appRoot, plan, options2 = {}) {
|
|
2251
2522
|
return plan.map(({ path, content, header }) => {
|
|
2252
|
-
const absolute =
|
|
2523
|
+
const absolute = join11(appRoot, path);
|
|
2253
2524
|
const status = inspectGenerated(absolute, content);
|
|
2254
2525
|
if (status === "unmarked") return { path, status, outcome: "skipped-unmarked" };
|
|
2255
2526
|
if (status === "drift" && !options2.force) return { path, status, outcome: "skipped-drift" };
|
|
@@ -2268,30 +2539,30 @@ function applyPlan(appRoot, plan, options2 = {}) {
|
|
|
2268
2539
|
function findOrphans(appRoot, all) {
|
|
2269
2540
|
const planned = new Set(planFiles(all).map((file) => file.path));
|
|
2270
2541
|
const candidates = [];
|
|
2271
|
-
const list2 = (dir, match) =>
|
|
2542
|
+
const list2 = (dir, match) => existsSync12(join11(appRoot, dir)) ? readdirSync6(join11(appRoot, dir)).filter(match).map((name) => `${dir}/${name}`) : [];
|
|
2272
2543
|
candidates.push(...list2("db/schema", (name) => name.endsWith(".ts")));
|
|
2273
2544
|
candidates.push(...list2("resources", (name) => /\.(client|validators)\.ts$/.test(name)));
|
|
2274
2545
|
for (const dir of list2("app/api", () => true)) {
|
|
2275
|
-
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (
|
|
2546
|
+
for (const file of [`${dir}/route.ts`, `${dir}/[id]/route.ts`]) if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2276
2547
|
}
|
|
2277
2548
|
for (const dir of list2("app/dashboard", () => true)) {
|
|
2278
2549
|
for (const file of [`${dir}/page.tsx`, `${dir}/new/page.tsx`, `${dir}/[id]/page.tsx`, `${dir}/[id]/edit/page.tsx`]) {
|
|
2279
|
-
if (
|
|
2550
|
+
if (existsSync12(join11(appRoot, file))) candidates.push(file);
|
|
2280
2551
|
}
|
|
2281
2552
|
}
|
|
2282
2553
|
const orphans = [];
|
|
2283
2554
|
for (const path of candidates) {
|
|
2284
|
-
const match = HEADER_PATTERN.exec(
|
|
2555
|
+
const match = HEADER_PATTERN.exec(readFileSync8(join11(appRoot, path), "utf8"));
|
|
2285
2556
|
if (match && !planned.has(path)) orphans.push({ path, resource: match[1] });
|
|
2286
2557
|
}
|
|
2287
2558
|
return orphans;
|
|
2288
2559
|
}
|
|
2289
2560
|
function ensureSupportFiles(appRoot, log2) {
|
|
2290
2561
|
for (const path of ["lib/api.ts", "lib/cache.ts"]) {
|
|
2291
|
-
const target =
|
|
2292
|
-
if (
|
|
2562
|
+
const target = join11(appRoot, path);
|
|
2563
|
+
if (existsSync12(target)) continue;
|
|
2293
2564
|
mkdirSync4(dirname5(target), { recursive: true });
|
|
2294
|
-
copyFileSync2(
|
|
2565
|
+
copyFileSync2(join11(templatesDir, "app", path), target);
|
|
2295
2566
|
log2(`${pc5.green("create".padEnd(9))} ${path}`);
|
|
2296
2567
|
}
|
|
2297
2568
|
}
|
|
@@ -2319,15 +2590,15 @@ function billingFiles(appName) {
|
|
|
2319
2590
|
}
|
|
2320
2591
|
var RETIRED_FILES = ["app/api/billing/status/route.ts", "components/billing/subscribe-button.tsx"];
|
|
2321
2592
|
function appendVars(appRoot, target) {
|
|
2322
|
-
const path =
|
|
2323
|
-
const text =
|
|
2593
|
+
const path = join12(appRoot, target);
|
|
2594
|
+
const text = existsSync13(path) ? readFileSync9(path, "utf8") : "";
|
|
2324
2595
|
if (/STRIPE_SECRET_KEY/.test(text)) return false;
|
|
2325
2596
|
appendFileSync2(path, target === ".dev.vars" ? billingDevVarsEntries() : billingDevVarsExampleEntries());
|
|
2326
2597
|
return true;
|
|
2327
2598
|
}
|
|
2328
2599
|
function addStripeDependency(appRoot) {
|
|
2329
|
-
const path =
|
|
2330
|
-
const pkg = JSON.parse(
|
|
2600
|
+
const path = join12(appRoot, "package.json");
|
|
2601
|
+
const pkg = JSON.parse(readFileSync9(path, "utf8"));
|
|
2331
2602
|
pkg.dependencies ??= {};
|
|
2332
2603
|
if (pkg.dependencies.stripe) return false;
|
|
2333
2604
|
pkg.dependencies.stripe = STRIPE_DEP;
|
|
@@ -2336,14 +2607,14 @@ function addStripeDependency(appRoot) {
|
|
|
2336
2607
|
}
|
|
2337
2608
|
function writeBillingResource(appRoot, name, force, log2) {
|
|
2338
2609
|
const relative2 = descriptorPath(name);
|
|
2339
|
-
const path =
|
|
2340
|
-
if (!
|
|
2610
|
+
const path = join12(appRoot, relative2);
|
|
2611
|
+
if (!existsSync13(path)) {
|
|
2341
2612
|
mkdirSync5(dirname6(path), { recursive: true });
|
|
2342
|
-
|
|
2613
|
+
writeFileSync7(path, renderBillingDescriptor(name));
|
|
2343
2614
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2344
2615
|
return;
|
|
2345
2616
|
}
|
|
2346
|
-
const parts = splitMarkers(
|
|
2617
|
+
const parts = splitMarkers(readFileSync9(path, "utf8"));
|
|
2347
2618
|
if (!parts) throw new Error(`${relative2} has no generated fields block, so the billing fields can't be merged. Add them by hand: ${BILLING_RESOURCES[name]}`);
|
|
2348
2619
|
const merged = mergeBillingFields(name, parts.block);
|
|
2349
2620
|
if (!merged) {
|
|
@@ -2367,26 +2638,26 @@ async function genBilling(options2 = {}) {
|
|
|
2367
2638
|
for (const name of ["Plan", "Customer", "Purchase"]) writeBillingResource(appRoot, name, options2.force, log2);
|
|
2368
2639
|
for (const [name, roles] of Object.entries(BILLING_POLICIES)) {
|
|
2369
2640
|
const relative2 = policyPath(name);
|
|
2370
|
-
if (
|
|
2641
|
+
if (existsSync13(join12(appRoot, relative2))) {
|
|
2371
2642
|
log2(`${pc6.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2372
2643
|
continue;
|
|
2373
2644
|
}
|
|
2374
|
-
mkdirSync5(
|
|
2375
|
-
|
|
2645
|
+
mkdirSync5(join12(appRoot, "policies"), { recursive: true });
|
|
2646
|
+
writeFileSync7(join12(appRoot, relative2), renderPolicy(name, roles));
|
|
2376
2647
|
log2(`${pc6.green("create".padEnd(9))} ${relative2}`);
|
|
2377
2648
|
}
|
|
2378
2649
|
const files = [];
|
|
2379
|
-
const appName = JSON.parse(
|
|
2650
|
+
const appName = JSON.parse(readFileSync9(join12(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2380
2651
|
for (const { path, content } of billingFiles(appName)) {
|
|
2381
|
-
const outcome = writeGenerated(
|
|
2652
|
+
const outcome = writeGenerated(join12(appRoot, path), content, { header: billingHeader(), force: options2.force });
|
|
2382
2653
|
files.push({ path, status: "missing", outcome });
|
|
2383
2654
|
const label = outcome === "create" ? pc6.green("create") : outcome === "update" ? pc6.yellow("update") : pc6.dim("identical");
|
|
2384
2655
|
log2(`${label.padEnd(18)} ${path}`);
|
|
2385
2656
|
}
|
|
2386
2657
|
for (const path of RETIRED_FILES) {
|
|
2387
|
-
const absolute =
|
|
2388
|
-
if (!
|
|
2389
|
-
const parts = splitMarkers(
|
|
2658
|
+
const absolute = join12(appRoot, path);
|
|
2659
|
+
if (!existsSync13(absolute)) continue;
|
|
2660
|
+
const parts = splitMarkers(readFileSync9(absolute, "utf8"));
|
|
2390
2661
|
const untouched = parts && parts.before.includes("flare gen billing") && !parts.after.trim() && parts.hash !== void 0 && hashBlock(parts.block) === parts.hash;
|
|
2391
2662
|
if (untouched) {
|
|
2392
2663
|
unlinkSync(absolute);
|
|
@@ -2407,7 +2678,7 @@ async function genBilling(options2 = {}) {
|
|
|
2407
2678
|
}
|
|
2408
2679
|
const all = await loadResources(appRoot);
|
|
2409
2680
|
ensureSupportFiles(appRoot, log2);
|
|
2410
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
2681
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
2411
2682
|
let migrations = [];
|
|
2412
2683
|
if (!options2.skipMigration) {
|
|
2413
2684
|
const result = await generateSchemaMigration(appRoot, "billing");
|
|
@@ -2430,8 +2701,8 @@ async function genBilling(options2 = {}) {
|
|
|
2430
2701
|
}
|
|
2431
2702
|
|
|
2432
2703
|
// src/commands/gen-security.ts
|
|
2433
|
-
import { existsSync as
|
|
2434
|
-
import { dirname as dirname7, join as
|
|
2704
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
2705
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
2435
2706
|
import pc7 from "picocolors";
|
|
2436
2707
|
|
|
2437
2708
|
// src/generator/security.ts
|
|
@@ -2959,28 +3230,28 @@ function securityFiles(appName) {
|
|
|
2959
3230
|
}
|
|
2960
3231
|
var PASS_THROUGH_HEADER = /^\/\/ Request protection for worker\/index\.ts\.[\s\S]*?\n(?=\/\/ generated:start)/;
|
|
2961
3232
|
function writeOnce(appRoot, relative2, content, log2) {
|
|
2962
|
-
const path =
|
|
2963
|
-
if (
|
|
3233
|
+
const path = join13(appRoot, relative2);
|
|
3234
|
+
if (existsSync14(path)) {
|
|
2964
3235
|
log2(`${pc7.dim("kept".padEnd(9))} ${relative2} (already exists)`);
|
|
2965
3236
|
return;
|
|
2966
3237
|
}
|
|
2967
3238
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2968
|
-
|
|
3239
|
+
writeFileSync8(path, content);
|
|
2969
3240
|
log2(`${pc7.green("create".padEnd(9))} ${relative2}`);
|
|
2970
3241
|
}
|
|
2971
3242
|
async function genSecurity(options2 = {}) {
|
|
2972
3243
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
2973
3244
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
2974
|
-
const appName = JSON.parse(
|
|
3245
|
+
const appName = JSON.parse(readFileSync10(join13(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
2975
3246
|
writeOnce(appRoot, "security.config.ts", renderSecurityConfig(), log2);
|
|
2976
3247
|
writeOnce(appRoot, descriptorPath("SecurityEvent"), renderSecurityDescriptor(), log2);
|
|
2977
3248
|
writeOnce(appRoot, policyPath("SecurityEvent"), renderPolicy("SecurityEvent", SECURITY_EVENT_POLICY), log2);
|
|
2978
3249
|
const files = [];
|
|
2979
3250
|
const write = (path, content, header) => {
|
|
2980
|
-
const absolute =
|
|
2981
|
-
if (path === "lib/security.ts" &&
|
|
2982
|
-
const source =
|
|
2983
|
-
if (PASS_THROUGH_HEADER.test(source))
|
|
3251
|
+
const absolute = join13(appRoot, path);
|
|
3252
|
+
if (path === "lib/security.ts" && existsSync14(absolute)) {
|
|
3253
|
+
const source = readFileSync10(absolute, "utf8");
|
|
3254
|
+
if (PASS_THROUGH_HEADER.test(source)) writeFileSync8(absolute, source.replace(PASS_THROUGH_HEADER, securityHeader()));
|
|
2984
3255
|
}
|
|
2985
3256
|
const outcome = writeGenerated(absolute, content, { header, force: options2.force });
|
|
2986
3257
|
files.push({ path, status: "missing", outcome });
|
|
@@ -2988,16 +3259,16 @@ async function genSecurity(options2 = {}) {
|
|
|
2988
3259
|
log2(`${label.padEnd(18)} ${path}`);
|
|
2989
3260
|
};
|
|
2990
3261
|
for (const { path, content } of securityFiles(appName)) write(path, content, securityHeader());
|
|
2991
|
-
const navPath =
|
|
2992
|
-
if (
|
|
3262
|
+
const navPath = join13(appRoot, "lib/dashboard-nav.ts");
|
|
3263
|
+
if (existsSync14(navPath) && splitMarkers(readFileSync10(navPath, "utf8"))) write("lib/dashboard-nav.ts", renderAdminNavBlock());
|
|
2993
3264
|
else log2(pc7.yellow(`lib/dashboard-nav.ts not found: link to /dashboard/security from your dashboard sidebar yourself.`));
|
|
2994
|
-
const wranglerPath =
|
|
3265
|
+
const wranglerPath = join13(appRoot, "wrangler.jsonc");
|
|
2995
3266
|
let bindings = [];
|
|
2996
|
-
if (
|
|
2997
|
-
const { text, added } = addSecurityBindings(
|
|
3267
|
+
if (existsSync14(wranglerPath)) {
|
|
3268
|
+
const { text, added } = addSecurityBindings(readFileSync10(wranglerPath, "utf8"));
|
|
2998
3269
|
bindings = added;
|
|
2999
3270
|
if (added.length) {
|
|
3000
|
-
|
|
3271
|
+
writeFileSync8(wranglerPath, text);
|
|
3001
3272
|
for (const line of added) log2(`${pc7.yellow("update".padEnd(9))} wrangler.jsonc (${line})`);
|
|
3002
3273
|
}
|
|
3003
3274
|
} else {
|
|
@@ -3005,7 +3276,7 @@ async function genSecurity(options2 = {}) {
|
|
|
3005
3276
|
}
|
|
3006
3277
|
const all = await loadResources(appRoot);
|
|
3007
3278
|
ensureSupportFiles(appRoot, log2);
|
|
3008
|
-
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot)), { force: options2.force }), log2);
|
|
3279
|
+
logResults(applyPlan(appRoot, planFiles(all, await loadPolicies(appRoot), readStack(appRoot)), { force: options2.force }), log2);
|
|
3009
3280
|
let migrations = [];
|
|
3010
3281
|
if (!options2.skipMigration) {
|
|
3011
3282
|
const result = await generateSchemaMigration(appRoot, "security");
|
|
@@ -3028,15 +3299,15 @@ async function genSecurity(options2 = {}) {
|
|
|
3028
3299
|
}
|
|
3029
3300
|
|
|
3030
3301
|
// src/commands/theme.ts
|
|
3031
|
-
import { existsSync as
|
|
3032
|
-
import { join as
|
|
3302
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
3303
|
+
import { join as join14 } from "path";
|
|
3033
3304
|
import pc8 from "picocolors";
|
|
3034
3305
|
var THEME_LINE = /theme: "([a-z]+)" as ThemeName,/;
|
|
3035
3306
|
function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
3036
3307
|
const appRoot = findAppRoot(cwd);
|
|
3037
|
-
const path =
|
|
3038
|
-
if (!
|
|
3039
|
-
const source =
|
|
3308
|
+
const path = join14(appRoot, "lib/site.ts");
|
|
3309
|
+
if (!existsSync15(path)) throw new Error("lib/site.ts not found: this app was created before themes. Add it from the Flare app template.");
|
|
3310
|
+
const source = readFileSync11(path, "utf8");
|
|
3040
3311
|
const current = THEME_LINE.exec(source)?.[1];
|
|
3041
3312
|
if (!name) {
|
|
3042
3313
|
for (const [theme2, description] of Object.entries(THEMES)) {
|
|
@@ -3047,7 +3318,7 @@ function setTheme(name, cwd = process.cwd(), log2 = console.log) {
|
|
|
3047
3318
|
}
|
|
3048
3319
|
const theme = parseTheme(name);
|
|
3049
3320
|
if (!current) throw new Error('lib/site.ts has no `theme: "\u2026" as ThemeName,` line to change.');
|
|
3050
|
-
|
|
3321
|
+
writeFileSync9(path, source.replace(THEME_LINE, `theme: "${theme}" as ThemeName,`));
|
|
3051
3322
|
log2(`${pc8.green("\u2714")} Theme is now ${pc8.bold(theme)}. Restart \`flare dev\` (or rebuild) to see it.`);
|
|
3052
3323
|
}
|
|
3053
3324
|
|
|
@@ -3119,9 +3390,9 @@ async function askCreateQuestions(given, appName) {
|
|
|
3119
3390
|
}
|
|
3120
3391
|
|
|
3121
3392
|
// src/commands/gen.ts
|
|
3122
|
-
import { existsSync as
|
|
3123
|
-
import { dirname as dirname8, join as
|
|
3124
|
-
import { camelCase as
|
|
3393
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
3394
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
3395
|
+
import { camelCase as camelCase5, defineResource as defineResource2, relationGraph as relationGraph2 } from "@flaredev/core";
|
|
3125
3396
|
import pc10 from "picocolors";
|
|
3126
3397
|
function validateFields(name, fields, others, log2) {
|
|
3127
3398
|
const known = /* @__PURE__ */ new Set([name, ...others.map((resource) => resource.name)]);
|
|
@@ -3133,7 +3404,7 @@ function validateFields(name, fields, others, log2) {
|
|
|
3133
3404
|
const candidate = defineResource2({ name, fields: Object.fromEntries(fields.map((f) => [f.key, toField(f)])) });
|
|
3134
3405
|
const graph = relationGraph2([...others, candidate]);
|
|
3135
3406
|
for (const pending of graph.pending.filter((p) => p.resource === name)) {
|
|
3136
|
-
log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${
|
|
3407
|
+
log2(pc10.dim(`note: ${name}.${pending.key} will link up once ${pending.target} exists (give it a ${camelCase5(name)}Id: belongsTo(${name}) field).`));
|
|
3137
3408
|
}
|
|
3138
3409
|
}
|
|
3139
3410
|
async function genResource(rawName, options2) {
|
|
@@ -3141,8 +3412,8 @@ async function genResource(rawName, options2) {
|
|
|
3141
3412
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3142
3413
|
const name = resourceName(rawName);
|
|
3143
3414
|
const relativePath = descriptorPath(name);
|
|
3144
|
-
const path =
|
|
3145
|
-
const exists =
|
|
3415
|
+
const path = join15(appRoot, relativePath);
|
|
3416
|
+
const exists = existsSync16(path);
|
|
3146
3417
|
if (!exists && !options2.fields) {
|
|
3147
3418
|
throw new Error(`--fields is required, e.g. flare gen resource ${name} --fields "name:string, email:string"`);
|
|
3148
3419
|
}
|
|
@@ -3153,7 +3424,7 @@ async function genResource(rawName, options2) {
|
|
|
3153
3424
|
validateFields(name, fields, others, log2);
|
|
3154
3425
|
if (!exists) {
|
|
3155
3426
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
3156
|
-
|
|
3427
|
+
writeFileSync10(path, renderDescriptor(name, fields, { group: options2.group, icon: options2.icon }));
|
|
3157
3428
|
log2(`${pc10.green("create".padEnd(9))} ${relativePath}`);
|
|
3158
3429
|
} else {
|
|
3159
3430
|
try {
|
|
@@ -3175,7 +3446,7 @@ async function genResource(rawName, options2) {
|
|
|
3175
3446
|
const all = await loadResources(appRoot);
|
|
3176
3447
|
table = all.find(({ resource }) => resource.name === name).resource.table;
|
|
3177
3448
|
ensureSupportFiles(appRoot, log2);
|
|
3178
|
-
written = applyPlan(appRoot, planFiles(all), { force: options2.force });
|
|
3449
|
+
written = applyPlan(appRoot, planFiles(all, void 0, readStack(appRoot)), { force: options2.force });
|
|
3179
3450
|
logResults(written, log2);
|
|
3180
3451
|
} catch (error) {
|
|
3181
3452
|
const hint = exists ? "" : `
|
|
@@ -3189,22 +3460,27 @@ Skipped ${skipped.length} hand-edited file(s); run \`flare sync-types\` for deta
|
|
|
3189
3460
|
}
|
|
3190
3461
|
let migrations = [];
|
|
3191
3462
|
if (!options2.skipMigration) {
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3463
|
+
if (readStack(appRoot) === "next") {
|
|
3464
|
+
log2(`
|
|
3465
|
+
Next: ${pc10.bold("npx prisma migrate dev")} to turn the schema into a migration and apply it.`);
|
|
3466
|
+
} else {
|
|
3467
|
+
const result = await generateSchemaMigration(appRoot, `${exists ? "update" : "create"}_${table}`);
|
|
3468
|
+
migrations = result.files;
|
|
3469
|
+
for (const file of migrations) log2(`${pc10.green("create".padEnd(9))} ${file}`);
|
|
3470
|
+
for (const repair of result.repairs) log2(pc10.dim(`repaired ${repair}`));
|
|
3471
|
+
if (migrations.length) log2(`
|
|
3197
3472
|
Next: ${pc10.bold("flare migrate")} to apply it locally.`);
|
|
3198
|
-
|
|
3473
|
+
else if (exists) log2(pc10.dim("\nNo table changes, so no migration."));
|
|
3474
|
+
}
|
|
3199
3475
|
}
|
|
3200
3476
|
const descriptor = { path: relativePath, status: exists ? "outdated" : "missing", outcome: exists ? "update" : "create" };
|
|
3201
3477
|
return { name, mode: exists ? "update" : "create", written: [descriptor, ...written], migrations };
|
|
3202
3478
|
}
|
|
3203
3479
|
|
|
3204
3480
|
// src/commands/gen-endpoint.ts
|
|
3205
|
-
import { existsSync as
|
|
3206
|
-
import { join as
|
|
3207
|
-
import { kebabCase as kebabCase3, pascalCase as
|
|
3481
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
3482
|
+
import { join as join16 } from "path";
|
|
3483
|
+
import { kebabCase as kebabCase3, pascalCase as pascalCase3 } from "@flaredev/core";
|
|
3208
3484
|
import pc11 from "picocolors";
|
|
3209
3485
|
var METHODS = ["GET", "POST", "PATCH", "PUT", "DELETE"];
|
|
3210
3486
|
var ACTIONS = ["read", "create", "update", "delete"];
|
|
@@ -3216,7 +3492,7 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3216
3492
|
const name = kebabCase3(rawName);
|
|
3217
3493
|
if (!name) throw new Error(`Invalid endpoint name "${rawName}".`);
|
|
3218
3494
|
const resources = await loadResources(appRoot);
|
|
3219
|
-
const wanted =
|
|
3495
|
+
const wanted = pascalCase3(resourceName2);
|
|
3220
3496
|
const entry = resources.find(({ resource: resource2 }) => resource2.name === wanted);
|
|
3221
3497
|
if (!entry) {
|
|
3222
3498
|
throw new Error(`No resource "${resourceName2}". Available: ${resources.map(({ resource: resource2 }) => resource2.name).join(", ") || "none"}.`);
|
|
@@ -3225,11 +3501,11 @@ async function genEndpoint(resourceName2, rawName, options2 = {}) {
|
|
|
3225
3501
|
if (!ACTIONS.includes(action)) throw new Error(`Unknown action "${action}". Use one of ${ACTIONS.join(", ")}.`);
|
|
3226
3502
|
const { resource, stem } = entry;
|
|
3227
3503
|
const relative2 = options2.record ? `app/api/${resource.slug}/[id]/${name}/route.ts` : `app/api/${resource.slug}/${name}/route.ts`;
|
|
3228
|
-
const path =
|
|
3229
|
-
if (
|
|
3230
|
-
mkdirSync8(
|
|
3504
|
+
const path = join16(appRoot, relative2);
|
|
3505
|
+
if (existsSync17(path)) throw new Error(`${relative2} already exists.`);
|
|
3506
|
+
mkdirSync8(join16(path, ".."), { recursive: true });
|
|
3231
3507
|
const route = options2.record ? `/api/${resource.slug}/[id]/${name}` : `/api/${resource.slug}/${name}`;
|
|
3232
|
-
|
|
3508
|
+
writeFileSync11(path, renderEndpoint({ resource: resource.name, stem, method, action, record: Boolean(options2.record), name, route }));
|
|
3233
3509
|
log2(`${pc11.green("create".padEnd(9))} ${relative2}`);
|
|
3234
3510
|
log2(`
|
|
3235
3511
|
It's an ordinary route handler \u2014 edit it freely; regeneration leaves it alone.`);
|
|
@@ -3265,8 +3541,8 @@ ${body}
|
|
|
3265
3541
|
}
|
|
3266
3542
|
|
|
3267
3543
|
// src/commands/gen-migration.ts
|
|
3268
|
-
import { existsSync as
|
|
3269
|
-
import { basename as basename2, join as
|
|
3544
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, writeFileSync as writeFileSync12 } from "fs";
|
|
3545
|
+
import { basename as basename2, join as join17 } from "path";
|
|
3270
3546
|
import { snakeCase } from "@flaredev/core";
|
|
3271
3547
|
import pc12 from "picocolors";
|
|
3272
3548
|
var DOWN_TEMPLATE = (up) => `-- Rollback for ${up}, run by \`flare migrate:rollback\`.
|
|
@@ -3287,12 +3563,12 @@ async function genMigration(rawName, options2 = {}) {
|
|
|
3287
3563
|
for (const file of files) log2(`${pc12.green("create".padEnd(9))} ${file}`);
|
|
3288
3564
|
for (const repair of repairs) log2(pc12.dim(`repaired ${repair}`));
|
|
3289
3565
|
if (!options2.fromSchema) {
|
|
3290
|
-
const downDir =
|
|
3566
|
+
const downDir = join17(appRoot, "migrations", "down");
|
|
3291
3567
|
mkdirSync9(downDir, { recursive: true });
|
|
3292
3568
|
for (const file of files) {
|
|
3293
|
-
const down =
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3569
|
+
const down = join17(downDir, basename2(file));
|
|
3570
|
+
if (existsSync18(down)) continue;
|
|
3571
|
+
writeFileSync12(down, DOWN_TEMPLATE(basename2(file)));
|
|
3296
3572
|
log2(`${pc12.green("create".padEnd(9))} migrations/down/${basename2(file)}`);
|
|
3297
3573
|
}
|
|
3298
3574
|
log2(`
|
|
@@ -3305,8 +3581,8 @@ Next: ${pc12.bold("flare migrate")}.`);
|
|
|
3305
3581
|
}
|
|
3306
3582
|
|
|
3307
3583
|
// src/commands/gen-policy.ts
|
|
3308
|
-
import { existsSync as
|
|
3309
|
-
import { dirname as dirname9, join as
|
|
3584
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
3585
|
+
import { dirname as dirname9, join as join18 } from "path";
|
|
3310
3586
|
import { definePolicy } from "@flaredev/core";
|
|
3311
3587
|
import pc13 from "picocolors";
|
|
3312
3588
|
function parseRoles(input, what = "--roles") {
|
|
@@ -3324,8 +3600,8 @@ async function genPolicy(rawName, options2) {
|
|
|
3324
3600
|
throw new Error(`No resource "${name}". Generate it first. Known resources: ${known}.`);
|
|
3325
3601
|
}
|
|
3326
3602
|
const relative2 = policyPath(name);
|
|
3327
|
-
const path =
|
|
3328
|
-
const exists =
|
|
3603
|
+
const path = join18(appRoot, relative2);
|
|
3604
|
+
const exists = existsSync19(path);
|
|
3329
3605
|
let roles;
|
|
3330
3606
|
let deleteRoles;
|
|
3331
3607
|
if (options2.roles || !exists) {
|
|
@@ -3337,7 +3613,7 @@ async function genPolicy(rawName, options2) {
|
|
|
3337
3613
|
const block = renderPolicyBlock({ read: roles, create: roles, update: roles, delete: deleteRoles });
|
|
3338
3614
|
if (!exists) {
|
|
3339
3615
|
mkdirSync10(dirname9(path), { recursive: true });
|
|
3340
|
-
|
|
3616
|
+
writeFileSync13(path, renderPolicy(name, { read: roles, create: roles, update: roles, delete: deleteRoles }));
|
|
3341
3617
|
log2(`${pc13.green("create".padEnd(9))} ${relative2}`);
|
|
3342
3618
|
} else {
|
|
3343
3619
|
try {
|
|
@@ -3352,16 +3628,16 @@ async function genPolicy(rawName, options2) {
|
|
|
3352
3628
|
}
|
|
3353
3629
|
}
|
|
3354
3630
|
const policies = await loadPolicies(appRoot);
|
|
3355
|
-
logResults(applyPlan(appRoot, planFiles(resources, policies)), log2);
|
|
3631
|
+
logResults(applyPlan(appRoot, planFiles(resources, policies, readStack(appRoot))), log2);
|
|
3356
3632
|
log2(`
|
|
3357
3633
|
${pc13.dim("Roles come from `flare role:add`; the API and the admin both enforce this policy.")}`);
|
|
3358
3634
|
return { name, path: relative2 };
|
|
3359
3635
|
}
|
|
3360
3636
|
|
|
3361
3637
|
// src/commands/migrate.ts
|
|
3362
|
-
import { existsSync as
|
|
3638
|
+
import { existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync3, writeFileSync as writeFileSync14 } from "fs";
|
|
3363
3639
|
import { tmpdir } from "os";
|
|
3364
|
-
import { join as
|
|
3640
|
+
import { join as join19 } from "path";
|
|
3365
3641
|
import pc14 from "picocolors";
|
|
3366
3642
|
|
|
3367
3643
|
// src/generator/down.ts
|
|
@@ -3455,7 +3731,17 @@ function context(options2) {
|
|
|
3455
3731
|
};
|
|
3456
3732
|
}
|
|
3457
3733
|
async function migrate(options2 = {}) {
|
|
3458
|
-
const
|
|
3734
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
3735
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
3736
|
+
if (readStack(appRoot) === "next") {
|
|
3737
|
+
if (options2.remote) {
|
|
3738
|
+
log2(pc14.dim("On Next.js, `--remote` is whichever database DATABASE_URL points at."));
|
|
3739
|
+
}
|
|
3740
|
+
const prisma = resolveBin(appRoot, "prisma", "prisma");
|
|
3741
|
+
log2(`${pc14.cyan("prisma")} migrate deploy`);
|
|
3742
|
+
return runNode([prisma, "migrate", "deploy"], appRoot);
|
|
3743
|
+
}
|
|
3744
|
+
const { db, target, wrangler } = context(options2);
|
|
3459
3745
|
const result = await runWrangler(wrangler, ["d1", "migrations", "apply", db.binding, ...target], appRoot);
|
|
3460
3746
|
return result.code;
|
|
3461
3747
|
}
|
|
@@ -3479,7 +3765,7 @@ async function rollback(options2 = {}) {
|
|
|
3479
3765
|
const steps = options2.steps ?? 1;
|
|
3480
3766
|
if (!Number.isInteger(steps) || steps < 1) throw new Error("--steps must be a positive integer.");
|
|
3481
3767
|
const { appRoot, db, target, where, wrangler, log: log2 } = context(options2);
|
|
3482
|
-
const migrationsDir =
|
|
3768
|
+
const migrationsDir = join19(appRoot, db.migrations_dir ?? "migrations");
|
|
3483
3769
|
const applied = await appliedMigrations(options2, steps);
|
|
3484
3770
|
if (applied.length === 0) {
|
|
3485
3771
|
log2("Nothing to roll back.");
|
|
@@ -3488,18 +3774,18 @@ async function rollback(options2 = {}) {
|
|
|
3488
3774
|
const plan = [];
|
|
3489
3775
|
for (const { name } of applied) {
|
|
3490
3776
|
if (!/^[\w.-]+\.sql$/.test(name)) throw new Error(`Unexpected migration name "${name}" in d1_migrations.`);
|
|
3491
|
-
const handWritten =
|
|
3492
|
-
if (
|
|
3493
|
-
const sql2 =
|
|
3777
|
+
const handWritten = join19(migrationsDir, "down", name);
|
|
3778
|
+
if (existsSync20(handWritten)) {
|
|
3779
|
+
const sql2 = readFileSync12(handWritten, "utf8");
|
|
3494
3780
|
if (splitStatements(sql2).length === 0) {
|
|
3495
3781
|
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.`);
|
|
3496
3782
|
}
|
|
3497
3783
|
plan.push({ name, sql: sql2, source: `migrations/down/${name}` });
|
|
3498
3784
|
continue;
|
|
3499
3785
|
}
|
|
3500
|
-
const upPath =
|
|
3501
|
-
if (!
|
|
3502
|
-
const down = deriveDown(
|
|
3786
|
+
const upPath = join19(migrationsDir, name);
|
|
3787
|
+
if (!existsSync20(upPath)) throw new Error(`${name} was applied but migrations/${name} is missing, so it can't be reversed.`);
|
|
3788
|
+
const down = deriveDown(readFileSync12(upPath, "utf8"));
|
|
3503
3789
|
if (!down.reversible) {
|
|
3504
3790
|
throw new Error(
|
|
3505
3791
|
`Can't roll back ${name}: ${down.reason}
|
|
@@ -3519,15 +3805,15 @@ Write the reverse SQL to migrations/down/${name} and run the rollback again. Not
|
|
|
3519
3805
|
}
|
|
3520
3806
|
const script = plan.map(({ name, sql: sql2 }) => `${sql2.trim()}
|
|
3521
3807
|
DELETE FROM d1_migrations WHERE name = '${name.replace(/'/g, "''")}';`).join("\n") + "\n";
|
|
3522
|
-
const dir = mkdtempSync(
|
|
3808
|
+
const dir = mkdtempSync(join19(tmpdir(), "flare-rollback-"));
|
|
3523
3809
|
try {
|
|
3524
|
-
const file =
|
|
3525
|
-
|
|
3810
|
+
const file = join19(dir, "rollback.sql");
|
|
3811
|
+
writeFileSync14(file, script);
|
|
3526
3812
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
3527
3813
|
if (result.code !== 0) throw new Error(`Rollback failed:
|
|
3528
3814
|
${result.output.trim()}`);
|
|
3529
3815
|
} finally {
|
|
3530
|
-
|
|
3816
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
3531
3817
|
}
|
|
3532
3818
|
log2(pc14.green(`
|
|
3533
3819
|
Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
@@ -3535,8 +3821,8 @@ Rolled back ${plan.map((step) => step.name).join(", ")}.`));
|
|
|
3535
3821
|
}
|
|
3536
3822
|
|
|
3537
3823
|
// src/commands/rm.ts
|
|
3538
|
-
import { existsSync as
|
|
3539
|
-
import { dirname as dirname10, join as
|
|
3824
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7, readFileSync as readFileSync13, rmdirSync, unlinkSync as unlinkSync2 } from "fs";
|
|
3825
|
+
import { dirname as dirname10, join as join20 } from "path";
|
|
3540
3826
|
import { relationGraph as relationGraph3 } from "@flaredev/core";
|
|
3541
3827
|
import pc15 from "picocolors";
|
|
3542
3828
|
var squash = (text) => text.replace(/\r\n/g, "\n").replace(/\s+/g, " ").trim();
|
|
@@ -3555,10 +3841,10 @@ function handWrittenReason(source, header, options2 = {}) {
|
|
|
3555
3841
|
return null;
|
|
3556
3842
|
}
|
|
3557
3843
|
function removeEmptyDirs(appRoot, start) {
|
|
3558
|
-
let dir = dirname10(
|
|
3559
|
-
const roots = [
|
|
3844
|
+
let dir = dirname10(join20(appRoot, start));
|
|
3845
|
+
const roots = [join20(appRoot, "app", "api"), join20(appRoot, "app", "dashboard")];
|
|
3560
3846
|
const inside = () => roots.some((root) => dir.startsWith(root) && dir !== root);
|
|
3561
|
-
while (inside() &&
|
|
3847
|
+
while (inside() && existsSync21(dir) && readdirSync7(dir).length === 0) {
|
|
3562
3848
|
rmdirSync(dir);
|
|
3563
3849
|
dir = dirname10(dir);
|
|
3564
3850
|
}
|
|
@@ -3586,8 +3872,8 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3586
3872
|
const owned = /* @__PURE__ */ new Set();
|
|
3587
3873
|
if (entry) {
|
|
3588
3874
|
owned.add(entry.file);
|
|
3589
|
-
for (const file of planFiles(all)) {
|
|
3590
|
-
if (
|
|
3875
|
+
for (const file of planFiles(all, void 0, readStack(appRoot))) {
|
|
3876
|
+
if (existsSync21(join20(appRoot, file.path)) && file.header === resourceHeader(name)) owned.add(file.path);
|
|
3591
3877
|
}
|
|
3592
3878
|
}
|
|
3593
3879
|
for (const orphan of findOrphans(appRoot, remaining)) if (orphan.resource === name) owned.add(orphan.path);
|
|
@@ -3596,7 +3882,7 @@ async function rmResource(rawName, options2 = {}) {
|
|
|
3596
3882
|
if (!entry && owned.size === 0) throw new Error(`No resource "${name}" (no ${descriptorPath(name)} and no generated files).`);
|
|
3597
3883
|
const findings = [];
|
|
3598
3884
|
for (const path of owned) {
|
|
3599
|
-
const source =
|
|
3885
|
+
const source = readFileSync13(join20(appRoot, path), "utf8");
|
|
3600
3886
|
let wrapper;
|
|
3601
3887
|
if (entry && path === entry.file) {
|
|
3602
3888
|
const standard = splitMarkers(renderDescriptor(name, [{ key: "placeholder", kind: "string", required: true, unique: false }]));
|
|
@@ -3617,15 +3903,15 @@ Move what you want to keep, or pass --force to delete anyway.`);
|
|
|
3617
3903
|
}
|
|
3618
3904
|
const removed = [...owned].sort();
|
|
3619
3905
|
for (const path of removed) {
|
|
3620
|
-
unlinkSync2(
|
|
3906
|
+
unlinkSync2(join20(appRoot, path));
|
|
3621
3907
|
removeEmptyDirs(appRoot, path);
|
|
3622
3908
|
log2(`${pc15.red("remove".padEnd(9))} ${path}`);
|
|
3623
3909
|
}
|
|
3624
|
-
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0)), log2);
|
|
3910
|
+
logResults(applyPlan(appRoot, planFiles(remaining, policy ? await loadPolicies(appRoot) : void 0, readStack(appRoot))), log2);
|
|
3625
3911
|
const table = entry ? tableExport(entry.resource) : void 0;
|
|
3626
|
-
if (table &&
|
|
3627
|
-
const seeds = readdirSync7(
|
|
3628
|
-
(file) => new RegExp(`\\b${table}\\b`).test(
|
|
3912
|
+
if (table && existsSync21(join20(appRoot, "seeds"))) {
|
|
3913
|
+
const seeds = readdirSync7(join20(appRoot, "seeds")).filter(
|
|
3914
|
+
(file) => new RegExp(`\\b${table}\\b`).test(readFileSync13(join20(appRoot, "seeds", file), "utf8"))
|
|
3629
3915
|
);
|
|
3630
3916
|
if (seeds.length) log2(pc15.yellow(`
|
|
3631
3917
|
These seeds still use ${table}: ${seeds.map((s) => `seeds/${s}`).join(", ")}.`));
|
|
@@ -3695,9 +3981,9 @@ ${result.output.trim()}`);
|
|
|
3695
3981
|
}
|
|
3696
3982
|
|
|
3697
3983
|
// src/commands/seed.ts
|
|
3698
|
-
import { existsSync as
|
|
3699
|
-
import { join as
|
|
3700
|
-
import { createFake, kebabCase as kebabCase4, storedFields as
|
|
3984
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readdirSync as readdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
3985
|
+
import { join as join22 } from "path";
|
|
3986
|
+
import { camelCase as camelCase6, createFake, kebabCase as kebabCase4, storedFields as storedFields3 } from "@flaredev/core";
|
|
3701
3987
|
import { createJiti as createJiti4 } from "jiti";
|
|
3702
3988
|
import pc17 from "picocolors";
|
|
3703
3989
|
|
|
@@ -3794,16 +4080,16 @@ function createInsertMany(sink, drizzle, onProgress) {
|
|
|
3794
4080
|
|
|
3795
4081
|
// src/seed/local-d1.ts
|
|
3796
4082
|
import { createRequire } from "module";
|
|
3797
|
-
import { join as
|
|
4083
|
+
import { join as join21 } from "path";
|
|
3798
4084
|
import { pathToFileURL } from "url";
|
|
3799
4085
|
var LOCAL_STATE = ".wrangler/state/v3";
|
|
3800
4086
|
async function openLocalD1(appRoot, binding) {
|
|
3801
4087
|
process.env.WRANGLER_LOG ??= "error";
|
|
3802
|
-
const appRequire = createRequire(
|
|
4088
|
+
const appRequire = createRequire(join21(appRoot, "package.json"));
|
|
3803
4089
|
const wrangler = await import(pathToFileURL(appRequire.resolve("wrangler")).href);
|
|
3804
4090
|
const { env, dispose } = await wrangler.getPlatformProxy({
|
|
3805
|
-
configPath:
|
|
3806
|
-
persist: { path:
|
|
4091
|
+
configPath: join21(appRoot, "wrangler.jsonc"),
|
|
4092
|
+
persist: { path: join21(appRoot, LOCAL_STATE) }
|
|
3807
4093
|
});
|
|
3808
4094
|
const bindings = env;
|
|
3809
4095
|
const name = binding ?? "DB";
|
|
@@ -3818,8 +4104,8 @@ async function openLocalD1(appRoot, binding) {
|
|
|
3818
4104
|
// src/commands/seed.ts
|
|
3819
4105
|
var SEEDS_DIR = "seeds";
|
|
3820
4106
|
function seedFiles(appRoot, names) {
|
|
3821
|
-
const dir =
|
|
3822
|
-
const all =
|
|
4107
|
+
const dir = join22(appRoot, SEEDS_DIR);
|
|
4108
|
+
const all = existsSync22(dir) ? readdirSync8(dir).filter((file) => file.endsWith(".seed.ts")).sort() : [];
|
|
3823
4109
|
if (!names?.length) return all;
|
|
3824
4110
|
return names.map((name) => {
|
|
3825
4111
|
const file = `${name.replace(/\.seed\.ts$/, "")}.seed.ts`;
|
|
@@ -3837,16 +4123,20 @@ async function runSeeds(options2 = {}) {
|
|
|
3837
4123
|
log2(`No seeds found. Create one with ${pc17.bold("flare seed:make <name>")}.`);
|
|
3838
4124
|
return;
|
|
3839
4125
|
}
|
|
3840
|
-
|
|
4126
|
+
if (readStack(appRoot) === "next") {
|
|
4127
|
+
await runPrismaSeeds(appRoot, files, log2);
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const jiti = createJiti4(join22(appRoot, "package.json"), { alias: { "@": appRoot }, moduleCache: false, fsCache: false });
|
|
3841
4131
|
const { drizzle } = await jiti.import("drizzle-orm/d1");
|
|
3842
4132
|
const drizzleHelpers = await jiti.import("drizzle-orm");
|
|
3843
|
-
const schema = await jiti.import(
|
|
4133
|
+
const schema = await jiti.import(join22(appRoot, "db/schema.ts"));
|
|
3844
4134
|
const local = await openLocalD1(appRoot);
|
|
3845
4135
|
try {
|
|
3846
4136
|
const db = drizzle(local.db, { schema });
|
|
3847
4137
|
const insertMany = createInsertMany(local.db, drizzleHelpers);
|
|
3848
4138
|
for (const file of files) {
|
|
3849
|
-
const mod = await jiti.import(
|
|
4139
|
+
const mod = await jiti.import(join22(appRoot, SEEDS_DIR, file));
|
|
3850
4140
|
if (typeof mod.default !== "function") throw new Error(`${SEEDS_DIR}/${file} must \`export default defineSeed(async ({ db }) => { ... })\`.`);
|
|
3851
4141
|
log2(`${pc17.cyan("seed")} ${file}`);
|
|
3852
4142
|
await mod.default({ db, env: local.env, insertMany, fake: createFake(), log: (message) => log2(` ${message}`) });
|
|
@@ -3896,9 +4186,17 @@ function sampleValue(key, def) {
|
|
|
3896
4186
|
return void 0;
|
|
3897
4187
|
}
|
|
3898
4188
|
}
|
|
3899
|
-
function renderSeed(resource) {
|
|
4189
|
+
function renderSeed(resource, stack = "cloudflare") {
|
|
3900
4190
|
if (!resource) {
|
|
3901
|
-
return `import { defineSeed } from "@flaredev/core";
|
|
4191
|
+
return stack === "next" ? `import { defineSeed } from "@flaredev/core";
|
|
4192
|
+
|
|
4193
|
+
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
4194
|
+
// \`db\` is this app's Prisma client, e.g.:
|
|
4195
|
+
// await db.contact.create({ data: { name: "Ada", email: "ada@example.com" } });
|
|
4196
|
+
// Or many at once: await insertMany("contact", [{ name: fake.fullName() }]);
|
|
4197
|
+
log("nothing to seed yet");
|
|
4198
|
+
});
|
|
4199
|
+
` : `import { defineSeed } from "@flaredev/core";
|
|
3902
4200
|
|
|
3903
4201
|
export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
3904
4202
|
// Insert rows with Drizzle, e.g.:
|
|
@@ -3911,11 +4209,30 @@ export default defineSeed(async ({ db, insertMany, fake, log }) => {
|
|
|
3911
4209
|
const table = tableExport(resource);
|
|
3912
4210
|
const lines = [];
|
|
3913
4211
|
const todos = [];
|
|
3914
|
-
for (const [key, def] of
|
|
4212
|
+
for (const [key, def] of storedFields3(resource)) {
|
|
3915
4213
|
const value = sampleValue(key, def);
|
|
3916
4214
|
if (value !== void 0) lines.push(` ${key}: ${value},`);
|
|
3917
4215
|
else if (def.required) todos.push(` // ${key}: TODO (${def.kind === "belongsTo" ? `id of an existing ${def.target}` : "R2 object key"}),`);
|
|
3918
4216
|
}
|
|
4217
|
+
if (stack === "next") {
|
|
4218
|
+
const body = [...lines, ...todos].map((line) => ` ${line}`).join("\n");
|
|
4219
|
+
return `import { defineSeed } from "@flaredev/core";
|
|
4220
|
+
|
|
4221
|
+
/** How many to create. createMany sends them in one statement. */
|
|
4222
|
+
const COUNT = 50;
|
|
4223
|
+
|
|
4224
|
+
export default defineSeed(async ({ insertMany, fake, log }) => {
|
|
4225
|
+
await insertMany(
|
|
4226
|
+
${JSON.stringify(camelCase6(resource.name))},
|
|
4227
|
+
Array.from({ length: COUNT }, () => ({
|
|
4228
|
+
${body}
|
|
4229
|
+
updatedAt: new Date(),
|
|
4230
|
+
})),
|
|
4231
|
+
);
|
|
4232
|
+
log(\`inserted \${COUNT.toLocaleString()} ${resource.pluralLabel.toLowerCase()}\`);
|
|
4233
|
+
});
|
|
4234
|
+
`;
|
|
4235
|
+
}
|
|
3919
4236
|
return `import { defineSeed } from "@flaredev/core";
|
|
3920
4237
|
import { ${table} } from "@/db/schema";
|
|
3921
4238
|
|
|
@@ -3936,8 +4253,8 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3936
4253
|
const name = kebabCase4(rawName);
|
|
3937
4254
|
if (!name) throw new Error(`Invalid seed name "${rawName}".`);
|
|
3938
4255
|
const relative2 = `${SEEDS_DIR}/${name}.seed.ts`;
|
|
3939
|
-
const path =
|
|
3940
|
-
if (
|
|
4256
|
+
const path = join22(appRoot, relative2);
|
|
4257
|
+
if (existsSync22(path)) throw new Error(`${relative2} already exists.`);
|
|
3941
4258
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
3942
4259
|
let resource;
|
|
3943
4260
|
if (options2.resource) {
|
|
@@ -3946,22 +4263,171 @@ async function makeSeed(rawName, options2 = {}) {
|
|
|
3946
4263
|
} else {
|
|
3947
4264
|
resource = resources.find((r) => [kebabCase4(r.name), r.slug].includes(name));
|
|
3948
4265
|
}
|
|
3949
|
-
mkdirSync11(
|
|
3950
|
-
|
|
4266
|
+
mkdirSync11(join22(appRoot, SEEDS_DIR), { recursive: true });
|
|
4267
|
+
writeFileSync15(path, renderSeed(resource, readStack(appRoot)));
|
|
3951
4268
|
log2(`${pc17.green("create".padEnd(9))} ${relative2}`);
|
|
3952
4269
|
log2(`
|
|
3953
4270
|
Run it with ${pc17.bold(`flare seed ${name}`)}.`);
|
|
3954
4271
|
return relative2;
|
|
3955
4272
|
}
|
|
4273
|
+
async function runPrismaSeeds(appRoot, files, log2) {
|
|
4274
|
+
const runner = join22(appRoot, ".flare-seed-runner.mts");
|
|
4275
|
+
const script = [
|
|
4276
|
+
`import "dotenv/config";`,
|
|
4277
|
+
`import { createFake } from "@flaredev/core";`,
|
|
4278
|
+
`import { prisma } from "./lib/db";`,
|
|
4279
|
+
...files.map((file, index) => `import seed${index} from "./${SEEDS_DIR}/${file.replace(/\.ts$/, "")}";`),
|
|
4280
|
+
"",
|
|
4281
|
+
`const files = ${JSON.stringify(files)};`,
|
|
4282
|
+
`const seeds = [${files.map((_, index) => `seed${index}`).join(", ")}];`,
|
|
4283
|
+
`const client = prisma as unknown as Record<string, { createMany(args: { data: unknown[] }): Promise<unknown> }>;`,
|
|
4284
|
+
"",
|
|
4285
|
+
`for (const [index, seed] of seeds.entries()) {`,
|
|
4286
|
+
` console.log("seed " + files[index]);`,
|
|
4287
|
+
` await seed({`,
|
|
4288
|
+
` db: prisma,`,
|
|
4289
|
+
` env: process.env,`,
|
|
4290
|
+
` insertMany: async (model: string, rows: Record<string, unknown>[]) => {`,
|
|
4291
|
+
` const delegate = client[model];`,
|
|
4292
|
+
` if (!delegate) throw new Error('No Prisma model "' + model + '". Use the camelCase name, e.g. "orderItem".');`,
|
|
4293
|
+
` await delegate.createMany({ data: rows });`,
|
|
4294
|
+
` },`,
|
|
4295
|
+
` fake: createFake(),`,
|
|
4296
|
+
` log: (message: string) => console.log(" " + message),`,
|
|
4297
|
+
` });`,
|
|
4298
|
+
`}`,
|
|
4299
|
+
`await (prisma as unknown as { $disconnect(): Promise<void> }).$disconnect();`,
|
|
4300
|
+
""
|
|
4301
|
+
].join("\n");
|
|
4302
|
+
writeFileSync15(runner, script);
|
|
4303
|
+
try {
|
|
4304
|
+
const tsx = resolveBin(appRoot, "tsx", "tsx");
|
|
4305
|
+
const code = await runNode([tsx, runner], appRoot);
|
|
4306
|
+
if (code !== 0) throw new Error("A seed failed; nothing further was run.");
|
|
4307
|
+
log2(pc17.green(`
|
|
4308
|
+
Ran ${files.length} seed(s).`));
|
|
4309
|
+
} finally {
|
|
4310
|
+
rmSync4(runner, { force: true });
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
3956
4313
|
|
|
3957
|
-
// src/commands/
|
|
3958
|
-
import { mkdtempSync as mkdtempSync2, rmSync as
|
|
4314
|
+
// src/commands/db-push.ts
|
|
4315
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync16 } from "fs";
|
|
3959
4316
|
import { tmpdir as tmpdir2 } from "os";
|
|
3960
|
-
import { join as
|
|
4317
|
+
import { join as join23 } from "path";
|
|
3961
4318
|
import pc18 from "picocolors";
|
|
3962
4319
|
|
|
4320
|
+
// src/terminal.ts
|
|
4321
|
+
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
4322
|
+
function formatDuration(ms) {
|
|
4323
|
+
const seconds = Math.round(ms / 1e3);
|
|
4324
|
+
if (seconds < 60) return `${seconds}s`;
|
|
4325
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
4326
|
+
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
4327
|
+
}
|
|
4328
|
+
var formatCount = (value) => value.toLocaleString();
|
|
4329
|
+
function formatRate(count, ms, unit = "rows") {
|
|
4330
|
+
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
4331
|
+
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
4332
|
+
}
|
|
4333
|
+
function progressLine(everyMs = 120) {
|
|
4334
|
+
if (!isTTY()) return { update: () => {
|
|
4335
|
+
}, done: () => {
|
|
4336
|
+
} };
|
|
4337
|
+
let last = 0;
|
|
4338
|
+
let width = 0;
|
|
4339
|
+
const write = (message) => {
|
|
4340
|
+
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
4341
|
+
width = Math.max(width, message.length);
|
|
4342
|
+
};
|
|
4343
|
+
return {
|
|
4344
|
+
update(message) {
|
|
4345
|
+
const now = Date.now();
|
|
4346
|
+
if (now - last < everyMs) return;
|
|
4347
|
+
last = now;
|
|
4348
|
+
write(message);
|
|
4349
|
+
},
|
|
4350
|
+
done() {
|
|
4351
|
+
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
4352
|
+
width = 0;
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
// src/commands/db-push.ts
|
|
4358
|
+
var PAGE = 2e3;
|
|
4359
|
+
function pickDatabase(databases, wanted) {
|
|
4360
|
+
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4361
|
+
if (wanted) {
|
|
4362
|
+
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
4363
|
+
if (!match) throw new Error(`No D1 database "${wanted}". Available: ${databases.map((db) => db.binding).join(", ")}.`);
|
|
4364
|
+
return match;
|
|
4365
|
+
}
|
|
4366
|
+
if (databases.length > 1) throw new Error(`Several D1 databases; pick one with --database (${databases.map((db) => db.binding).join(", ")}).`);
|
|
4367
|
+
return databases[0];
|
|
4368
|
+
}
|
|
4369
|
+
async function dbPush(tables, options2 = {}) {
|
|
4370
|
+
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4371
|
+
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4372
|
+
if (tables.length === 0) throw new Error("Name at least one table, e.g. flare db:push products categories --yes");
|
|
4373
|
+
const bad = tables.find((table) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(table));
|
|
4374
|
+
if (bad) throw new Error(`"${bad}" isn't a table name.`);
|
|
4375
|
+
if (!options2.yes) {
|
|
4376
|
+
throw new Error(`This writes ${tables.join(", ")} to the deployed database. Re-run with --yes to confirm.`);
|
|
4377
|
+
}
|
|
4378
|
+
const db = pickDatabase(readD1Databases(appRoot), options2.database);
|
|
4379
|
+
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4380
|
+
const started = Date.now();
|
|
4381
|
+
const local = await openLocalD1(appRoot, options2.database);
|
|
4382
|
+
const dir = mkdtempSync2(join23(tmpdir2(), "flare-push-"));
|
|
4383
|
+
try {
|
|
4384
|
+
const statements = [];
|
|
4385
|
+
const counts = [];
|
|
4386
|
+
for (const table of tables) {
|
|
4387
|
+
if (options2.truncate) statements.push(`DELETE FROM "${table}";`);
|
|
4388
|
+
let offset = 0;
|
|
4389
|
+
let rows = 0;
|
|
4390
|
+
for (; ; ) {
|
|
4391
|
+
const page = await local.db.prepare(`SELECT * FROM "${table}" LIMIT ${PAGE} OFFSET ${offset}`).all();
|
|
4392
|
+
const results = page.results ?? [];
|
|
4393
|
+
if (results.length === 0) break;
|
|
4394
|
+
const columns = Object.keys(results[0]);
|
|
4395
|
+
for (const statement of insertStatements(table, columns, results)) statements.push(statement.sql);
|
|
4396
|
+
rows += results.length;
|
|
4397
|
+
if (results.length < PAGE) break;
|
|
4398
|
+
offset += PAGE;
|
|
4399
|
+
}
|
|
4400
|
+
counts.push({ table, rows });
|
|
4401
|
+
log2(`${pc18.cyan("read")} ${table} ${pc18.dim(`(${formatCount(rows)} rows)`)}`);
|
|
4402
|
+
}
|
|
4403
|
+
const total = counts.reduce((sum, entry) => sum + entry.rows, 0);
|
|
4404
|
+
if (total === 0) {
|
|
4405
|
+
log2("Nothing to push: those tables are empty locally.");
|
|
4406
|
+
return { tables: counts, ms: Date.now() - started };
|
|
4407
|
+
}
|
|
4408
|
+
const file = join23(dir, "push.sql");
|
|
4409
|
+
writeFileSync16(file, statements.join("\n"));
|
|
4410
|
+
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4411
|
+
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4412
|
+
if (result.code !== 0) throw new Error(`Pushing to ${db.database_name} failed:
|
|
4413
|
+
${result.output.trim()}`);
|
|
4414
|
+
const ms = Date.now() - started;
|
|
4415
|
+
log2(`${pc18.green("\u2714")} Pushed ${pc18.bold(formatCount(total))} rows to ${pc18.bold(db.database_name)} ${pc18.dim(`(${formatDuration(ms)})`)}`);
|
|
4416
|
+
return { tables: counts, ms };
|
|
4417
|
+
} finally {
|
|
4418
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
4419
|
+
await local.dispose();
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
// src/commands/seed-resource.ts
|
|
4424
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync6, writeFileSync as writeFileSync17 } from "fs";
|
|
4425
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
4426
|
+
import { join as join24 } from "path";
|
|
4427
|
+
import pc19 from "picocolors";
|
|
4428
|
+
|
|
3963
4429
|
// src/seed/rows.ts
|
|
3964
|
-
import { columnName as
|
|
4430
|
+
import { columnName as columnName3, createFake as createFake2, storedFields as storedFields4 } from "@flaredev/core";
|
|
3965
4431
|
var has = (key, ...words) => words.some((word) => key.includes(word));
|
|
3966
4432
|
function stringValue(key, def, fake, resource, index) {
|
|
3967
4433
|
switch (def.format) {
|
|
@@ -3981,7 +4447,9 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3981
4447
|
return fake.slug();
|
|
3982
4448
|
}
|
|
3983
4449
|
const name = key.toLowerCase();
|
|
3984
|
-
const
|
|
4450
|
+
const resourceName2 = resource.name.toLowerCase();
|
|
4451
|
+
const organisation = has(resourceName2, "company", "vendor", "supplier", "organisation", "organization", "account", "team", "store", "brand");
|
|
4452
|
+
const catalogue = has(resourceName2, "product", "item", "article", "sku", "listing", "merch", "book", "course", "plan", "service");
|
|
3985
4453
|
let value;
|
|
3986
4454
|
if (has(name, "email")) value = fake.email();
|
|
3987
4455
|
else if (has(name, "phone", "mobile", "tel", "whatsapp")) value = fake.phone();
|
|
@@ -3996,7 +4464,7 @@ function stringValue(key, def, fake, resource, index) {
|
|
|
3996
4464
|
else if (has(name, "colour", "color")) value = fake.color();
|
|
3997
4465
|
else if (has(name, "code", "sku", "reference", "number")) value = `${fake.words(1).slice(0, 3).toUpperCase()}-${String(index + 1).padStart(6, "0")}`;
|
|
3998
4466
|
else if (has(name, "title", "position", "role", "job")) value = fake.jobTitle();
|
|
3999
|
-
else if (has(name, "name")) value = organisation ? fake.company() : fake.fullName();
|
|
4467
|
+
else if (has(name, "name")) value = organisation ? fake.company() : catalogue ? fake.product(digitalish(resource)) : fake.fullName();
|
|
4000
4468
|
else if (has(name, "description", "summary", "note", "bio", "about", "message", "comment")) value = fake.sentence();
|
|
4001
4469
|
else value = `${fake.words(2)}`.replace(/^./, (letter) => letter.toUpperCase());
|
|
4002
4470
|
if (def.maxLength && value.length > def.maxLength) value = value.slice(0, def.maxLength);
|
|
@@ -4058,13 +4526,18 @@ function uniquify(value, row) {
|
|
|
4058
4526
|
if (/^[a-z0-9.-]+$/.test(value)) return `${value}-${row}`;
|
|
4059
4527
|
return `${value} ${row}`;
|
|
4060
4528
|
}
|
|
4529
|
+
function digitalish(resource) {
|
|
4530
|
+
return Object.entries(resource.fields).some(
|
|
4531
|
+
([key, def]) => def.kind === "enum" && /kind|type/.test(key) && def.options.some((option) => /digital|download|licence|license/i.test(option))
|
|
4532
|
+
);
|
|
4533
|
+
}
|
|
4061
4534
|
function seedColumns(resource) {
|
|
4062
|
-
return ["id", ...
|
|
4535
|
+
return ["id", ...storedFields4(resource).map(([key]) => columnName3(key)), "created_at", "updated_at"];
|
|
4063
4536
|
}
|
|
4064
4537
|
function* seedRows(resource, count, options2 = {}) {
|
|
4065
4538
|
const fake = createFake2(options2.seed ?? Math.floor(Math.random() * 2 ** 31));
|
|
4066
4539
|
const parentIds = options2.parentIds ?? /* @__PURE__ */ new Map();
|
|
4067
|
-
const fields =
|
|
4540
|
+
const fields = storedFields4(resource);
|
|
4068
4541
|
const now = options2.now ?? Date.now();
|
|
4069
4542
|
const start = options2.startIndex ?? 0;
|
|
4070
4543
|
for (let i = 0; i < count; i++) {
|
|
@@ -4073,7 +4546,7 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4073
4546
|
for (const [key, def] of fields) {
|
|
4074
4547
|
let value = fieldValue(key, def, fake, resource, parentIds, number);
|
|
4075
4548
|
if (typeof value === "string" && "unique" in def && def.unique) value = uniquify(value, number + 1);
|
|
4076
|
-
row[
|
|
4549
|
+
row[columnName3(key)] = value;
|
|
4077
4550
|
}
|
|
4078
4551
|
const created = now - fake.int(0, 365) * 864e5 - fake.int(0, 86399999);
|
|
4079
4552
|
row.created_at = created;
|
|
@@ -4082,43 +4555,6 @@ function* seedRows(resource, count, options2 = {}) {
|
|
|
4082
4555
|
}
|
|
4083
4556
|
}
|
|
4084
4557
|
|
|
4085
|
-
// src/terminal.ts
|
|
4086
|
-
var isTTY = () => Boolean(process.stdout.isTTY);
|
|
4087
|
-
function formatDuration(ms) {
|
|
4088
|
-
const seconds = Math.round(ms / 1e3);
|
|
4089
|
-
if (seconds < 60) return `${seconds}s`;
|
|
4090
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
4091
|
-
return `${Math.floor(seconds / 3600)}h ${String(Math.floor(seconds % 3600 / 60)).padStart(2, "0")}m`;
|
|
4092
|
-
}
|
|
4093
|
-
var formatCount = (value) => value.toLocaleString();
|
|
4094
|
-
function formatRate(count, ms, unit = "rows") {
|
|
4095
|
-
if (ms <= 0) return `${formatCount(count)} ${unit}/s`;
|
|
4096
|
-
return `${formatCount(Math.round(count / ms * 1e3))} ${unit}/s`;
|
|
4097
|
-
}
|
|
4098
|
-
function progressLine(everyMs = 120) {
|
|
4099
|
-
if (!isTTY()) return { update: () => {
|
|
4100
|
-
}, done: () => {
|
|
4101
|
-
} };
|
|
4102
|
-
let last = 0;
|
|
4103
|
-
let width = 0;
|
|
4104
|
-
const write = (message) => {
|
|
4105
|
-
process.stdout.write(`\r${message.padEnd(width)}`);
|
|
4106
|
-
width = Math.max(width, message.length);
|
|
4107
|
-
};
|
|
4108
|
-
return {
|
|
4109
|
-
update(message) {
|
|
4110
|
-
const now = Date.now();
|
|
4111
|
-
if (now - last < everyMs) return;
|
|
4112
|
-
last = now;
|
|
4113
|
-
write(message);
|
|
4114
|
-
},
|
|
4115
|
-
done() {
|
|
4116
|
-
if (width > 0) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
4117
|
-
width = 0;
|
|
4118
|
-
}
|
|
4119
|
-
};
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
4558
|
// src/commands/seed-resource.ts
|
|
4123
4559
|
async function countRows(db, table) {
|
|
4124
4560
|
const row = await db.prepare(`SELECT count(*) AS total FROM "${table}"`).first();
|
|
@@ -4143,7 +4579,7 @@ function pickResource(resources, name) {
|
|
|
4143
4579
|
}
|
|
4144
4580
|
return match;
|
|
4145
4581
|
}
|
|
4146
|
-
function
|
|
4582
|
+
function pickDatabase2(databases, wanted) {
|
|
4147
4583
|
if (databases.length === 0) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4148
4584
|
if (wanted) {
|
|
4149
4585
|
const match = databases.find((db) => db.binding === wanted || db.database_name === wanted);
|
|
@@ -4172,13 +4608,13 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4172
4608
|
...options2,
|
|
4173
4609
|
onProgress: (written, total) => {
|
|
4174
4610
|
const elapsed = Date.now() - started;
|
|
4175
|
-
progress.update(` ${
|
|
4611
|
+
progress.update(` ${pc19.cyan(formatCount(written))} of ${formatCount(total)} rows ${pc19.dim(formatRate(written, elapsed))}`);
|
|
4176
4612
|
}
|
|
4177
4613
|
});
|
|
4178
4614
|
progress.done();
|
|
4179
4615
|
const { rows, ms, where } = result;
|
|
4180
4616
|
console.log(
|
|
4181
|
-
`${
|
|
4617
|
+
`${pc19.green("\u2714")} Seeded ${pc19.bold(formatCount(rows))} ${result.resource.pluralLabel.toLowerCase()} into the ${where} database ` + pc19.dim(`(${formatDuration(ms)}, ${formatRate(rows, ms)})`)
|
|
4182
4618
|
);
|
|
4183
4619
|
return result;
|
|
4184
4620
|
} catch (error) {
|
|
@@ -4189,6 +4625,11 @@ async function seedResourceCommand(name, options2 = {}) {
|
|
|
4189
4625
|
async function seedResource(name, options2 = {}) {
|
|
4190
4626
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4191
4627
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4628
|
+
if (readStack(appRoot) === "next") {
|
|
4629
|
+
throw new Error(
|
|
4630
|
+
"flare seed:resource is Cloudflare-only so far. On this stack, write a seed with `flare seed:make <name> --resource " + name + "` \u2014 it comes with rows already filled in \u2014 and run it with `flare seed`."
|
|
4631
|
+
);
|
|
4632
|
+
}
|
|
4192
4633
|
const resources = (await loadResources(appRoot)).map(({ resource: resource2 }) => resource2);
|
|
4193
4634
|
const resource = pickResource(resources, name);
|
|
4194
4635
|
const total = parseCount(options2.count);
|
|
@@ -4202,7 +4643,7 @@ async function seedResource(name, options2 = {}) {
|
|
|
4202
4643
|
let startIndex = 0;
|
|
4203
4644
|
const rows = () => seedRows(resource, total, { seed: options2.seed, parentIds: parents, startIndex });
|
|
4204
4645
|
if (options2.remote) {
|
|
4205
|
-
const db =
|
|
4646
|
+
const db = pickDatabase2(readD1Databases(appRoot), options2.database);
|
|
4206
4647
|
const wrangler = resolveBin(appRoot, "wrangler", "wrangler");
|
|
4207
4648
|
const target = ["--remote", ...options2.env ? ["--env", options2.env] : []];
|
|
4208
4649
|
const query = async (sql2) => {
|
|
@@ -4221,8 +4662,8 @@ ${result.output.trim()}`);
|
|
|
4221
4662
|
const [counted] = await query(`SELECT count(*) AS total FROM "${resource.table}"`);
|
|
4222
4663
|
startIndex = counted?.total ?? 0;
|
|
4223
4664
|
}
|
|
4224
|
-
const dir =
|
|
4225
|
-
const file =
|
|
4665
|
+
const dir = mkdtempSync3(join24(tmpdir3(), "flare-seed-"));
|
|
4666
|
+
const file = join24(dir, `${resource.table}.sql`);
|
|
4226
4667
|
try {
|
|
4227
4668
|
const statements = [];
|
|
4228
4669
|
if (options2.truncate) statements.push(`DELETE FROM "${resource.table}";`);
|
|
@@ -4232,13 +4673,13 @@ ${result.output.trim()}`);
|
|
|
4232
4673
|
written += statement.rows;
|
|
4233
4674
|
options2.onProgress?.(written, total);
|
|
4234
4675
|
}
|
|
4235
|
-
|
|
4236
|
-
log2(`${
|
|
4676
|
+
writeFileSync17(file, statements.join("\n"));
|
|
4677
|
+
log2(`${pc19.cyan("\u25CF")} Uploading ${total.toLocaleString()} rows to ${pc19.bold(db.database_name)}`);
|
|
4237
4678
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--yes", "--file", file], appRoot, { capture: true });
|
|
4238
4679
|
if (result.code !== 0) throw new Error(`Seeding the remote database failed:
|
|
4239
4680
|
${result.output.trim()}`);
|
|
4240
4681
|
} finally {
|
|
4241
|
-
|
|
4682
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
4242
4683
|
}
|
|
4243
4684
|
return { resource, rows: total, ms: Date.now() - started, where };
|
|
4244
4685
|
}
|
|
@@ -4265,17 +4706,17 @@ ${result.output.trim()}`);
|
|
|
4265
4706
|
|
|
4266
4707
|
// src/commands/billing-sync.ts
|
|
4267
4708
|
import { createRequire as createRequire2 } from "module";
|
|
4268
|
-
import { existsSync as
|
|
4269
|
-
import { tmpdir as
|
|
4270
|
-
import { join as
|
|
4709
|
+
import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "fs";
|
|
4710
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
4711
|
+
import { join as join25 } from "path";
|
|
4271
4712
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
4272
4713
|
import { kebabCase as kebabCase5 } from "@flaredev/core";
|
|
4273
|
-
import
|
|
4714
|
+
import pc20 from "picocolors";
|
|
4274
4715
|
function stripeKeys(appRoot) {
|
|
4275
4716
|
const map = {};
|
|
4276
|
-
const path =
|
|
4277
|
-
if (
|
|
4278
|
-
for (const line of
|
|
4717
|
+
const path = join25(appRoot, ".dev.vars");
|
|
4718
|
+
if (existsSync23(path)) {
|
|
4719
|
+
for (const line of readFileSync14(path, "utf8").split(/\r?\n/)) {
|
|
4279
4720
|
const match = /^\s*([A-Z][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
4280
4721
|
if (match) map[match[1]] = match[2].trim();
|
|
4281
4722
|
}
|
|
@@ -4358,10 +4799,10 @@ async function syncPlans(options2 = {}) {
|
|
|
4358
4799
|
if (!key) throw new Error("STRIPE_SECRET_KEY is not set in .dev.vars or the environment.");
|
|
4359
4800
|
const db = readD1Databases(appRoot)[0];
|
|
4360
4801
|
if (!db) throw new Error("No d1_databases in wrangler.jsonc.");
|
|
4361
|
-
const appRequire = createRequire2(
|
|
4802
|
+
const appRequire = createRequire2(join25(appRoot, "package.json"));
|
|
4362
4803
|
const stripeModule = await import(pathToFileURL2(appRequire.resolve("stripe")).href);
|
|
4363
4804
|
const stripe = new stripeModule.default(key, { apiVersion: "2026-08-26.dahlia" });
|
|
4364
|
-
const appName = JSON.parse(
|
|
4805
|
+
const appName = JSON.parse(readFileSync14(join25(appRoot, "package.json"), "utf8")).name ?? "app";
|
|
4365
4806
|
const everything = await stripe.products.list({ active: true, limit: 100 }).autoPagingToArray({ limit: 1e4 });
|
|
4366
4807
|
const products = options2.all ? everything : everything.filter((product) => product.metadata?.flare_app === appName);
|
|
4367
4808
|
if (products.length === 0) {
|
|
@@ -4382,42 +4823,42 @@ async function syncPlans(options2 = {}) {
|
|
|
4382
4823
|
${existingResult.output.trim()}`);
|
|
4383
4824
|
}
|
|
4384
4825
|
const known = new Set(extractJson(existingResult.output)[0]?.results.map((row) => row.stripe_price_id));
|
|
4385
|
-
const dir =
|
|
4826
|
+
const dir = mkdtempSync4(join25(tmpdir4(), "flare-sync-"));
|
|
4386
4827
|
try {
|
|
4387
|
-
const file =
|
|
4388
|
-
|
|
4828
|
+
const file = join25(dir, "sync-plans.sql");
|
|
4829
|
+
writeFileSync18(file, renderSyncSql(rows));
|
|
4389
4830
|
const result = await runWrangler(wrangler, ["d1", "execute", db.binding, ...target, "--file", file], appRoot, { capture: true });
|
|
4390
4831
|
if (result.code !== 0) throw new Error(`Could not write plans:
|
|
4391
4832
|
${result.output.trim()}`);
|
|
4392
4833
|
} finally {
|
|
4393
|
-
|
|
4834
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
4394
4835
|
}
|
|
4395
4836
|
let created = 0;
|
|
4396
4837
|
for (const row of rows) {
|
|
4397
4838
|
const isNew = !known.has(row.stripePriceId);
|
|
4398
4839
|
if (isNew) created++;
|
|
4399
4840
|
const price = `${row.currency.toUpperCase()} ${(row.amount / 100).toFixed(2)}${row.interval ? `/${row.interval}` : " once"}`;
|
|
4400
|
-
log2(`${(isNew ?
|
|
4841
|
+
log2(`${(isNew ? pc20.green("create") : pc20.dim("update")).padEnd(18)} ${row.name} (${row.slug}) ${pc20.dim(price)}`);
|
|
4401
4842
|
}
|
|
4402
|
-
for (const name of skipped) log2(
|
|
4843
|
+
for (const name of skipped) log2(pc20.yellow(`skipped ${name}: only monthly, yearly and one-time prices become plans`));
|
|
4403
4844
|
const configuration = await syncPortalConfiguration(stripe, rows, appName);
|
|
4404
|
-
if (configuration) log2(`${
|
|
4405
|
-
log2(
|
|
4845
|
+
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})`);
|
|
4846
|
+
log2(pc20.green(`
|
|
4406
4847
|
${rows.length} plan(s) synced to the ${options2.remote ? "remote" : "local"} database; plans no longer active in Stripe were deactivated.`));
|
|
4407
4848
|
return { created, updated: rows.length - created, skipped };
|
|
4408
4849
|
}
|
|
4409
4850
|
|
|
4410
4851
|
// src/commands/sync.ts
|
|
4411
|
-
import { existsSync as
|
|
4412
|
-
import { join as
|
|
4413
|
-
import
|
|
4852
|
+
import { existsSync as existsSync24 } from "fs";
|
|
4853
|
+
import { join as join26 } from "path";
|
|
4854
|
+
import pc21 from "picocolors";
|
|
4414
4855
|
async function syncTypes(options2 = {}) {
|
|
4415
4856
|
const log2 = options2.log ?? ((message) => console.log(message));
|
|
4416
4857
|
const appRoot = findAppRoot(options2.cwd ?? process.cwd());
|
|
4417
4858
|
const all = await loadResources(appRoot);
|
|
4418
4859
|
const policies = await loadPolicies(appRoot);
|
|
4419
|
-
const tracksPolicies = policies.length > 0 ||
|
|
4420
|
-
const plan = planFiles(all, tracksPolicies ? policies : void 0);
|
|
4860
|
+
const tracksPolicies = policies.length > 0 || existsSync24(join26(appRoot, "policies/index.ts"));
|
|
4861
|
+
const plan = planFiles(all, tracksPolicies ? policies : void 0, readStack(appRoot));
|
|
4421
4862
|
if (!options2.check) ensureSupportFiles(appRoot, log2);
|
|
4422
4863
|
const results = applyPlan(appRoot, plan, { force: options2.force, dryRun: options2.check });
|
|
4423
4864
|
logResults(results, log2);
|
|
@@ -4428,42 +4869,42 @@ async function syncTypes(options2 = {}) {
|
|
|
4428
4869
|
if (drift.length) {
|
|
4429
4870
|
log2(
|
|
4430
4871
|
`
|
|
4431
|
-
${
|
|
4872
|
+
${pc21.red("Drift:")} ${drift.length} generated block(s) were edited by hand and were ${options2.check ? "not checked further" : "left unchanged"}.
|
|
4432
4873
|
Move custom code outside the // generated:start \u2026 // generated:end block, or re-run with --force to overwrite.`
|
|
4433
4874
|
);
|
|
4434
4875
|
}
|
|
4435
4876
|
if (unmarked.length) {
|
|
4436
4877
|
log2(`
|
|
4437
|
-
${
|
|
4878
|
+
${pc21.red("Conflict:")} ${unmarked.map((r) => r.path).join(", ")} exist without a generated block (hand-written). Rename or remove them.`);
|
|
4438
4879
|
}
|
|
4439
4880
|
if (orphans.length) {
|
|
4440
4881
|
const resources = [...new Set(orphans.map((o) => o.resource))];
|
|
4441
4882
|
log2(`
|
|
4442
|
-
${
|
|
4883
|
+
${pc21.yellow("Orphaned:")} generated files the descriptors no longer produce (${resources.join(", ")}):`);
|
|
4443
4884
|
for (const orphan of orphans) log2(` ${orphan.path}`);
|
|
4444
4885
|
log2(
|
|
4445
|
-
`If the resource was deleted, remove them with ${
|
|
4886
|
+
`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.`
|
|
4446
4887
|
);
|
|
4447
4888
|
}
|
|
4448
4889
|
const tablesChanged = changed.some((r) => r.path.startsWith("db/schema/") && (r.status === "outdated" || r.status === "missing"));
|
|
4449
4890
|
if (tablesChanged && !options2.check) {
|
|
4450
4891
|
log2(`
|
|
4451
|
-
${
|
|
4892
|
+
${pc21.cyan("Tables changed.")} Create a migration with ${pc21.bold("flare gen migration <name> --from-schema")}, then ${pc21.bold("flare migrate")}.`);
|
|
4452
4893
|
}
|
|
4453
4894
|
const problems = drift.length + unmarked.length + orphans.length;
|
|
4454
4895
|
if (options2.check) {
|
|
4455
4896
|
const stale = changed.length;
|
|
4456
|
-
if (stale === 0 && problems === 0) log2(
|
|
4897
|
+
if (stale === 0 && problems === 0) log2(pc21.green("Generated files are in sync with the resource descriptors."));
|
|
4457
4898
|
else log2(`
|
|
4458
|
-
${
|
|
4899
|
+
${pc21.red("Out of sync:")} ${stale} file(s) would change. Run ${pc21.bold("flare sync-types")}.`);
|
|
4459
4900
|
return stale + problems > 0 ? 1 : 0;
|
|
4460
4901
|
}
|
|
4461
|
-
if (changed.length === 0 && problems === 0) log2(
|
|
4902
|
+
if (changed.length === 0 && problems === 0) log2(pc21.green("Everything is in sync."));
|
|
4462
4903
|
return problems > 0 ? 1 : 0;
|
|
4463
4904
|
}
|
|
4464
4905
|
|
|
4465
4906
|
// src/commands/user-role.ts
|
|
4466
|
-
import
|
|
4907
|
+
import pc22 from "picocolors";
|
|
4467
4908
|
var ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
4468
4909
|
var EMAIL = /^[^\s@'"\\]+@[^\s@'"\\]+\.[^\s@'"\\]+$/;
|
|
4469
4910
|
function userRoleSql(email, role) {
|
|
@@ -4477,8 +4918,8 @@ async function setUserRole(email, role, options2 = {}) {
|
|
|
4477
4918
|
const sql2 = userRoleSql(email, role);
|
|
4478
4919
|
const roles = await listRoles(options2);
|
|
4479
4920
|
if (roles && !roles.includes(role)) {
|
|
4480
|
-
log2(
|
|
4481
|
-
log2(
|
|
4921
|
+
log2(pc22.red(`No role "${role}" is registered. Known roles: ${roles.join(", ") || "none"}.`));
|
|
4922
|
+
log2(pc22.dim(`Register it with: flare role:add ${role}${options2.remote ? " --remote" : ""}`));
|
|
4482
4923
|
return 1;
|
|
4483
4924
|
}
|
|
4484
4925
|
const db = readD1Databases(appRoot)[0];
|
|
@@ -4492,31 +4933,31 @@ ${result.output.trim()}`);
|
|
|
4492
4933
|
const [first] = extractJson(result.output);
|
|
4493
4934
|
const updated = first?.results ?? [];
|
|
4494
4935
|
if (updated.length === 0) {
|
|
4495
|
-
log2(
|
|
4936
|
+
log2(pc22.red(`No user with email ${email} in the ${options2.remote ? "remote" : "local"} database. Sign up first, then run this again.`));
|
|
4496
4937
|
return 1;
|
|
4497
4938
|
}
|
|
4498
|
-
log2(
|
|
4939
|
+
log2(pc22.green(`${updated[0].email} now has role "${updated[0].role}" (${options2.remote ? "remote" : "local"}).`));
|
|
4499
4940
|
return 0;
|
|
4500
4941
|
}
|
|
4501
4942
|
|
|
4502
4943
|
// src/utils/install.ts
|
|
4503
4944
|
import { spawn as spawn5 } from "child_process";
|
|
4504
|
-
import { existsSync as
|
|
4505
|
-
import { join as
|
|
4945
|
+
import { existsSync as existsSync25, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "fs";
|
|
4946
|
+
import { join as join27 } from "path";
|
|
4506
4947
|
function expectedPackages(dir) {
|
|
4507
|
-
const npmLock =
|
|
4508
|
-
if (
|
|
4948
|
+
const npmLock = join27(dir, "package-lock.json");
|
|
4949
|
+
if (existsSync25(npmLock)) {
|
|
4509
4950
|
try {
|
|
4510
|
-
const lock = JSON.parse(
|
|
4951
|
+
const lock = JSON.parse(readFileSync15(npmLock, "utf8"));
|
|
4511
4952
|
return Math.max(0, Object.keys(lock.packages ?? {}).length - 1);
|
|
4512
4953
|
} catch {
|
|
4513
4954
|
return 0;
|
|
4514
4955
|
}
|
|
4515
4956
|
}
|
|
4516
|
-
const pnpmLock =
|
|
4517
|
-
if (
|
|
4957
|
+
const pnpmLock = join27(dir, "pnpm-lock.yaml");
|
|
4958
|
+
if (existsSync25(pnpmLock)) {
|
|
4518
4959
|
try {
|
|
4519
|
-
const text =
|
|
4960
|
+
const text = readFileSync15(pnpmLock, "utf8");
|
|
4520
4961
|
const packages = text.slice(text.indexOf("\npackages:"));
|
|
4521
4962
|
return (packages.match(/\n {2}[^\s:]+:/g) ?? []).length;
|
|
4522
4963
|
} catch {
|
|
@@ -4526,15 +4967,15 @@ function expectedPackages(dir) {
|
|
|
4526
4967
|
return 0;
|
|
4527
4968
|
}
|
|
4528
4969
|
function installedPackages(dir) {
|
|
4529
|
-
const root =
|
|
4530
|
-
if (!
|
|
4970
|
+
const root = join27(dir, "node_modules");
|
|
4971
|
+
if (!existsSync25(root)) return 0;
|
|
4531
4972
|
let total = 0;
|
|
4532
4973
|
try {
|
|
4533
4974
|
for (const entry of readdirSync9(root, { withFileTypes: true })) {
|
|
4534
4975
|
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
4535
4976
|
if (entry.name.startsWith("@")) {
|
|
4536
4977
|
try {
|
|
4537
|
-
total += readdirSync9(
|
|
4978
|
+
total += readdirSync9(join27(root, entry.name)).length;
|
|
4538
4979
|
} catch {
|
|
4539
4980
|
}
|
|
4540
4981
|
} else if (entry.name !== ".pnpm" && entry.name !== ".vite") {
|
|
@@ -4578,31 +5019,33 @@ function install(packageManager, dir, onProgress) {
|
|
|
4578
5019
|
// src/cli.ts
|
|
4579
5020
|
function createCli() {
|
|
4580
5021
|
const cli = cac("flare");
|
|
4581
|
-
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(
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
5022
|
+
cli.command("create <dir>", "Scaffold a new Flare app").option("--pm <manager>", "Package manager: pnpm, npm, yarn, or bun (default: detected)").option("--stack <name>", "Where it runs: cloudflare (default) or next").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 --stack next").example("flare create shop --theme mono --auth passkeys,2fa-app --auth-providers google,github --yes").action(
|
|
5023
|
+
async (dir, options2) => {
|
|
5024
|
+
const wanted = !options2.skipInstall;
|
|
5025
|
+
let answers = { theme: options2.theme, auth: options2.auth, authProviders: options2.authProviders };
|
|
5026
|
+
if (canPrompt(options2.yes)) answers = await askCreateQuestions(answers, toAppName(dir));
|
|
5027
|
+
const result = createApp(dir, { pm: options2.pm, stack: options2.stack, install: false, ...answers, log: (message) => prompts3.log.message(message) });
|
|
5028
|
+
let installed = false;
|
|
5029
|
+
if (wanted) {
|
|
5030
|
+
const spin = prompts3.spinner({ indicator: "timer" });
|
|
5031
|
+
spin.start(`Installing dependencies with ${result.packageManager}`);
|
|
5032
|
+
const outcome = await install(result.packageManager, result.dir, ({ installed: done, expected, percent }) => {
|
|
5033
|
+
spin.message(
|
|
5034
|
+
percent === void 0 ? `Installing dependencies with ${result.packageManager} \xB7 ${formatCount(done)} packages` : `Installing dependencies with ${result.packageManager} \xB7 ${percent}% (${formatCount(done)} of ${formatCount(expected)})`
|
|
5035
|
+
);
|
|
5036
|
+
});
|
|
5037
|
+
installed = outcome.code === 0;
|
|
5038
|
+
if (installed) {
|
|
5039
|
+
spin.stop(`Installed ${formatCount(expectedPackages(result.dir) || 0)} packages with ${result.packageManager} ${pc23.dim(`(${formatDuration(outcome.ms)})`)}`);
|
|
5040
|
+
} else {
|
|
5041
|
+
spin.error(`${result.packageManager} install failed`);
|
|
5042
|
+
process.stderr.write(outcome.output);
|
|
5043
|
+
process.exitCode = 1;
|
|
5044
|
+
}
|
|
4602
5045
|
}
|
|
5046
|
+
printNextSteps(result, installed);
|
|
4603
5047
|
}
|
|
4604
|
-
|
|
4605
|
-
});
|
|
5048
|
+
);
|
|
4606
5049
|
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(
|
|
4607
5050
|
async (generator, name, second, options2) => {
|
|
4608
5051
|
if (generator === "billing")
|
|
@@ -4643,6 +5086,9 @@ function createCli() {
|
|
|
4643
5086
|
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) => {
|
|
4644
5087
|
await seedResourceCommand(resource, { ...options2, count: options2.count ?? count });
|
|
4645
5088
|
});
|
|
5089
|
+
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) => {
|
|
5090
|
+
await dbPush(tables, options2);
|
|
5091
|
+
});
|
|
4646
5092
|
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) => {
|
|
4647
5093
|
await makeSeed(name, options2);
|
|
4648
5094
|
});
|
|
@@ -4688,6 +5134,6 @@ try {
|
|
|
4688
5134
|
}
|
|
4689
5135
|
}
|
|
4690
5136
|
} catch (error) {
|
|
4691
|
-
console.error(
|
|
5137
|
+
console.error(pc24.red(`\u2716 ${error instanceof Error ? error.message : String(error)}`));
|
|
4692
5138
|
process.exitCode = 1;
|
|
4693
5139
|
}
|