appflare 0.2.5 → 0.2.7

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 (63) hide show
  1. package/cli/commands/index.ts +73 -0
  2. package/cli/generate.ts +8 -0
  3. package/cli/index.ts +32 -1
  4. package/cli/templates/auth/config.ts +0 -2
  5. package/cli/templates/core/handlers.route.ts +1 -0
  6. package/cli/templates/core/imports.ts +1 -0
  7. package/cli/templates/dashboard/builders/functions/execute-handler.ts +124 -0
  8. package/cli/templates/dashboard/builders/functions/index.ts +22 -0
  9. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -0
  10. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -0
  11. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +67 -0
  12. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +19 -0
  13. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +17 -0
  14. package/cli/templates/dashboard/builders/navigation.ts +122 -0
  15. package/cli/templates/dashboard/builders/storage/index.ts +13 -0
  16. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -0
  17. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -0
  18. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -0
  19. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -0
  20. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -0
  21. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -0
  22. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -0
  23. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -0
  24. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -0
  25. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -0
  26. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -0
  27. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -0
  28. package/cli/templates/dashboard/builders/table-routes/fragments.ts +214 -0
  29. package/cli/templates/dashboard/builders/table-routes/helpers.ts +49 -0
  30. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -0
  31. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -0
  32. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +166 -0
  33. package/cli/templates/dashboard/builders/table-routes/table/index.ts +77 -0
  34. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +111 -0
  35. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -0
  36. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -0
  37. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -0
  38. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -0
  39. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +127 -0
  40. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -0
  41. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -0
  42. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -0
  43. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -0
  44. package/cli/templates/dashboard/components/dashboard-home.ts +23 -0
  45. package/cli/templates/dashboard/components/layout.ts +388 -0
  46. package/cli/templates/dashboard/components/login-page.ts +65 -0
  47. package/cli/templates/dashboard/index.ts +61 -0
  48. package/cli/templates/dashboard/types.ts +9 -0
  49. package/cli/templates/handlers/generators/types/core.ts +5 -0
  50. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +168 -0
  51. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +133 -0
  52. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +686 -0
  53. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +97 -0
  54. package/cli/templates/handlers/generators/types/query-definitions.ts +11 -1083
  55. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -0
  56. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +164 -0
  57. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +85 -0
  58. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -0
  59. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +137 -0
  60. package/cli/templates/handlers/generators/types/query-runtime.ts +13 -431
  61. package/cli/utils/schema-discovery.ts +10 -1
  62. package/package.json +1 -1
  63. package/test-better-auth-hash.ts +2 -0
@@ -159,3 +159,76 @@ export async function runMigrate(
159
159
  );
160
160
  }
161
161
  }
