appflare 0.2.4 → 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 +98 -4
- 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/registration/modules/realtime/durable-object.ts +1 -1
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
3
4
|
import { generateArtifacts } from "../generate";
|
|
4
5
|
import { loadConfig } from "../load-config";
|
|
5
6
|
|
|
@@ -9,6 +10,23 @@ type MigrateOptions = {
|
|
|
9
10
|
preview?: boolean;
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
function findNearestPackageDir(startDir: string): string {
|
|
14
|
+
let currentDir = startDir;
|
|
15
|
+
|
|
16
|
+
while (true) {
|
|
17
|
+
if (existsSync(resolve(currentDir, "package.json"))) {
|
|
18
|
+
return currentDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parentDir = dirname(currentDir);
|
|
22
|
+
if (parentDir === currentDir) {
|
|
23
|
+
return startDir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
currentDir = parentDir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export async function runBuild(configPath?: string): Promise<void> {
|
|
13
31
|
const loadedConfig = await loadConfig(configPath);
|
|
14
32
|
await generateArtifacts(loadedConfig);
|
|
@@ -75,6 +93,8 @@ export async function runMigrate(
|
|
|
75
93
|
options: MigrateOptions = {},
|
|
76
94
|
): Promise<void> {
|
|
77
95
|
const loadedConfig = await loadConfig(configPath);
|
|
96
|
+
const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
97
|
+
const packageDir = findNearestPackageDir(process.cwd());
|
|
78
98
|
const selectedTargetCount = [
|
|
79
99
|
Boolean(options.local),
|
|
80
100
|
Boolean(options.remote),
|
|
@@ -90,15 +110,16 @@ export async function runMigrate(
|
|
|
90
110
|
"drizzle.config.ts",
|
|
91
111
|
);
|
|
92
112
|
const drizzleGenerate = Bun.spawn(
|
|
93
|
-
[
|
|
113
|
+
[npxCommand, "drizzle-kit", "generate", "--config", drizzleConfigPath],
|
|
94
114
|
{
|
|
95
|
-
cwd:
|
|
115
|
+
cwd: packageDir,
|
|
96
116
|
stdin: "inherit",
|
|
97
117
|
stdout: "inherit",
|
|
98
118
|
stderr: "inherit",
|
|
99
119
|
},
|
|
100
120
|
);
|
|
101
121
|
|
|
122
|
+
console.log(`npx drizzle-kit generate --config ${drizzleConfigPath}`);
|
|
102
123
|
const drizzleExitCode = await drizzleGenerate.exited;
|
|
103
124
|
if (drizzleExitCode !== 0) {
|
|
104
125
|
throw new Error(
|
|
@@ -108,7 +129,7 @@ export async function runMigrate(
|
|
|
108
129
|
|
|
109
130
|
const databaseName = loadedConfig.config.database[0].databaseName;
|
|
110
131
|
const wranglerArgs = [
|
|
111
|
-
|
|
132
|
+
npxCommand,
|
|
112
133
|
"wrangler",
|
|
113
134
|
"d1",
|
|
114
135
|
"migrations",
|
|
@@ -138,3 +159,76 @@ export async function runMigrate(
|
|
|
138
159
|
);
|
|
139
160
|
}
|
|
140
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
|
+
}
|