@flaredev/cli 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.js +698 -341
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  4. package/templates/app/app/dashboard/page.tsx +4 -7
  5. package/templates/app/app/page.tsx +10 -0
  6. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  7. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  8. package/templates/app/components/dashboard/record-actions.tsx +7 -7
  9. package/templates/app/components/dashboard/record-detail.tsx +1 -1
  10. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  11. package/templates/app/components/dashboard/resource-form.tsx +129 -33
  12. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  13. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  14. package/templates/app/components/dashboard/resource-table.tsx +4 -4
  15. package/templates/app/components/dashboard/row-actions.tsx +3 -3
  16. package/templates/app/lib/costs.ts +138 -0
  17. package/templates/app/lib/dashboard-nav.ts +5 -1
  18. package/templates/app/lib/dashboard.ts +66 -61
  19. package/templates/app/lib/number.ts +33 -0
  20. package/templates/app/lib/site.ts +56 -56
  21. package/templates/app/lib/suggest.ts +35 -0
  22. package/templates/app/lib/usage.ts +32 -0
  23. package/templates/next/.env.example +21 -0
  24. package/templates/next/README.md +16 -0
  25. package/templates/next/app/api/health/route.ts +7 -0
  26. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  27. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  28. package/templates/next/app/dashboard/actions.ts +277 -0
  29. package/templates/next/app/dashboard/layout.tsx +64 -0
  30. package/templates/next/app/dashboard/page.tsx +127 -0
  31. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  32. package/templates/next/lib/analytics.ts +71 -0
  33. package/templates/next/lib/audit.ts +80 -0
  34. package/templates/next/lib/auth-emails.ts +92 -0
  35. package/templates/next/lib/auth.ts +156 -0
  36. package/templates/next/lib/dashboard.ts +161 -0
  37. package/templates/next/lib/db.ts +24 -0
  38. package/templates/next/lib/mail.ts +35 -0
  39. package/templates/next/lib/realtime.ts +23 -0
  40. package/templates/next/lib/redis.ts +17 -0
  41. package/templates/next/lib/storage.ts +57 -0
  42. package/templates/next/lib/theme.ts +83 -0
  43. package/templates/next/lib/views.ts +23 -0
  44. package/templates/next/next.config.ts +13 -0
  45. package/templates/next/prisma/schema/base.prisma +162 -0
  46. package/templates/next/prisma.config.ts +15 -0
  47. package/templates/next/tsconfig.json +24 -0
  48. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