162
+
163
+ export async function runAddAdmin(
164
+ configPath?: string,
165
+ options: {
166
+ name: string;
167
+ email: string;
168
+ password: string;
169
+ local?: boolean;
170
+ remote?: boolean;
171
+ } = { name: "", email: "", password: "" },
172
+ ): Promise<void> {
173
+ const loadedConfig = await loadConfig(configPath);
174
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
175
+
176
+ const selectedTargetCount = [
177
+ Boolean(options.local),
178
+ Boolean(options.remote),
179
+ ].filter(Boolean).length;
180
+
181
+ if (selectedTargetCount > 1) {
182
+ throw new Error("Only one of --local or --remote can be set.");
183
+ }
184
+
185
+ const { hashPassword } = await import("better-auth/crypto");
186
+ const passwordHash = await hashPassword(options.password);
187
+
188
+ const userId = crypto.randomUUID();
189
+ const accountId = crypto.randomUUID();
190
+ const now = Date.now();
191
+
192
+ const safeName = options.name.replace(/'/g, "''");
193
+ const safeEmail = options.email.replace(/'/g, "''");
194
+
195
+ const sqlQuery = `
196
+ INSERT INTO users (id, name, email, email_verified, created_at, updated_at, role, banned)
197
+ VALUES ('${userId}', '${safeName}', '${safeEmail}', 1, ${now}, ${now}, 'admin', 0);
198
+ INSERT INTO accounts (id, account_id, provider_id, user_id, password, created_at, updated_at)
199
+ VALUES ('${accountId}', '${safeEmail}', 'credential', '${userId}', '${passwordHash}', ${now}, ${now});
200
+ `;
201
+
202
+ const databaseName = loadedConfig.config.database[0].databaseName;
203
+ const wranglerArgs = [
204
+ npxCommand,
205
+ "wrangler",
206
+ "d1",
207
+ "execute",
208
+ databaseName,
209
+ "--command",
210
+ sqlQuery,
211
+ ];
212
+
213
+ if (options.local) {
214
+ wranglerArgs.push("--local");
215
+ } else if (options.remote) {
216
+ wranglerArgs.push("--remote");
217
+ }
218
+
219
+ const wranglerExecute = Bun.spawn(wranglerArgs, {
220
+ cwd: loadedConfig.configDir,
221
+ stdin: "inherit",
222
+ stdout: "inherit",
223
+ stderr: "inherit",
224
+ });
225
+
226
+ const wranglerExitCode = await wranglerExecute.exited;
227
+ if (wranglerExitCode !== 0) {
228
+ throw new Error(
229
+ `Failed to add admin user. wrangler d1 execute exited with code ${wranglerExitCode}`,
230
+ );
231
+ }
232
+
233
+ console.log("✅ Admin user " + options.email + " created successfully!");
234
+ }
package/cli/generate.ts CHANGED
@@ -6,6 +6,7 @@ import { generateDrizzleConfigSource } from "./templates/core/drizzle";
6
6
  import { generateHandlersArtifacts } from "./templates/core/handlers";
7
7
  import { generateServerSource } from "./templates/core/server";
8
8
  import { generateWranglerJson } from "./templates/core/wrangler";
9
+ import { generateDashboardSource } from "./templates/dashboard/index";
9
10
  import { compileSchemaDsl } from "./schema-compiler";
10
11
  import type { LoadedAppflareConfig } from "./types";
11
12
  import { discoverHandlerOperations } from "./utils/handler-discovery";
@@ -85,6 +86,12 @@ export async function generateArtifacts(
85
86
  : config.schema;
86
87
  const drizzleConfigSource = generateDrizzleConfigSource(drizzleSchemaPaths);
87
88
  const wranglerJson = generateWranglerJson(loadedConfig, discoveredHandlers);
89
+ const dashboardSource = generateDashboardSource(
90
+ schemaImport,
91
+ discoveredSchema,
92
+ discoveredHandlers,
93
+ );
94
+ const dashboardPath = resolve(outDirAbs, "admin.routes.ts");
88
95
  const handlerWriteOperations = handlerArtifacts.map((artifact) => {
89
96
  return Bun.write(
90
97
  resolve(outDirAbs, artifact.relativePath),
@@ -107,6 +114,7 @@ export async function generateArtifacts(
107
114
  Bun.write(authSchemaPath, ""),
108
115
  Bun.write(drizzleConfigPath, drizzleConfigSource),
109
116
  Bun.write(wranglerPath, `${JSON.stringify(wranglerJson, null, 2)}\n`),
117
+ Bun.write(dashboardPath, dashboardSource),
110
118
  ]);
111
119
 
112
120
  const authConfigPathFromConfigDir = relative(
package/cli/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Command } from "commander";
3
- import { runBuild, runDev, runMigrate } from "./commands/index";
3
+ import { runBuild, runDev, runMigrate, runAddAdmin } from "./commands/index";
4
4
 
5
5
  const program = new Command();
6
6
 
@@ -74,4 +74,35 @@ program
74
74
  },
75
75
  );
76
76
 
77
+ program
78
+ .command("add-admin")
79
+ .description("Add an admin user to the database")
80
+ .requiredOption("-n, --name <name>", "Admin's display name")
81
+ .requiredOption("-e, --email <email>", "Admin's email address")
82
+ .requiredOption("-p, --password <password>", "Admin's password")
83
+ .option(
84
+ "-c, --config <path>",
85
+ "Path to appflare.config.ts",
86
+ "appflare.config.ts",
87
+ )
88
+ .option(
89
+ "--local",
90
+ "Execute command against a local DB for use with wrangler dev",
91
+ false,
92
+ )
93
+ .option(
94
+ "--remote",
95
+ "Execute command against a remote DB for use with wrangler dev --remote",
96
+ false,
97
+ )
98
+ .action(async (options: any) => {
99
+ await runAddAdmin(options.config, {
100
+ name: options.name,
101
+ email: options.email,
102
+ password: options.password,
103
+ local: options.local,
104
+ remote: options.remote,
105
+ });
106
+ });
107
+
77
108
  await program.parseAsync(process.argv);
@@ -37,7 +37,6 @@ export const createAuth = (
37
37
  db,
38
38
  options: {
39
39
  usePlural: true,
40
- debugLogs: true,
41
40
  },
42
41
  }
43
42
  : undefined,
@@ -51,7 +50,6 @@ export const createAuth = (
51
50
  database: drizzleAdapter({} as D1Database, {
52
51
  provider: "sqlite",
53
52
  usePlural: true,
54
- debugLogs: true,
55
53
  }),
56
54
  }),
57
55
  });
