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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { DiscoveredTable } from "../../types";
|
|
2
|
+
|
|
3
|
+
export function buildSearchConditions(table: DiscoveredTable): string {
|
|
4
|
+
return table.columns
|
|
5
|
+
.filter((column) => column.type === "string")
|
|
6
|
+
.map(
|
|
7
|
+
(column) => `
|
|
8
|
+
\t\t\ttry { searchConditions.push(like(tableSchema.${column.name}, \`%\${search}%\`)); } catch (e) {}
|
|
9
|
+
\t\t\t`,
|
|
10
|
+
)
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns an Iconify icon name for a column type.
|
|
16
|
+
*/
|
|
17
|
+
function columnTypeIcon(type: string): string {
|
|
18
|
+
switch (type) {
|
|
19
|
+
case "number":
|
|
20
|
+
return "mdi:pound";
|
|
21
|
+
case "boolean":
|
|
22
|
+
return "mdi:toggle-switch-outline";
|
|
23
|
+
case "date":
|
|
24
|
+
return "mdi:calendar";
|
|
25
|
+
default:
|
|
26
|
+
return "mdi:format-text";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildColumnHeaders(
|
|
31
|
+
table: DiscoveredTable,
|
|
32
|
+
columns: string[],
|
|
33
|
+
): string {
|
|
34
|
+
return columns
|
|
35
|
+
.map((column) => {
|
|
36
|
+
const col = table.columns.find((c) => c.name === column);
|
|
37
|
+
const icon = col ? columnTypeIcon(col.type) : "mdi:format-text";
|
|
38
|
+
return `
|
|
39
|
+
\t\t\t\t\t\t\t<th>
|
|
40
|
+
\t\t\t\t\t\t\t\t<a href="#"
|
|
41
|
+
\t\t\t\t\t\t\t\t hx-get="/admin/table/${table.exportName}?page=\${page}&search=\${search}&sort=${column}&order=\${sort === '${column}' && order === 'asc' ? 'desc' : 'asc'}"
|
|
42
|
+
\t\t\t\t\t\t\t\t hx-target="#main-content"
|
|
43
|
+
\t\t\t\t\t\t\t\t hx-push-url="true"
|
|
44
|
+
\t\t\t\t\t\t\t\t class="hover:text-primary flex items-center gap-1.5 transition-colors whitespace-nowrap">
|
|
45
|
+
\t\t\t\t\t\t\t\t <iconify-icon icon="${icon}" width="14" height="14" class="opacity-40"></iconify-icon>
|
|
46
|
+
\t\t\t\t\t\t\t\t ${column}
|
|
47
|
+
\t\t\t\t\t\t\t\t <span class="text-[10px] opacity-30">\${sort === '${column}' ? (order === 'asc' ? '▲' : '▼') : ''}</span>
|
|
48
|
+
\t\t\t\t\t\t\t\t</a>
|
|
49
|
+
\t\t\t\t\t\t\t</th>
|
|
50
|
+
\t\t\t\t\t\t\t`;
|
|
51
|
+
})
|
|
52
|
+
.join("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildRowCells(columns: string[]): string {
|
|
56
|
+
return columns
|
|
57
|
+
.map(
|
|
58
|
+
(column) =>
|
|
59
|
+
`<td><div class="truncate max-w-[200px] text-sm" title="\${String((row as any).${column} ?? '')}">\${String((row as any).${column} ?? '')}</div></td>`,
|
|
60
|
+
)
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildFieldInput(
|
|
65
|
+
table: DiscoveredTable,
|
|
66
|
+
columnName: string,
|
|
67
|
+
mode: "create" | "edit",
|
|
68
|
+
): string {
|
|
69
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
70
|
+
if (!column) {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const requiredAttr = !column.optional ? " required" : "";
|
|
75
|
+
const inputType =
|
|
76
|
+
column.type === "number"
|
|
77
|
+
? "number"
|
|
78
|
+
: column.type === "date"
|
|
79
|
+
? "date"
|
|
80
|
+
: "text";
|
|
81
|
+
|
|
82
|
+
if (column.type === "boolean") {
|
|
83
|
+
if (mode === "edit") {
|
|
84
|
+
return `
|
|
85
|
+
\t\t<div class="form-control">
|
|
86
|
+
\t\t\t<label class="label cursor-pointer justify-start gap-3">
|
|
87
|
+
\t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" \${(row as any).${columnName} ? 'checked' : ''} />
|
|
88
|
+
\t\t\t\t<span class="label-text text-sm">${columnName}</span>
|
|
89
|
+
\t\t\t</label>
|
|
90
|
+
\t\t</div>
|
|
91
|
+
\t\t`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `
|
|
95
|
+
\t\t<div class="form-control">
|
|
96
|
+
\t\t\t<label class="label cursor-pointer justify-start gap-3">
|
|
97
|
+
\t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" />
|
|
98
|
+
\t\t\t\t<span class="label-text text-sm">${columnName}</span>
|
|
99
|
+
\t\t\t</label>
|
|
100
|
+
\t\t</div>
|
|
101
|
+
\t\t`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (mode === "edit") {
|
|
105
|
+
const valueExpression =
|
|
106
|
+
column.type === "date"
|
|
107
|
+
? `\${(() => {
|
|
108
|
+
const value = (row as any).${columnName};
|
|
109
|
+
if (value == null || value === '') return '';
|
|
110
|
+
|
|
111
|
+
const date =
|
|
112
|
+
value instanceof Date
|
|
113
|
+
? value
|
|
114
|
+
: new Date(typeof value === 'number' ? value : String(value));
|
|
115
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
116
|
+
|
|
117
|
+
const year = date.getFullYear();
|
|
118
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
119
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
120
|
+
return String(year) + '-' + month + '-' + day;
|
|
121
|
+
})()}`
|
|
122
|
+
: `\${String((row as any).${columnName} ?? '')}`;
|
|
123
|
+
|
|
124
|
+
return `
|
|
125
|
+
\t\t<div class="form-control">
|
|
126
|
+
\t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
|
|
127
|
+
\t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm" value="${valueExpression}"${requiredAttr} />
|
|
128
|
+
\t\t</div>
|
|
129
|
+
\t\t`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `
|
|
133
|
+
\t\t<div class="form-control">
|
|
134
|
+
\t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
|
|
135
|
+
\t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm"${requiredAttr} />
|
|
136
|
+
\t\t</div>
|
|
137
|
+
\t\t`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildPayloadAssignments(
|
|
141
|
+
table: DiscoveredTable,
|
|
142
|
+
fields: string[],
|
|
143
|
+
): string {
|
|
144
|
+
return fields
|
|
145
|
+
.map((field) => {
|
|
146
|
+
const column = table.columns.find((item) => item.name === field);
|
|
147
|
+
if (!column) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const requiredCheck = !column.optional
|
|
152
|
+
? `
|
|
153
|
+
\t\tif (raw_${field} === '') {
|
|
154
|
+
\t\t\treturn c.text('${field} is required', 400);
|
|
155
|
+
\t\t}
|
|
156
|
+
\t\t`
|
|
157
|
+
: "";
|
|
158
|
+
|
|
159
|
+
if (column.type === "number") {
|
|
160
|
+
return `
|
|
161
|
+
\t\tconst raw_${field} = getValue(body['${field}']);
|
|
162
|
+
\t\t${requiredCheck}
|
|
163
|
+
\t\tif (raw_${field} !== '') {
|
|
164
|
+
\t\t\tconst parsed_${field} = Number(raw_${field});
|
|
165
|
+
\t\t\tif (Number.isNaN(parsed_${field})) {
|
|
166
|
+
\t\t\t\treturn c.text('${field} must be a valid number', 400);
|
|
167
|
+
\t\t\t}
|
|
168
|
+
\t\t\tpayload.${field} = parsed_${field};
|
|
169
|
+
\t\t}
|
|
170
|
+
\t\t`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (column.type === "boolean") {
|
|
174
|
+
return `
|
|
175
|
+
\t\tconst raw_${field} = getValue(body['${field}']);
|
|
176
|
+
\t\tpayload.${field} = raw_${field} === 'true' || raw_${field} === 'on' || raw_${field} === '1';
|
|
177
|
+
\t\t`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (column.type === "date") {
|
|
181
|
+
return `
|
|
182
|
+
const raw_${field} = getValue(body['${field}']);
|
|
183
|
+
${requiredCheck}
|
|
184
|
+
if (raw_${field} !== '') {
|
|
185
|
+
if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw_${field})) {
|
|
186
|
+
return c.text('${field} must be a valid date (YYYY-MM-DD)', 400);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [year_${field}, month_${field}, day_${field}] = raw_${field}.split('-').map(Number);
|
|
190
|
+
const parsed_${field} = new Date(year_${field}, month_${field} - 1, day_${field});
|
|
191
|
+
if (
|
|
192
|
+
Number.isNaN(parsed_${field}.getTime()) ||
|
|
193
|
+
parsed_${field}.getFullYear() !== year_${field} ||
|
|
194
|
+
parsed_${field}.getMonth() !== month_${field} - 1 ||
|
|
195
|
+
parsed_${field}.getDate() !== day_${field}
|
|
196
|
+
) {
|
|
197
|
+
return c.text('${field} must be a valid date', 400);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
payload.${field} = parsed_${field};
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return `
|
|
206
|
+
\t\tconst raw_${field} = getValue(body['${field}']);
|
|
207
|
+
\t\t${requiredCheck}
|
|
208
|
+
\t\tif (raw_${field} !== '') {
|
|
209
|
+
\t\t\tpayload.${field} = raw_${field};
|
|
210
|
+
\t\t}
|
|
211
|
+
\t\t`;
|
|
212
|
+
})
|
|
213
|
+
.join("\n");
|
|
214
|
+
}
|
|
@@ -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
|
+
}
|