@@ -0,0 +1,83 @@
1
+ import { site } from "./site";
2
+
3
+ /**
4
+ * The six looks every Flare app ships with. The active one sets `data-theme` on <html>;
5
+ * app/globals.css maps it to colours, radius and type, and the auth screens pick the
6
+ * layout below.
7
+ */
8
+ export const THEMES = {
9
+ default: {
10
+ label: "Default",
11
+ description: "Calm and centred, indigo accents.",
12
+ auth: "centered",
13
+ social: "after",
14
+ socialStyle: "full",
15
+ signIn: (name: string) => ({ title: `Sign in to ${name}` }),
16
+ signUp: (name: string) => ({ title: `Create your ${name} account` }),
17
+ },
18
+ coral: {
19
+ label: "Coral",
20
+ description: "Warm and rounded, a card floating over the page.",
21
+ auth: "modal",
22
+ social: "after",
23
+ socialStyle: "icon",
24
+ signIn: () => ({ title: "Log in or sign up" }),
25
+ signUp: () => ({ title: "Create your account" }),
26
+ },
27
+ amber: {
28
+ label: "Amber",
29
+ description: "Plain and direct, a boxed form with pill buttons.",
30
+ auth: "boxed",
31
+ social: "after",
32
+ socialStyle: "full",
33
+ signIn: () => ({ title: "Sign in" }),
34
+ signUp: () => ({ title: "Create account" }),
35
+ },
36
+ sky: {
37
+ label: "Sky",
38
+ description: "Crisp blue, a bold heading with social sign-in first.",
39
+ auth: "banner",
40
+ social: "before",
41
+ socialStyle: "full",
42
+ signIn: (name: string) => ({ title: `Log in to ${name}` }),
43
+ signUp: (name: string) => ({ title: `Get started with ${name}` }),
44
+ },
45
+ mono: {
46
+ label: "Mono",
47
+ description: "Black and white on a fine grid, a showcase beside the form.",
48
+ auth: "showcase",
49
+ social: "after",
50
+ socialStyle: "full",
51
+ signIn: (name: string) => ({ title: `Log in to your ${name} account` }),
52
+ signUp: (name: string) => ({ title: `Create your ${name} account` }),
53
+ },
54
+ emerald: {
55
+ label: "Emerald",
56
+ description: "Fresh green, the form beside a customer quote.",
57
+ auth: "quote",
58
+ social: "before",
59
+ socialStyle: "full",
60
+ signIn: () => ({ title: "Welcome back", subtitle: "Sign in to your account" }),
61
+ signUp: () => ({ title: "Get started", subtitle: "Create a new account" }),
62
+ },
63
+ } as const;
64
+
65
+ export type ThemeName = keyof typeof THEMES;
66
+ export type AuthLayout = (typeof THEMES)[ThemeName]["auth"];
67
+
68
+ /** Set from FLARE_THEME by vite.config.ts when the app is built or run. */
69
+ /**
70
+ * On Cloudflare this comes from a Vite define, so `FLARE_THEME=mono flare dev` can
71
+ * switch the look without editing code. Next has no such constant, so the theme is
72
+ * whatever lib/site.ts says — change it there, or with `flare theme <name>`.
73
+ */
74
+
75
+ export function isThemeName(value: unknown): value is ThemeName {
76
+ return typeof value === "string" && value in THEMES;
77
+ }
78
+
79
+ /** The theme in use: FLARE_THEME if it names one, else lib/site.ts. */
80
+ export const activeTheme: ThemeName = isThemeName(site.theme) ? site.theme : "default";
81
+
82
+ export const theme = THEMES[activeTheme];
83
+ export const authLayout: AuthLayout = theme.auth;
@@ -0,0 +1,23 @@
1
+ import { prisma } from "@/lib/db";
2
+ import { dashboardSession } from "./dashboard";
3
+
4
+ export interface SavedView {
5
+ id: string;
6
+ resource: string;
7
+ name: string;
8
+ query: string;
9
+ }
10
+
11
+ /** How many views one person may keep per resource, so the sidebar stays a sidebar. */
12
+ export const MAX_VIEWS = 12;
13
+
14
+ /** This person's saved views, oldest first so the sidebar doesn't reshuffle. */
15
+ export async function listViews(resource?: string): Promise<SavedView[]> {
16
+ const { session } = await dashboardSession();
17
+ if (!session) return [];
18
+ return prisma.savedView.findMany({
19
+ where: { userId: session.user.id, ...(resource ? { resource } : {}) },
20
+ select: { id: true, resource: true, name: true, query: true },
21
+ orderBy: { createdAt: "asc" },
22
+ });
23
+ }
@@ -0,0 +1,13 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const config: NextConfig = {
4
+ // The generated Prisma client is a real dependency of the server bundle; without this
5
+ // Next tries to trace it as application code and misses the query engine beside it.
6
+ serverExternalPackages: ["@prisma/client", "@prisma/adapter-pg"],
7
+ // Compile @flaredev/core from source rather than resolving it as an opaque package.
8
+ // Needed when it is linked from a checkout or a monorepo: the bundler won't follow a
9
+ // symlink out of the project root, and every import from it fails to resolve.
10
+ transpilePackages: ["@flaredev/core"],
11
+ };
12
+
13
+ export default config;
@@ -0,0 +1,162 @@
1
+ // The part of the schema that is yours.
2
+ //
3
+ // Your resources live in resources.prisma beside this file, written by
4
+ // `flare gen resource` from the descriptors in resources/. This file is never
5
+ // regenerated: the generator, the datasource, the tables Better Auth needs and the two
6
+ // Flare's own dashboard uses are all here, and yours to edit.
7
+
8
+ generator client {
9
+ provider = "prisma-client"
10
+ output = "../../lib/generated/prisma"
11
+ }
12
+
13
+ datasource db {
14
+ provider = "postgresql"
15
+ }
16
+
17
+ /// Better Auth: the signed-in person.
18
+ model User {
19
+ id String @id @default(uuid())
20
+ name String
21
+ email String @unique
22
+ emailVerified Boolean @default(false) @map("email_verified")
23
+ image String?
24
+ /// Set with `flare user:role`; the dashboard's policies read it.
25
+ role String?
26
+ banned Boolean?
27
+ banReason String? @map("ban_reason")
28
+ banExpires DateTime? @map("ban_expires")
29
+ twoFactorEnabled Boolean? @map("two_factor_enabled")
30
+ createdAt DateTime @default(now()) @map("created_at")
31
+ updatedAt DateTime @updatedAt @map("updated_at")
32
+
33
+ sessions Session[]
34
+ accounts Account[]
35
+ passkeys Passkey[]
36
+ twoFactors TwoFactor[]
37
+ auditLogs AuditLog[]
38
+ savedViews SavedView[]
39
+
40
+ @@map("users")
41
+ }
42
+
43
+ model Session {
44
+ id String @id @default(uuid())
45
+ expiresAt DateTime @map("expires_at")
46
+ token String @unique
47
+ ipAddress String? @map("ip_address")
48
+ userAgent String? @map("user_agent")
49
+ userId String @map("user_id")
50
+ impersonatedBy String? @map("impersonated_by")
51
+ createdAt DateTime @default(now()) @map("created_at")
52
+ updatedAt DateTime @updatedAt @map("updated_at")
53
+
54
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
55
+
56
+ @@index([userId])
57
+ @@map("sessions")
58
+ }
59
+
60
+ model Account {
61
+ id String @id @default(uuid())
62
+ accountId String @map("account_id")
63
+ providerId String @map("provider_id")
64
+ userId String @map("user_id")
65
+ accessToken String? @map("access_token")
66
+ refreshToken String? @map("refresh_token")
67
+ idToken String? @map("id_token")
68
+ accessTokenExpiresAt DateTime? @map("access_token_expires_at")
69
+ refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
70
+ scope String?
71
+ password String?
72
+ createdAt DateTime @default(now()) @map("created_at")
73
+ updatedAt DateTime @updatedAt @map("updated_at")
74
+
75
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
76
+
77
+ @@index([userId])
78
+ @@map("accounts")
79
+ }
80
+
81
+ model Verification {
82
+ id String @id @default(uuid())
83
+ identifier String
84
+ value String
85
+ expiresAt DateTime @map("expires_at")
86
+ createdAt DateTime @default(now()) @map("created_at")
87
+ updatedAt DateTime @updatedAt @map("updated_at")
88
+
89
+ @@index([identifier])
90
+ @@map("verifications")
91
+ }
92
+
93
+ model Passkey {
94
+ id String @id @default(uuid())
95
+ name String?
96
+ publicKey String @map("public_key")
97
+ userId String @map("user_id")
98
+ credentialID String @map("credential_id")
99
+ counter Int
100
+ deviceType String @map("device_type")
101
+ backedUp Boolean @map("backed_up")
102
+ transports String?
103
+ aaguid String?
104
+ createdAt DateTime? @default(now()) @map("created_at")
105
+
106
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
107
+
108
+ @@index([userId])
109
+ @@map("passkeys")
110
+ }
111
+
112
+ model TwoFactor {
113
+ id String @id @default(uuid())
114
+ secret String
115
+ backupCodes String @map("backup_codes")
116
+ userId String @map("user_id")
117
+ /// Better Auth checks for these three at startup and refuses to run without them.
118
+ verified Boolean? @default(true)
119
+ failedVerificationCount Int? @default(0) @map("failed_verification_count")
120
+ lockedUntil DateTime? @map("locked_until")
121
+
122
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
123
+
124
+ @@index([secret])
125
+ @@index([userId])
126
+ @@map("two_factors")
127
+ }
128
+
129
+ /// Who changed what, written by the dashboard's actions.
130
+ model AuditLog {
131
+ id String @id @default(uuid())
132
+ action String
133
+ resource String
134
+ recordId String? @map("record_id")
135
+ recordLabel String? @map("record_label")
136
+ userId String? @map("user_id")
137
+ userEmail String? @map("user_email")
138
+ changes Json?
139
+ ip String?
140
+ createdAt DateTime @default(now()) @map("created_at")
141
+
142
+ user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
143
+
144
+ @@index([resource])
145
+ @@index([createdAt])
146
+ @@map("audit_log")
147
+ }
148
+
149
+ /// A saved set of filters on a resource's table.
150
+ model SavedView {
151
+ id String @id @default(uuid())
152
+ name String
153
+ resource String
154
+ query String
155
+ userId String @map("user_id")
156
+ createdAt DateTime @default(now()) @map("created_at")
157
+
158
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
159
+
160
+ @@index([userId, resource])
161
+ @@map("saved_views")
162
+ }
@@ -0,0 +1,15 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ /**
5
+ * Prisma 7 keeps the connection string out of the schema and loads env itself.
6
+ *
7
+ * `schema` points at a folder, not a file: `base.prisma` holds the generator, the
8
+ * datasource and the tables Flare's own features need, and `resources.prisma` is written
9
+ * by `flare gen resource` from your descriptors.
10
+ */
11
+ export default defineConfig({
12
+ schema: "prisma/schema",
13
+ migrations: { path: "prisma/migrations" },
14
+ datasource: { url: env("DATABASE_URL") },
15
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "incremental": true,
15
+ "jsx": "preserve",
16
+ "types": ["node"],
17
+ "plugins": [{ "name": "next" }],
18
+ "paths": {
19
+ "@/*": ["./*"]
20
+ }
21
+ },
22
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
23
+ "exclude": ["node_modules", ".next", "lib/generated"]
24
+ }
@@ -1,80 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { PlusIcon } from "lucide-react";
5
- import type { ClientResource } from "@flaredev/core";
6
- import { Button } from "@/components/ui/button";
7
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
8
- import type { RelationMeta } from "./fields/field-widget";
9
- import { ResourceForm } from "./resource-form";
10
-
11
- export type FormRelations = Record<string, RelationMeta & { initialTitle?: string }>;
12
-
13
- interface Props {
14
- resource: ClientResource;
15
- relations: FormRelations;
16
- listHref: string;
17
- mode: "create" | "edit";
18
- /** Edit: the record and its id. */
19
- id?: string;
20
- record?: Record<string, unknown> | null;
21
- /** Controlled use (a row menu opens it); otherwise pass `trigger`. */
22
- open?: boolean;
23
- onOpenChange?: (open: boolean) => void;
24
- trigger?: React.ReactNode;
25
- }
26
-
27
- /**
28
- * The create/edit form in a dialog, so adding a record doesn't lose the list, its
29
- * filters or the place in it. Apps that would rather have a page set
30
- * `site.dashboard.forms` to "page" (lib/site.ts) and get the same form full width.
31
- */
32
- export function ResourceFormDialog({ resource, relations, listHref, mode, id, record, open, onOpenChange, trigger }: Props) {
33
- const [internalOpen, setInternalOpen] = useState(false);
34
- const isOpen = open ?? internalOpen;
35
- const setOpen = onOpenChange ?? setInternalOpen;
36
- const noun = resource.label.toLowerCase();
37
-
38
- return (
39
- <Dialog open={isOpen} onOpenChange={setOpen}>
40
- {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
41
- <DialogContent className="sm:max-w-2xl">
42
- <DialogHeader>
43
- <DialogTitle>{mode === "create" ? `New ${noun}` : `Edit ${noun}`}</DialogTitle>
44
- <DialogDescription>{mode === "create" ? `Fill this in and it appears in the list.` : "Changes save straight away."}</DialogDescription>
45
- </DialogHeader>
46
- {/* Remounting on open clears whatever was typed and abandoned last time. */}
47
- {isOpen && (
48
- <ResourceForm
49
- key={`${mode}-${id ?? "new"}`}
50
- resource={resource}
51
- mode={mode}
52
- id={id}
53
- record={record}
54
- relations={relations}
55
- listHref={listHref}
56
- onDone={() => setOpen(false)}
57
- />
58
- )}
59
- </DialogContent>
60
- </Dialog>
61
- );
62
- }
63
-
64
- /** The list page's "New …" button, when forms open in a dialog. */
65
- export function NewRecordButton({ resource, relations, listHref }: { resource: ClientResource; relations: FormRelations; listHref: string }) {
66
- return (
67
- <ResourceFormDialog
68
- resource={resource}
69
- relations={relations}
70
- listHref={listHref}
71
- mode="create"
72
- trigger={
73
- <Button>
74
- <PlusIcon data-icon="inline-start" />
75
- New {resource.label.toLowerCase()}
76
- </Button>
77
- }
78
- />
79
- );
80
- }