@@ -19,5 +19,6 @@ export function generateHandlersRoute(
19
19
  };
20
20
  registerGeneratedHandlers(app, generatedHandlerOptions);
21
21
  registerGeneratedStorageRoutes(app, generatedHandlerOptions);
22
+ registerAdminDashboard(app, generatedHandlerOptions);
22
23
  `;
23
24
  }
@@ -1,6 +1,7 @@
1
1
  export function generateImports(): string {
2
2
  return `import { createAuth } from "./auth.config";
3
3
  import { AppflareRealtimeDurableObject, executeCronTriggers, executeScheduledBatch, registerGeneratedHandlers, registerGeneratedStorageRoutes } from "./handlers.routes";
4
+ import { registerAdminDashboard } from "./admin.routes";
4
5
  import { Hono } from "hono";
5
6
  import { cors } from "hono/cors";
6
7
  import type { D1Database, IncomingRequestCfProperties, KVNamespace } from "@cloudflare/workers-types";
@@ -0,0 +1,124 @@
1
+ import { DiscoveredHandlerOperation } from "../../../../utils/handler-discovery";
2
+
3
+ export function buildExecutionLogic(h: DiscoveredHandlerOperation): string {
4
+ return `
5
+ const body = await c.req.json();
6
+ let args = {};
7
+ let customHeaders: Record<string, string> = {};
8
+ let token = body.token || "";
9
+
10
+ try {
11
+ args = typeof body.args === 'string' && body.args.trim() ? JSON.parse(body.args) : body.args || {};
12
+ } catch (e) {}
13
+
14
+ try {
15
+ if (body.headers && typeof body.headers === 'string' && body.headers.trim()) {
16
+ customHeaders = JSON.parse(body.headers);
17
+ } else if (body.headers && typeof body.headers === 'object') {
18
+ customHeaders = body.headers;
19
+ }
20
+ } catch (e) {}
21
+
22
+ try {
23
+ // We'll perform a local fetch to the actual API endpoint for maximum compatibility
24
+ const protocol = c.req.raw.url.startsWith('https') ? 'https' : 'http';
25
+ const host = c.req.header('host');
26
+ const baseUrl = body.baseUrl || c.env?.APPFLARE_API_BASE || \`\${protocol}://\${host}\`;
27
+ const isQuery = ${h.kind === "query"};
28
+ const method = isQuery ? "GET" : "POST";
29
+
30
+ let pathWithQuery = \`${h.routePath}\`;
31
+ if (isQuery && args && typeof args === "object") {
32
+ const params = new URLSearchParams();
33
+ for (const [key, value] of Object.entries(args)) {
34
+ if (value !== undefined && value !== null) {
35
+ if (Array.isArray(value)) {
36
+ value.forEach(v => params.append(key, typeof v === "object" ? JSON.stringify(v) : String(v)));
37
+ } else if (typeof value === "object") {
38
+ params.append(key, JSON.stringify(value));
39
+ } else {
40
+ params.append(key, String(value));
41
+ }
42
+ }
43
+ }
44
+ const qs = params.toString();
45
+ if (qs) {
46
+ pathWithQuery += \`?\${qs}\`;
47
+ }
48
+ }
49
+
50
+ const targetUrl = \`\${baseUrl}\${pathWithQuery}\`;
51
+ const internalUrl = \`https://internal\${pathWithQuery}\`;
52
+
53
+ const fetchHeaders: Record<string, string> = {
54
+ 'Content-Type': 'application/json',
55
+ 'Cookie': c.req.header('cookie') || '',
56
+ ...customHeaders
57
+ };
58
+
59
+ if (token) {
60
+ fetchHeaders['Authorization'] = \`Bearer \${token}\`;
61
+ }
62
+
63
+ const requestInit: RequestInit = {
64
+ method,
65
+ headers: fetchHeaders,
66
+ body: method === "POST" ? JSON.stringify(args) : undefined
67
+ };
68
+
69
+ let res: Response;
70
+ let transport = "service-binding";
71
+ try {
72
+ if (!c.env?.INTERNAL_WORKER || typeof c.env.INTERNAL_WORKER.fetch !== "function") {
73
+ throw new Error("INTERNAL_WORKER binding not configured");
74
+ }
75
+ res = await c.env.INTERNAL_WORKER.fetch(internalUrl, requestInit);
76
+ } catch (bindingErr) {
77
+ transport = "url-fetch";
78
+ res = await fetch(targetUrl, requestInit);
79
+ }
80
+
81
+ console.log('Fetch transport:', transport, 'target:', transport === 'service-binding' ? internalUrl : targetUrl, 'status:', res.status, res.statusText);
82
+ const contentType = res.headers.get('content-type') || '';
83
+ const rawBody = await res.text();
84
+ let parsedBody: any = rawBody;
85
+ if (rawBody && contentType.includes('application/json')) {
86
+ try {
87
+ parsedBody = JSON.parse(rawBody);
88
+ } catch (parseErr: any) {
89
+ parsedBody = {
90
+ parseError: parseErr?.message || 'Failed to parse JSON',
91
+ body: rawBody
92
+ };
93
+ }
94
+ }
95
+
96
+ const result = {
97
+ transport,
98
+ method,
99
+ internalUrl,
100
+ targetUrl,
101
+ status: res.status,
102
+ statusText: res.statusText,
103
+ headers: Object.fromEntries(res.headers),
104
+ body: parsedBody
105
+ };
106
+
107
+ return c.html(\`
108
+ <div class="p-5 font-mono text-sm h-full overflow-auto">
109
+ <pre class="text-success"><code class="language-json">\${JSON.stringify(result, null, 2)\}</code></pre>
110
+ </div>
111
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
112
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
113
+ <script>hljs.highlightAll();</script>
114
+ \`);
115
+ } catch (err: any) {
116
+ return c.html(\`
117
+ <div class="p-5 font-mono text-sm h-full overflow-auto text-error bg-error/5">
118
+ <p class="font-bold mb-2">Execution Error:</p>
119
+ <pre>\${err.message}\\n\${err.stack}</pre>
120
+ </div>
121
+ \`);
122
+ }
123
+ `;
124
+ }
@@ -0,0 +1,22 @@
1
+ import { DiscoveredHandlerOperation } from "../../../../utils/handler-discovery";
2
+ import { buildFunctionPage } from "./render-page";
3
+ import { buildExecutionLogic } from "./execute-handler";
4
+
5
+ export function buildFunctionRoutes(
6
+ handlers: DiscoveredHandlerOperation[],
7
+ ): string {
8
+ const routes = handlers.map((h) => {
9
+ if (h.kind !== "query" && h.kind !== "mutation") return "";
10
+
11
+ return `
12
+ adminApp.get('/functions${h.routePath}', (c) => {
13
+ ${buildFunctionPage(h)}
14
+ });
15
+
16
+ adminApp.post('/functions/execute${h.routePath}', async (c) => {
17
+ ${buildExecutionLogic(h)}
18
+ });`;
19
+ });
20
+
21
+ return routes.join("\n");
22
+ }
@@ -0,0 +1,20 @@
1
+ import { DiscoveredHandlerOperation } from "../../../../../utils/handler-discovery";
2
+
3
+ export function renderHeader(h: DiscoveredHandlerOperation): string {
4
+ return `
5
+ <div class="flex items-center justify-between">
6
+ <div class="flex items-center gap-3">
7
+ <div class="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
8
+ <iconify-icon icon="${h.kind === "query" ? "solar:reorder-bold-duotone" : "solar:bolt-bold-duotone"}" width="24" height="24" class="text-primary"></iconify-icon>
9
+ </div>
10
+ <div>
11
+ <h1 class="text-xl font-bold tracking-tight">${h.exportName}</h1>
12
+ <p class="text-xs opacity-50 font-medium uppercase tracking-wider">${h.kind}</p>
13
+ </div>
14
+ </div>
15
+ <div class="flex items-center gap-2">
16
+ <span class="badge badge-sm badge-ghost font-mono opacity-50 px-2 py-3">/api${h.routePath}</span>
17
+ </div>
18
+ </div>
19
+ `;
20
+ }
@@ -0,0 +1,33 @@
1
+ import { DiscoveredHandlerOperation } from "../../../../../utils/handler-discovery";
2
+ import { renderHeader } from "./header";
3
+ import { renderRequestPanel } from "./request-panel";
4
+ import { renderResultPanel } from "./result-panel";
5
+ import { renderScripts } from "./scripts";
6
+
7
+ export function buildFunctionPage(h: DiscoveredHandlerOperation): string {
8
+ return `
9
+ const content = html\`
10
+ <div class="flex flex-col gap-6 max-w-5xl mx-auto" id="main-content">
11
+ ${renderHeader(h)}
12
+
13
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
14
+ <!-- Request Panel -->
15
+ ${renderRequestPanel(h)}
16
+
17
+ <!-- Result Panel -->
18
+ ${renderResultPanel()}
19
+ </div>
20
+ </div>
21
+
22
+ ${renderScripts()}
23
+ \`;
24
+
25
+ if (c.req.header('hx-request')) {
26
+ return c.html(content);
27
+ }
28
+
29
+ return c.html(Layout({
30
+ title: "${h.exportName} - Functions",
31
+ children: content
32
+ }));`;
33
+ }
@@ -0,0 +1,67 @@
1
+ import { DiscoveredHandlerOperation } from "../../../../../utils/handler-discovery";
2
+
3
+ export function renderRequestPanel(h: DiscoveredHandlerOperation): string {
4
+ return `
5
+ <div class="card bg-base-100 border border-base-200 shadow-sm overflow-hidden">
6
+ <div class="px-5 py-4 border-b border-base-200 bg-base-200/20 flex items-center justify-between">
7
+ <h3 class="text-xs font-bold uppercase tracking-widest opacity-40">Request Parameters</h3>
8
+ <iconify-icon icon="solar:settings-linear" width="16" height="16" class="opacity-30"></iconify-icon>
9
+ </div>
10
+ <div class="p-5">
11
+ <form hx-post="/admin/functions/execute${h.routePath}" hx-target="#execution-result" hx-ext="json-enc" class="flex flex-col gap-5">
12
+ <div class="form-control">
13
+ <label class="label pt-0">
14
+ <span class="label-text text-[11px] font-bold uppercase tracking-wider opacity-40">Arguments (JSON)</span>
15
+ </label>
16
+ <div class="relative group">
17
+ <textarea
18
+ name="args"
19
+ class="textarea textarea-bordered font-mono text-sm w-full min-h-[160px] bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200"
20
+ placeholder='{ }'
21
+ >{}</textarea>
22
+ <div class="absolute right-3 top-3 opacity-0 group-hover:opacity-40 transition-opacity pointer-events-none">
23
+ <iconify-icon icon="solar:code-file-linear" width="18" height="18"></iconify-icon>
24
+ </div>
25
+ </div>
26
+ <label class="label">
27
+ <span class="label-text-alt opacity-40 italic">Provide the JSON arguments for this ${h.kind}</span>
28
+ </label>
29
+ </div>
30
+
31
+ <div class="form-control">
32
+ <label class="label pt-0">
33
+ <span class="label-text text-[11px] font-bold uppercase tracking-wider opacity-40">Bearer Token (Optional)</span>
34
+ </label>
35
+ <div class="relative group">
36
+ <input type="text" name="token" class="input input-sm input-bordered font-mono text-sm w-full bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200" placeholder="e.g. eyJhbGciOi..." />
37
+ <div class="absolute right-3 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-40 transition-opacity pointer-events-none">
38
+ <iconify-icon icon="solar:key-linear" width="16" height="16"></iconify-icon>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="form-control">
44
+ <label class="label pt-0">
45
+ <span class="label-text text-[11px] font-bold uppercase tracking-wider opacity-40">Custom Headers (JSON)</span>
46
+ </label>
47
+ <div class="relative group">
48
+ <textarea
49
+ name="headers"
50
+ class="textarea textarea-bordered font-mono text-sm w-full min-h-[80px] bg-base-200/30 focus:bg-base-100 focus:border-primary transition-all rounded-xl border-base-200"
51
+ placeholder='{ "x-custom-header": "value" }'
52
+ ></textarea>
53
+ <div class="absolute right-3 top-3 opacity-0 group-hover:opacity-40 transition-opacity pointer-events-none">
54
+ <iconify-icon icon="solar:list-linear" width="18" height="18"></iconify-icon>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <button type="submit" class="btn btn-primary w-full gap-2 rounded-xl shadow-lg shadow-primary/20 hover:shadow-xl hover:shadow-primary/30 transition-all font-semibold">
60
+ <iconify-icon icon="solar:play-bold" width="18" height="18"></iconify-icon>
61
+ Run ${h.kind}
62
+ </button>
63
+ </form>
64
+ </div>
65
+ </div>
66
+ `;
67
+ }
@@ -0,0 +1,19 @@
1
+ export function renderResultPanel(): string {
2
+ return `
3
+ <div class="card bg-base-100 border border-base-200 shadow-sm overflow-hidden flex flex-col">
4
+ <div class="px-5 py-4 border-b border-base-200 bg-base-200/20 flex items-center justify-between">
5
+ <h3 class="text-xs font-bold uppercase tracking-widest opacity-40">Result</h3>
6
+ <div class="flex items-center gap-2">
7
+ <div class="w-1.5 h-1.5 rounded-full bg-success animate-pulse opacity-0" id="execution-indicator"></div>
8
+ <iconify-icon icon="solar:box-minimalistic-linear" width="16" height="16" class="opacity-30"></iconify-icon>
9
+ </div>
10
+ </div>
11
+ <div class="flex-1 p-0 relative min-h-[300px] bg-neutral/5" id="execution-result">
12
+ <div class="absolute inset-0 flex flex-col items-center justify-center opacity-20 pointer-events-none p-10 text-center">
13
+ <iconify-icon icon="solar:course-down-bold-duotone" width="48" height="48" class="mb-3"></iconify-icon>
14
+ <p class="text-xs font-semibold uppercase tracking-widest">Execute To See Output</p>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ `;
19
+ }
@@ -0,0 +1,17 @@
1
+ export function renderScripts(): string {
2
+ return `
3
+ <script>
4
+ document.body.addEventListener('htmx:beforeRequest', function(e) {
5
+ if (e.detail.target.id === 'execution-result') {
6
+ document.getElementById('execution-indicator')?.classList.remove('opacity-0');
7
+ e.detail.target.innerHTML = '<div class="absolute inset-0 flex items-center justify-center bg-base-100/50 backdrop-blur-[2px] z-10"><span class="loading loading-ring loading-md text-primary"></span></div>';
8
+ }
9
+ });
10
+ document.body.addEventListener('htmx:afterRequest', function(e) {
11
+ if (e.detail.target.id === 'execution-result') {
12
+ document.getElementById('execution-indicator')?.classList.add('opacity-0');
13
+ }
14
+ });
15
+ </script>
16
+ `;
17
+ }
@@ -0,0 +1,122 @@
1
+ import { DiscoveredSchema } from "../../../utils/schema-discovery";
2
+ import { DiscoveredHandlerOperation } from "../../../utils/handler-discovery";
3
+ import { TableInfo } from "../types";
4
+
5
+ export function collectTablesInfo(schema: DiscoveredSchema): TableInfo[] {
6
+ return schema.tables.map((table) => ({
7
+ exportName: table.exportName,
8
+ tableName: table.tableName,
9
+ columns: table.columns.map((column) => column.name),
10
+ }));
11
+ }
12
+
13
+ /** Generates the right-pane list items: users entry first, then schema tables */
14
+ export function buildSidebarTableList(tablesInfo: TableInfo[]): string {
15
+ const tableItems = tablesInfo
16
+ .map(
17
+ (table) =>
18
+ `<li data-name="${table.tableName}">
19
+ \t\t\t\t\t\t<a href="/admin/table/${table.exportName}" hx-get="/admin/table/${table.exportName}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
20
+ \t\t\t\t\t\t\t<iconify-icon icon="mdi:table" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
21
+ \t\t\t\t\t\t\t<span class="truncate">${table.tableName}</span>
22
+ \t\t\t\t\t\t</a>
23
+ \t\t\t\t\t</li>`,
24
+ )
25
+ .join("\n\t\t\t\t\t");
26
+
27
+ return `<li data-name="users">
28
+ \t\t\t\t\t\t<a href="/admin/users" hx-get="/admin/users" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
29
+ \t\t\t\t\t\t\t<iconify-icon icon="mdi:account-group" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
30
+ \t\t\t\t\t\t\t<span class="truncate">users</span>
31
+ \t\t\t\t\t\t</a>
32
+ \t\t\t\t\t</li>
33
+ \t\t\t\t\t${tableItems}`;
34
+ }
35
+
36
+ export function buildSidebarFunctionList(
37
+ handlers: DiscoveredHandlerOperation[],
38
+ ): string {
39
+ const queries = handlers.filter((h) => h.kind === "query");
40
+ const mutations = handlers.filter((h) => h.kind === "mutation");
41
+
42
+ const queryItems = queries
43
+ .map(
44
+ (h) => `
45
+ <li data-name="${h.exportName}">
46
+ <a href="/admin/functions${h.routePath}" hx-get="/admin/functions${h.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
47
+ <iconify-icon icon="solar:reorder-linear" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
48
+ <span class="truncate">${h.exportName}</span>
49
+ </a>
50
+ </li>`,
51
+ )
52
+ .join("");
53
+
54
+ const mutationItems = mutations
55
+ .map(
56
+ (h) => `
57
+ <li data-name="${h.exportName}">
58
+ <a href="/admin/functions${h.routePath}" hx-get="/admin/functions${h.routePath}" hx-target="#main-content" hx-push-url="true" hx-swap="outerHTML" class="sidebar-link flex items-center gap-2 px-3 py-2 text-sm rounded-lg w-full">
59
+ <iconify-icon icon="solar:bolt-linear" width="16" height="16" class="opacity-50 shrink-0"></iconify-icon>
60
+ <span class="truncate">${h.exportName}</span>
61
+ </a>
62
+ </li>`,
63
+ )
64
+ .join("");
65
+
66
+ return `
67
+ <div id="pane-functions" class="flex flex-col h-full hidden">
68
+ <div class="px-3 pt-5 pb-3">
69
+ <p class="text-[10px] font-semibold uppercase tracking-widest opacity-35 mb-3 px-1">Functions</p>
70
+ <div class="relative">
71
+ <iconify-icon icon="solar:magnifer-linear" width="13" height="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 opacity-35 pointer-events-none"></iconify-icon>
72
+ <input
73
+ type="text"
74
+ id="function-search"
75
+ placeholder="Search functions..."
76
+ class="input input-sm border border-base-200 bg-base-200/50 focus:bg-base-100 focus:border-primary focus:outline-none w-full pl-7 text-xs rounded-lg h-8"
77
+ onkeyup="filterFunctions(this.value)"
78
+ />
79
+ </div>
80
+ </div>
81
+ <nav class="flex-1 overflow-y-auto px-2 pb-4">
82
+ ${
83
+ queries.length > 0
84
+ ? `<p class="text-[9px] font-bold uppercase tracking-wider opacity-25 mt-4 mb-1 px-2">Queries</p>
85
+ <ul class="flex flex-col gap-0.5">${queryItems}</ul>`
86
+ : ""
87
+ }
88
+ ${
89
+ mutations.length > 0
90
+ ? `<p class="text-[9px] font-bold uppercase tracking-wider opacity-25 mt-4 mb-1 px-2">Mutations</p>
91
+ <ul class="flex flex-col gap-0.5">${mutationItems}</ul>`
92
+ : ""
93
+ }
94
+ </nav>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ export function buildDashboardCards(tablesInfo: TableInfo[]): string {
100
+ return tablesInfo
101
+ .map((table) =>
102
+ `
103
+ <a
104
+ href="/admin/table/${table.exportName}"
105
+ class="card bg-base-100 border border-base-200 hover:border-primary/30 hover:shadow-md transition-all cursor-pointer group"
106
+ >
107
+ <div class="card-body p-5">
108
+ <div class="flex items-center gap-3">
109
+ <div class="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center">
110
+ <iconify-icon icon="mdi:table" width="20" height="20" class="text-primary"></iconify-icon>
111
+ </div>
112
+ <div>
113
+ <h2 class="font-semibold text-sm capitalize group-hover:text-primary transition-colors">${table.tableName}</h2>
114
+ <p class="text-xs opacity-40 mt-0.5">Manage records</p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </a>
119
+ `.replace(/\n/g, "\\n"),
120
+ )
121
+ .join("");
122
+ }
@@ -0,0 +1,13 @@
1
+ import { buildStorageRuntimeHelpers } from "./runtime/helpers";
2
+ import { buildStoragePageRuntime } from "./runtime/storage-page";
3
+ import { buildStorageRouteHandlers } from "./routes";
4
+
5
+ export function buildStorageRoutes(): string {
6
+ return `
7
+ ${buildStorageRuntimeHelpers()}
8
+
9
+ ${buildStoragePageRuntime()}
10
+
11
+ ${buildStorageRouteHandlers()}
12
+ `;
13
+ }
@@ -0,0 +1,29 @@
1
+ export function buildStorageCreateDirectoryRoute(): string {
2
+ return `
3
+ adminApp.post('/storage/directory', async (c) => {
4
+ const bucket = getStorageBucket(c);
5
+ if (!bucket) return c.text("Storage not configured", 400);
6
+
7
+ const body = await c.req.parseBody();
8
+ const prefix = normalizePrefix((body['prefix'] as string) || '');
9
+ const directory = ((body['directory'] as string) || '').trim();
10
+
11
+ if (directory) {
12
+ const normalizedDirectory = directory
13
+ .split('/')
14
+ .map((part) => part.trim())
15
+ .filter(Boolean)
16
+ .join('/');
17
+
18
+ if (normalizedDirectory) {
19
+ const markerKey = prefix + normalizedDirectory + '/.keep';
20
+ await bucket.put(markerKey, '', {
21
+ httpMetadata: { contentType: 'text/plain; charset=utf-8' },
22
+ });
23
+ }
24
+ }
25
+
26
+ return c.redirect(prefixToStoragePath(prefix));
27
+ });
28
+ `;
29
+ }