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
@@ -0,0 +1,49 @@
1
+ import { DiscoveredTable } from "../../types";
2
+
3
+ export function resolvePrimaryKey(table: DiscoveredTable): string {
4
+ return (
5
+ table.columns.find((column) => column.primaryKey)?.name ||
6
+ table.columns[0]?.name ||
7
+ ""
8
+ );
9
+ }
10
+
11
+ function hasColumnDefault(expression: string): boolean {
12
+ return /\.default\s*\(/i.test(expression);
13
+ }
14
+
15
+ export function shouldIncludeCreateField(
16
+ table: DiscoveredTable,
17
+ columnName: string,
18
+ ): boolean {
19
+ const column = table.columns.find((item) => item.name === columnName);
20
+ if (!column) {
21
+ return false;
22
+ }
23
+
24
+ if (column.autoIncrement) {
25
+ return false;
26
+ }
27
+
28
+ if (column.primaryKey && hasColumnDefault(column.expression)) {
29
+ return false;
30
+ }
31
+
32
+ return true;
33
+ }
34
+
35
+ export function shouldIncludeEditField(
36
+ table: DiscoveredTable,
37
+ columnName: string,
38
+ ): boolean {
39
+ const column = table.columns.find((item) => item.name === columnName);
40
+ if (!column) {
41
+ return false;
42
+ }
43
+
44
+ if (column.primaryKey || column.autoIncrement) {
45
+ return false;
46
+ }
47
+
48
+ return true;
49
+ }
@@ -0,0 +1,8 @@
1
+ import { DiscoveredSchema } from "../../../../utils/schema-discovery";
2
+ import { buildTableRoute } from "./table-route";
3
+ import { buildUsersRoute } from "./users-route";
4
+
5
+ export function buildTableRoutes(schema: DiscoveredSchema): string {
6
+ return `${schema.tables.map((table) => buildTableRoute(table)).join("\n")}
7
+ ${buildUsersRoute()}`;
8
+ }
@@ -0,0 +1,71 @@
1
+ import { DiscoveredTable } from "../../../types";
2
+
3
+ /**
4
+ * Builds the actions cell HTML string (a build-time template string) for a table row.
5
+ * PocketBase-style with icon-based edit and a ⋯ dropdown for delete.
6
+ */
7
+ export function buildActionsCell(
8
+ table: DiscoveredTable,
9
+ hasPrimaryKey: boolean,
10
+ primaryKey: string,
11
+ editInputs: string,
12
+ ): string {
13
+ if (!hasPrimaryKey) {
14
+ return `<td class="text-right"><span class="text-xs opacity-30">No primary key</span></td>`;
15
+ }
16
+
17
+ return `<td class="text-right">
18
+ \t\t<div class="drawer drawer-end">
19
+ \t\t\t<input id="edit-drawer-${table.exportName}-\${rowIndex}" type="checkbox" class="drawer-toggle" />
20
+ \t\t\t<div class="drawer-content">
21
+ \t\t\t\t<div class="flex items-center justify-end gap-1">
22
+ \t\t\t\t\t<label for="edit-drawer-${table.exportName}-\${rowIndex}" class="btn btn-ghost btn-xs btn-square" title="Edit">
23
+ \t\t\t\t\t\t<iconify-icon icon="mdi:pencil-outline" width="15" height="15" class="opacity-50"></iconify-icon>
24
+ \t\t\t\t\t</label>
25
+ \t\t\t\t\t<button type="button" class="btn btn-ghost btn-xs btn-square" title="Delete" onclick="document.getElementById('delete-dialog-${table.exportName}-\${rowIndex}').showModal()">
26
+ \t\t\t\t\t\t<iconify-icon icon="mdi:delete-outline" width="15" height="15" class="opacity-50 hover:text-error"></iconify-icon>
27
+ \t\t\t\t\t</button>
28
+ \t\t\t\t</div>
29
+ \t\t\t\t<dialog id="delete-dialog-${table.exportName}-\${rowIndex}" class="modal">
30
+ \t\t\t\t\t<div class="modal-box max-w-sm">
31
+ \t\t\t\t\t\t<h3 class="font-semibold text-base">Delete record?</h3>
32
+ \t\t\t\t\t\t<p class="py-3 text-sm text-base-content/60">This action cannot be undone.</p>
33
+ \t\t\t\t\t\t<div class="modal-action">
34
+ \t\t\t\t\t\t\t<form method="dialog">
35
+ \t\t\t\t\t\t\t\t<button class="btn btn-ghost btn-sm">Cancel</button>
36
+ \t\t\t\t\t\t\t</form>
37
+ \t\t\t\t\t\t\t<form hx-post="/admin/table/${table.exportName}/delete" hx-target="#main-content" hx-swap="outerHTML" class="inline">
38
+ \t\t\t\t\t\t\t\t<input type="hidden" name="${primaryKey}" value="\${String((row as any).${primaryKey} ?? '')}" />
39
+ \t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
40
+ \t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
41
+ \t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
42
+ \t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
43
+ \t\t\t\t\t\t\t\t<button type="submit" class="btn btn-error btn-sm">Delete</button>
44
+ \t\t\t\t\t\t\t</form>
45
+ \t\t\t\t\t\t</div>
46
+ \t\t\t\t\t</div>
47
+ \t\t\t\t</dialog>
48
+ \t\t\t</div>
49
+ \t\t\t<div class="drawer-side z-50">
50
+ \t\t\t\t<label for="edit-drawer-${table.exportName}-\${rowIndex}" aria-label="close sidebar" class="drawer-overlay"></label>
51
+ \t\t\t\t<div class="w-full max-w-md min-h-full bg-base-100 p-6 border-l border-base-200 overflow-y-auto shadow-lg">
52
+ \t\t\t\t\t<div class="flex justify-between items-center mb-5">
53
+ \t\t\t\t\t\t<h3 class="text-base font-semibold">Edit ${table.tableName}</h3>
54
+ \t\t\t\t\t\t<label for="edit-drawer-${table.exportName}-\${rowIndex}" class="btn btn-sm btn-ghost btn-square">
55
+ \t\t\t\t\t\t\t<iconify-icon icon="mdi:close" width="18" height="18"></iconify-icon>
56
+ \t\t\t\t\t\t</label>
57
+ \t\t\t\t\t</div>
58
+ \t\t\t\t\t<form hx-post="/admin/table/${table.exportName}/edit" hx-target="#main-content" hx-swap="outerHTML" class="flex flex-col gap-4">
59
+ \t\t\t\t\t\t<input type="hidden" name="${primaryKey}" value="\${String((row as any).${primaryKey} ?? '')}" />
60
+ \t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
61
+ \t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
62
+ \t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
63
+ \t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
64
+ \t\t\t\t\t\t${editInputs}
65
+ \t\t\t\t\t\t<button type="submit" class="btn btn-primary btn-sm mt-2">Save changes</button>
66
+ \t\t\t\t\t</form>
67
+ \t\t\t\t</div>
68
+ \t\t\t</div>
69
+ \t\t</div>
70
+ \t</td>`;
71
+ }
@@ -0,0 +1,166 @@
1
+ import { DiscoveredTable } from "../../../types";
2
+ import { buildColumnHeaders, buildRowCells } from "../fragments";
3
+ import { buildPaginationHtml } from "../common/pagination";
4
+ import { buildSearchBarHtml } from "../common/search-bar";
5
+ import { buildActionsCell } from "./actions-cell";
6
+ import { resolvePrimaryKey } from "../helpers";
7
+ import { buildFieldInput, buildSearchConditions } from "../fragments";
8
+ import { shouldIncludeEditField, shouldIncludeCreateField } from "../helpers";
9
+
10
+ /**
11
+ * Builds the GET route handler code string for /admin/table/:tableName
12
+ * PocketBase-style layout with breadcrumb header, filter bar, and clean table.
13
+ */
14
+ export function buildTableGetRoute(
15
+ table: DiscoveredTable,
16
+ defaultSort: string,
17
+ columns: string[],
18
+ searchConditions: string,
19
+ headers: string,
20
+ rowCells: string,
21
+ actionsCell: string,
22
+ createInputs: string,
23
+ ): string {
24
+ const paginationHtml = buildPaginationHtml(
25
+ `/admin/table/${table.exportName}`,
26
+ );
27
+ const searchBarHtml = buildSearchBarHtml(
28
+ `/admin/table/${table.exportName}`,
29
+ "Search term or filter...",
30
+ );
31
+
32
+ return `
33
+ \tadminApp.get('/table/${table.exportName}', async (c) => {
34
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
35
+ \t\tconst page = parseInt(c.req.query('page') || '1');
36
+ \t\tconst limit = 20;
37
+ \t\tconst offset = (page - 1) * limit;
38
+ \t\tconst sort = c.req.query('sort') || '${defaultSort}';
39
+ \t\tconst order = c.req.query('order') || 'desc';
40
+ \t\tconst search = c.req.query('search') || '';
41
+
42
+ \t\tlet tableSchema = (schema as any).${table.exportName};
43
+ \t\tif (!tableSchema) return c.text("Table missing", 404);
44
+
45
+ \t\tlet query = db.select().from(tableSchema);
46
+ \t\tlet countQuery = db.select({ count: sql\`count(*)\` }).from(tableSchema);
47
+
48
+ \t\tif (search) {
49
+ \t\t\tconst searchConditions = [];
50
+ \t\t\t${searchConditions}
51
+ \t\t\tif (searchConditions.length > 0) {
52
+ \t\t\t\tquery = query.where(or(...searchConditions)) as any;
53
+ \t\t\t\tcountQuery = countQuery.where(or(...searchConditions)) as any;
54
+ \t\t\t}
55
+ \t\t}
56
+
57
+ \t\tif (sort && tableSchema[sort]) {
58
+ \t\t\tquery = query.orderBy(order === 'asc' ? asc(tableSchema[sort]) : desc(tableSchema[sort])) as any;
59
+ \t\t}
60
+
61
+ \t\tconst data = await query.limit(limit).offset(offset).execute();
62
+ \t\tconst totalResult = await countQuery.execute();
63
+ \t\tconst total = Number(totalResult[0]?.count || 0);
64
+ \t\tconst totalPages = Math.ceil(total / limit);
65
+
66
+ \t\tconst tableHtml = html\`
67
+ \t\t\t<div class="bg-base-100 rounded-xl border border-base-200 overflow-hidden">
68
+ \t\t\t\t<div class="overflow-x-auto">
69
+ \t\t\t\t\t<table class="table table-sm md:table-md w-full">
70
+ \t\t\t\t\t\t<thead>
71
+ \t\t\t\t\t\t\t<tr class="border-b border-base-200">
72
+ \t\t\t\t\t\t\t\t<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>
73
+ \t\t\t\t\t\t\t\t${headers}
74
+ \t\t\t\t\t\t\t\t<th class="w-[100px] text-right">
75
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:dots-horizontal" width="16" height="16" class="opacity-30"></iconify-icon>
76
+ \t\t\t\t\t\t\t\t</th>
77
+ \t\t\t\t\t\t\t</tr>
78
+ \t\t\t\t\t\t</thead>
79
+ \t\t\t\t\t\t<tbody>
80
+ \t\t\t\t\t\t\t\${data.map((row, rowIndex) => html\`
81
+ \t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
82
+ \t\t\t\t\t\t\t\t<td><input type="checkbox" class="checkbox checkbox-xs" /></td>
83
+ \t\t\t\t\t\t\t\t${rowCells}
84
+ \t\t\t\t\t\t\t\t${actionsCell}
85
+ \t\t\t\t\t\t\t</tr>
86
+ \t\t\t\t\t\t\t\`)}
87
+ \t\t\t\t\t\t\t\${data.length === 0 ? html\`
88
+ \t\t\t\t\t\t\t\t<tr>
89
+ \t\t\t\t\t\t\t\t\t<td colspan="${columns.length + 2}" class="text-center py-12">
90
+ \t\t\t\t\t\t\t\t\t\t<div class="flex flex-col items-center gap-3">
91
+ \t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:database-off-outline" width="40" height="40" class="opacity-20"></iconify-icon>
92
+ \t\t\t\t\t\t\t\t\t\t\t<p class="text-sm text-base-content/40">No records found.</p>
93
+ \t\t\t\t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-primary gap-1">
94
+ \t\t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
95
+ \t\t\t\t\t\t\t\t\t\t\t\tNew record
96
+ \t\t\t\t\t\t\t\t\t\t\t</label>
97
+ \t\t\t\t\t\t\t\t\t\t</div>
98
+ \t\t\t\t\t\t\t\t\t</td>
99
+ \t\t\t\t\t\t\t\t</tr>
100
+ \t\t\t\t\t\t\t\` : ''}
101
+ \t\t\t\t\t\t</tbody>
102
+ \t\t\t\t\t</table>
103
+ \t\t\t\t</div>
104
+ \t\t\t\t${paginationHtml}
105
+ \t\t\t</div>
106
+ \t\t\`;
107
+
108
+ \t\tconst content = html\`
109
+ \t\t\t<div id="main-content">
110
+ \t\t\t\t<div class="drawer drawer-end">
111
+ \t\t\t\t\t<input id="create-drawer-${table.exportName}" type="checkbox" class="drawer-toggle" />
112
+ \t\t\t\t\t<div class="drawer-content">
113
+ \t\t\t\t\t\t<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-5 gap-3">
114
+ \t\t\t\t\t\t\t<div class="flex items-center gap-2 text-sm">
115
+ \t\t\t\t\t\t\t\t<a href="/admin" class="text-base-content/40 hover:text-primary transition-colors">Collections</a>
116
+ \t\t\t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="14" height="14" class="opacity-30"></iconify-icon>
117
+ \t\t\t\t\t\t\t\t<span class="font-semibold capitalize">${table.tableName}</span>
118
+ \t\t\t\t\t\t\t\t<button class="btn btn-ghost btn-xs btn-square opacity-40 hover:opacity-100" onclick="window.location.reload()">
119
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:refresh" width="14" height="14"></iconify-icon>
120
+ \t\t\t\t\t\t\t\t</button>
121
+ \t\t\t\t\t\t\t</div>
122
+ \t\t\t\t\t\t\t<div class="flex items-center gap-2 w-full md:w-auto">
123
+ \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-primary btn-sm gap-1">
124
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
125
+ \t\t\t\t\t\t\t\t\tNew record
126
+ \t\t\t\t\t\t\t\t</label>
127
+ \t\t\t\t\t\t\t</div>
128
+ \t\t\t\t\t\t</div>
129
+ \t\t\t\t\t\t<div class="mb-4">
130
+ \t\t\t\t\t\t\t${searchBarHtml}
131
+ \t\t\t\t\t\t</div>
132
+ \t\t\t\t\t\t\${tableHtml}
133
+ \t\t\t\t\t</div>
134
+ \t\t\t\t\t<div class="drawer-side z-50">
135
+ \t\t\t\t\t\t<label for="create-drawer-${table.exportName}" aria-label="close sidebar" class="drawer-overlay"></label>
136
+ \t\t\t\t\t\t<div class="w-full max-w-md min-h-full bg-base-100 p-6 border-l border-base-200 overflow-y-auto shadow-lg">
137
+ \t\t\t\t\t\t\t<div class="flex justify-between items-center mb-5">
138
+ \t\t\t\t\t\t\t\t<h3 class="text-base font-semibold">Create ${table.tableName}</h3>
139
+ \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-ghost btn-square">
140
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:close" width="18" height="18"></iconify-icon>
141
+ \t\t\t\t\t\t\t\t</label>
142
+ \t\t\t\t\t\t\t</div>
143
+ \t\t\t\t\t\t\t<form hx-post="/admin/table/${table.exportName}/create" hx-target="#main-content" hx-swap="outerHTML" class="flex flex-col gap-4">
144
+ \t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
145
+ \t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
146
+ \t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
147
+ \t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
148
+ \t\t\t\t\t\t\t\t${createInputs}
149
+ \t\t\t\t\t\t\t\t<button type="submit" class="btn btn-primary btn-sm mt-2">Create record</button>
150
+ \t\t\t\t\t\t\t</form>
151
+ \t\t\t\t\t\t</div>
152
+ \t\t\t\t\t</div>
153
+ \t\t\t\t</div>
154
+ \t\t\t</div>
155
+ \t\t\`;
156
+
157
+ \t\tif (c.req.header('hx-request')) {
158
+ \t\t\treturn c.html(content);
159
+ \t\t}
160
+
161
+ \t\treturn c.html(Layout({
162
+ \t\t\ttitle: "${table.tableName} - Admin Dashboard",
163
+ \t\t\tchildren: content
164
+ \t\t}));
165
+ \t});`;
166
+ }
@@ -0,0 +1,77 @@
1
+ import { DiscoveredTable } from "../../../types";
2
+ import {
3
+ buildColumnHeaders,
4
+ buildFieldInput,
5
+ buildPayloadAssignments,
6
+ buildRowCells,
7
+ buildSearchConditions,
8
+ } from "../fragments";
9
+ import {
10
+ resolvePrimaryKey,
11
+ shouldIncludeCreateField,
12
+ shouldIncludeEditField,
13
+ } from "../helpers";
14
+ import { buildActionsCell } from "./actions-cell";
15
+ import { buildTableGetRoute } from "./get-route";
16
+ import { buildTablePostRoutes } from "./post-routes";
17
+
18
+ /**
19
+ * Builds the complete set of Hono route handlers for a discovered table.
20
+ * Delegates to focused builder functions for GET and POST handlers.
21
+ */
22
+ export function buildTableRoute(table: DiscoveredTable): string {
23
+ const primaryKey = resolvePrimaryKey(table);
24
+ const hasPrimaryKey = Boolean(primaryKey);
25
+ const columns = table.columns.map((column) => column.name);
26
+ const createColumns = columns.filter((columnName) =>
27
+ shouldIncludeCreateField(table, columnName),
28
+ );
29
+ const editColumns = columns.filter((columnName) =>
30
+ shouldIncludeEditField(table, columnName),
31
+ );
32
+ const searchConditions = buildSearchConditions(table);
33
+ const headers = buildColumnHeaders(table, columns);
34
+ const rowCells = buildRowCells(columns);
35
+ const createInputs = createColumns
36
+ .map((columnName) => buildFieldInput(table, columnName, "create"))
37
+ .join("");
38
+ const editInputs = editColumns
39
+ .map((columnName) => buildFieldInput(table, columnName, "edit"))
40
+ .join("");
41
+ const createAssignments = buildPayloadAssignments(table, createColumns);
42
+ const editAssignments = buildPayloadAssignments(table, editColumns);
43
+ const defaultSort = hasPrimaryKey ? primaryKey : columns[0] || "id";
44
+ const primaryKeyType = table.columns.find(
45
+ (column) => column.name === primaryKey,
46
+ )?.type;
47
+
48
+ const actionsCell = buildActionsCell(
49
+ table,
50
+ hasPrimaryKey,
51
+ primaryKey,
52
+ editInputs,
53
+ );
54
+
55
+ return (
56
+ buildTableGetRoute(
57
+ table,
58
+ defaultSort,
59
+ columns,
60
+ searchConditions,
61
+ headers,
62
+ rowCells,
63
+ actionsCell,
64
+ createInputs,
65
+ ) +
66
+ "\n" +
67
+ buildTablePostRoutes(
68
+ table.exportName,
69
+ defaultSort,
70
+ primaryKey,
71
+ primaryKeyType,
72
+ hasPrimaryKey,
73
+ createAssignments,
74
+ editAssignments,
75
+ )
76
+ );
77
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Builds the POST route handlers code string for create/edit/delete operations
3
+ * on a generic table (/admin/table/:tableName/create|edit|delete).
4
+ */
5
+ export function buildTablePostRoutes(
6
+ exportName: string,
7
+ defaultSort: string,
8
+ primaryKey: string,
9
+ primaryKeyType: string | undefined,
10
+ hasPrimaryKey: boolean,
11
+ createAssignments: string,
12
+ editAssignments: string,
13
+ ): string {
14
+ const numericIdGuard =
15
+ primaryKeyType === "number"
16
+ ? `
17
+ \t\tconst parsedId = Number(rawId);
18
+ \t\tif (Number.isNaN(parsedId)) return c.text('${primaryKey} must be a valid number', 400);
19
+ \t\tidValue = parsedId;
20
+ \t\t`
21
+ : "";
22
+
23
+ const editRoute = hasPrimaryKey
24
+ ? `
25
+ \tadminApp.post('/table/${exportName}/edit', async (c) => {
26
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
27
+ \t\tconst tableSchema = (schema as any).${exportName};
28
+ \t\tif (!tableSchema) return c.text('Table missing', 404);
29
+
30
+ \t\tconst body = await c.req.parseBody();
31
+ \t\tconst getValue = (value: unknown) => (typeof value === 'string' ? value : '');
32
+ \t\tconst rawId = getValue(body['${primaryKey}']);
33
+ \t\tif (rawId === '') return c.text('${primaryKey} is required', 400);
34
+
35
+ \t\tconst payload: Record<string, unknown> = {};
36
+
37
+ \t\t${editAssignments}
38
+
39
+ \t\tlet idValue: unknown = rawId;
40
+ \t\t${numericIdGuard}
41
+
42
+ \t\tif (Object.keys(payload).length > 0) {
43
+ \t\t\tawait db
44
+ \t\t\t\t.update(tableSchema)
45
+ \t\t\t\t.set(payload as any)
46
+ \t\t\t\t.where(eq(tableSchema.${primaryKey}, idValue as any))
47
+ \t\t\t\t.execute();
48
+ \t\t}
49
+
50
+ \t\tconst query = new URLSearchParams({
51
+ \t\t\tpage: getValue(body.page) || '1',
52
+ \t\t\tsort: getValue(body.sort) || '${defaultSort}',
53
+ \t\t\torder: getValue(body.order) || 'desc',
54
+ \t\t\tsearch: getValue(body.search) || '',
55
+ \t\t});
56
+ \t\treturn c.redirect('/admin/table/${exportName}?' + query.toString());
57
+ \t});
58
+
59
+ \tadminApp.post('/table/${exportName}/delete', async (c) => {
60
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
61
+ \t\tconst tableSchema = (schema as any).${exportName};
62
+ \t\tif (!tableSchema) return c.text('Table missing', 404);
63
+
64
+ \t\tconst body = await c.req.parseBody();
65
+ \t\tconst getValue = (value: unknown) => (typeof value === 'string' ? value : '');
66
+ \t\tconst rawId = getValue(body['${primaryKey}']);
67
+ \t\tif (rawId === '') return c.text('${primaryKey} is required', 400);
68
+
69
+ \t\tlet idValue: unknown = rawId;
70
+ \t\t${numericIdGuard}
71
+
72
+ \t\tawait db
73
+ \t\t\t.delete(tableSchema)
74
+ \t\t\t.where(eq(tableSchema.${primaryKey}, idValue as any))
75
+ \t\t\t.execute();
76
+
77
+ \t\tconst query = new URLSearchParams({
78
+ \t\t\tpage: getValue(body.page) || '1',
79
+ \t\t\tsort: getValue(body.sort) || '${defaultSort}',
80
+ \t\t\torder: getValue(body.order) || 'desc',
81
+ \t\t\tsearch: getValue(body.search) || '',
82
+ \t\t});
83
+ \t\treturn c.redirect('/admin/table/${exportName}?' + query.toString());
84
+ \t});`
85
+ : "";
86
+
87
+ return `
88
+ \tadminApp.post('/table/${exportName}/create', async (c) => {
89
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
90
+ \t\tconst tableSchema = (schema as any).${exportName};
91
+ \t\tif (!tableSchema) return c.text('Table missing', 404);
92
+
93
+ \t\tconst body = await c.req.parseBody();
94
+ \t\tconst getValue = (value: unknown) => (typeof value === 'string' ? value : '');
95
+ \t\tconst payload: Record<string, unknown> = {};
96
+
97
+ \t\t${createAssignments}
98
+
99
+ \t\tawait db.insert(tableSchema).values(payload as any).execute();
100
+
101
+ \t\tconst query = new URLSearchParams({
102
+ \t\t\tpage: getValue(body.page) || '1',
103
+ \t\t\tsort: getValue(body.sort) || '${defaultSort}',
104
+ \t\t\torder: getValue(body.order) || 'desc',
105
+ \t\t\tsearch: getValue(body.search) || '',
106
+ \t\t});
107
+ \t\treturn c.redirect('/admin/table/${exportName}?' + query.toString());
108
+ \t});
109
+ ${editRoute}
110
+ \t`;
111
+ }
@@ -0,0 +1,7 @@
1
+ // Re-exported from the modular table/ directory.
2
+ // The actual implementation has been split into focused files:
3
+ // - table/actions-cell.ts — edit drawer + delete dialog cell
4
+ // - table/get-route.ts — GET /admin/table/:tableName handler
5
+ // - table/post-routes.ts — POST create/edit/delete handlers
6
+ // - table/index.ts — orchestrating buildTableRoute()
7
+ export { buildTableRoute } from "./table/index";
@@ -0,0 +1,69 @@
1
+ import { buildUsersTableHtml } from "./html/table";
2
+ import { buildUsersPageHtml } from "./html/page";
3
+
4
+ /**
5
+ * Builds the GET /admin/users route handler (runtime template string).
6
+ * Handles search, sort, pagination, and renders the users page.
7
+ */
8
+ export function buildUsersGetRoute(): string {
9
+ const tableHtml = buildUsersTableHtml();
10
+ const pageHtml = buildUsersPageHtml();
11
+
12
+ return `
13
+ \tadminApp.get('/users', async (c) => {
14
+ \t\tconst db = drizzle(c.env[options.databaseBinding]);
15
+ \t\tconst auth = createAuth({ DATABASE: c.env[options.databaseBinding] } as any, c.req.raw.cf as any);
16
+ \t\tconst session = await auth.api.getSession({ headers: c.req.raw.headers });
17
+ \t\tconst page = parseInt(c.req.query('page') || '1');
18
+ \t\tconst limit = 20;
19
+ \t\tconst offset = (page - 1) * limit;
20
+ \t\tconst sort = c.req.query('sort') || 'createdAt';
21
+ \t\tconst order = c.req.query('order') || 'desc';
22
+ \t\tconst search = c.req.query('search') || '';
23
+ \t\tconst currentUserId = session?.user?.id || '';
24
+
25
+ \t\tlet query = db.select().from(users);
26
+ \t\tlet countQuery = db.select({ count: sql\`count(*)\` }).from(users);
27
+
28
+ \t\tif (search) {
29
+ \t\t\tconst searchConditions = [
30
+ \t\t\t\tlike(users.name, \`%\${search}%\`),
31
+ \t\t\t\tlike(users.email, \`%\${search}%\`),
32
+ \t\t\t\tlike(users.role, \`%\${search}%\`),
33
+ \t\t\t];
34
+ \t\t\tquery = query.where(or(...searchConditions)) as any;
35
+ \t\t\tcountQuery = countQuery.where(or(...searchConditions)) as any;
36
+ \t\t}
37
+
38
+ \t\tconst sortColumns: Record<string, any> = {
39
+ \t\t\tid: users.id,
40
+ \t\t\tname: users.name,
41
+ \t\t\temail: users.email,
42
+ \t\t\trole: users.role,
43
+ \t\t\tcreatedAt: users.createdAt,
44
+ \t\t\tbanned: users.banned,
45
+ \t\t};
46
+
47
+ \t\tif (sortColumns[sort]) {
48
+ \t\t\tquery = query.orderBy(order === 'asc' ? asc(sortColumns[sort]) : desc(sortColumns[sort])) as any;
49
+ \t\t}
50
+
51
+ \t\tconst data = await query.limit(limit).offset(offset).execute();
52
+ \t\tconst totalResult = await countQuery.execute();
53
+ \t\tconst total = Number(totalResult[0]?.count || 0);
54
+ \t\tconst totalPages = Math.ceil(total / limit);
55
+
56
+ \t\t${tableHtml}
57
+
58
+ \t\t${pageHtml}
59
+
60
+ \t\tif (c.req.header('hx-request')) {
61
+ \t\t\treturn c.html(content);
62
+ \t\t}
63
+
64
+ \t\treturn c.html(Layout({
65
+ \t\t\ttitle: "users - Admin Dashboard",
66
+ \t\t\tchildren: content,
67
+ \t\t}));
68
+ \t});`;
69
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Builds the ban/unban confirmation modal HTML for a users table row.
3
+ * Clean PocketBase-style modal.
4
+ */
5
+ export function buildBanUserModal(): string {
6
+ return `
7
+ \t\t\t\t\t\t\t\${(row as any).id === currentUserId ? '' : html\`
8
+ \t\t\t\t\t\t\t\t<input type="checkbox" id="ban-user-modal-\${String((row as any).id)}" class="modal-toggle" />
9
+ \t\t\t\t\t\t\t\t<div class="modal">
10
+ \t\t\t\t\t\t\t\t\t<div class="modal-box max-w-sm">
11
+ \t\t\t\t\t\t\t\t\t\t<h3 class="font-semibold text-base">\${(row as any).banned ? 'Unban user' : 'Ban user'}</h3>
12
+ \t\t\t\t\t\t\t\t\t\t<p class="py-3 text-sm text-base-content/60">\${(row as any).banned ? 'Restore access for' : 'Disable access for'} <span class="font-semibold">\${String((row as any).email ?? '')}</span>?</p>
13
+ \t\t\t\t\t\t\t\t\t\t<form hx-post="/admin/users/\${(row as any).banned ? 'unban' : 'ban'}" hx-target="#main-content" hx-swap="outerHTML" class="space-y-3">
14
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="id" value="\${String((row as any).id)}" />
15
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
16
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
17
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
18
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
19
+ \t\t\t\t\t\t\t\t\t\t\t<div class="modal-action">
20
+ \t\t\t\t\t\t\t\t\t\t\t\t<label for="ban-user-modal-\${String((row as any).id)}" class="btn btn-ghost btn-sm">Cancel</label>
21
+ \t\t\t\t\t\t\t\t\t\t\t\t<button class="btn btn-sm \${(row as any).banned ? 'btn-info' : 'btn-warning'}" type="submit">\${(row as any).banned ? 'Unban' : 'Ban'}</button>
22
+ \t\t\t\t\t\t\t\t\t\t\t</div>
23
+ \t\t\t\t\t\t\t\t\t\t</form>
24
+ \t\t\t\t\t\t\t\t\t</div>
25
+ \t\t\t\t\t\t\t\t\t<label class="modal-backdrop" for="ban-user-modal-\${String((row as any).id)}">Close</label>
26
+ \t\t\t\t\t\t\t\t</div>
27
+ \t\t\t\t\t\t\t\`}`;
28
+ }
29
+
30
+ /**
31
+ * Builds the delete confirmation modal HTML for a users table row.
32
+ * Clean PocketBase-style modal.
33
+ */
34
+ export function buildDeleteUserModal(): string {
35
+ return `
36
+ \t\t\t\t\t\t\t\${(row as any).id === currentUserId ? '' : html\`
37
+ \t\t\t\t\t\t\t\t<input type="checkbox" id="delete-user-modal-\${String((row as any).id)}" class="modal-toggle" />
38
+ \t\t\t\t\t\t\t\t<div class="modal">
39
+ \t\t\t\t\t\t\t\t\t<div class="modal-box max-w-sm">
40
+ \t\t\t\t\t\t\t\t\t\t<h3 class="font-semibold text-base text-error">Delete user</h3>
41
+ \t\t\t\t\t\t\t\t\t\t<p class="py-3 text-sm text-base-content/60">Delete user <span class="font-semibold">\${String((row as any).email ?? '')}</span>? This action cannot be undone.</p>
42
+ \t\t\t\t\t\t\t\t\t\t<form hx-post="/admin/users/delete" hx-target="#main-content" hx-swap="outerHTML">
43
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="id" value="\${String((row as any).id)}" />
44
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
45
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
46
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
47
+ \t\t\t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
48
+ \t\t\t\t\t\t\t\t\t\t\t<div class="modal-action">
49
+ \t\t\t\t\t\t\t\t\t\t\t\t<label for="delete-user-modal-\${String((row as any).id)}" class="btn btn-ghost btn-sm">Cancel</label>
50
+ \t\t\t\t\t\t\t\t\t\t\t\t<button class="btn btn-error btn-sm" type="submit">Delete</button>
51
+ \t\t\t\t\t\t\t\t\t\t\t</div>
52
+ \t\t\t\t\t\t\t\t\t\t</form>
53
+ \t\t\t\t\t\t\t\t\t</div>
54
+ \t\t\t\t\t\t\t\t\t<label class="modal-backdrop" for="delete-user-modal-\${String((row as any).id)}">Close</label>
55
+ \t\t\t\t\t\t\t\t</div>
56
+ \t\t\t\t\t\t\t\`}`;
57
+ }
@@ -0,0 +1,27 @@
1
+ import { buildSearchBarHtml } from "../../common/search-bar";
2
+
3
+ /**
4
+ * Builds the full page content HTML (runtime template string) for the users management page.
5
+ * PocketBase-style breadcrumb header with search filter.
6
+ */
7
+ export function buildUsersPageHtml(): string {
8
+ const searchBarHtml = buildSearchBarHtml("/admin/users", "Search users...");
9
+
10
+ return `
11
+ \t\tconst content = html\`
12
+ \t\t\t<div id="main-content">
13
+ \t\t\t\t<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-5 gap-3">
14
+ \t\t\t\t\t<div class="flex items-center gap-2 text-sm">
15
+ \t\t\t\t\t\t<a href="/admin" class="text-base-content/40 hover:text-primary transition-colors">Collections</a>
16
+ \t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="14" height="14" class="opacity-30"></iconify-icon>
17
+ \t\t\t\t\t\t<span class="font-semibold">users</span>
18
+ \t\t\t\t\t\t<button class="btn btn-ghost btn-xs btn-square opacity-40 hover:opacity-100" onclick="window.location.reload()">
19
+ \t\t\t\t\t\t\t<iconify-icon icon="mdi:refresh" width="14" height="14"></iconify-icon>
20
+ \t\t\t\t\t\t</button>
21
+ \t\t\t\t\t</div>
22
+ \t\t\t\t\t${searchBarHtml}
23
+ \t\t\t\t</div>
24
+ \t\t\t\t\${tableHtml}
25
+ \t\t\t</div>
26
+ \t\t\`;`;
27
+ }