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.
- package/cli/commands/index.ts +73 -0
- package/cli/generate.ts +8 -0
- package/cli/index.ts +32 -1
- package/cli/templates/auth/config.ts +0 -2
- package/cli/templates/core/handlers.route.ts +1 -0
- package/cli/templates/core/imports.ts +1 -0
- package/cli/templates/dashboard/builders/functions/execute-handler.ts +124 -0
- package/cli/templates/dashboard/builders/functions/index.ts +22 -0
- package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -0
- package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -0
- package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +67 -0
- package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +19 -0
- package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +17 -0
- package/cli/templates/dashboard/builders/navigation.ts +122 -0
- package/cli/templates/dashboard/builders/storage/index.ts +13 -0
- package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -0
- package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -0
- package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -0
- package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -0
- package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -0
- package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -0
- package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -0
- package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -0
- package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -0
- package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -0
- package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -0
- package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -0
- package/cli/templates/dashboard/builders/table-routes/fragments.ts +214 -0
- package/cli/templates/dashboard/builders/table-routes/helpers.ts +49 -0
- package/cli/templates/dashboard/builders/table-routes/index.ts +8 -0
- package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -0
- package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +166 -0
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +77 -0
- package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +111 -0
- package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -0
- package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +127 -0
- package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -0
- package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -0
- package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -0
- package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -0
- package/cli/templates/dashboard/components/dashboard-home.ts +23 -0
- package/cli/templates/dashboard/components/layout.ts +388 -0
- package/cli/templates/dashboard/components/login-page.ts +65 -0
- package/cli/templates/dashboard/index.ts +61 -0
- package/cli/templates/dashboard/types.ts +9 -0
- package/cli/templates/handlers/generators/types/core.ts +5 -0
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +168 -0
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +133 -0
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +686 -0
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +97 -0
- package/cli/templates/handlers/generators/types/query-definitions.ts +11 -1083
- package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +164 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +85 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +137 -0
- package/cli/templates/handlers/generators/types/query-runtime.ts +13 -431
- package/cli/utils/schema-discovery.ts +10 -1
- package/package.json +1 -1
- package/test-better-auth-hash.ts +2 -0
package/cli/commands/index.ts
CHANGED
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|