@cfast/admin 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1898 @@
1
+ // src/introspect.ts
2
+ import { getTableConfig } from "drizzle-orm/sqlite-core";
3
+ import { getTableColumns, getTableName } from "drizzle-orm";
4
+ var AUTO_EXCLUDED_TABLES = /* @__PURE__ */ new Set([
5
+ "session",
6
+ "account",
7
+ "verification",
8
+ "passkey"
9
+ ]);
10
+ function tableNameToLabel(name) {
11
+ const words = name.replace(/([a-z])([A-Z])/g, "$1_$2").split("_").filter(Boolean);
12
+ if (words.length === 0) return name;
13
+ const titled = words.map(
14
+ (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
15
+ );
16
+ const lastIndex = titled.length - 1;
17
+ titled[lastIndex] = pluralize(titled[lastIndex]);
18
+ return titled.join(" ");
19
+ }
20
+ function pluralize(word) {
21
+ const lower = word.toLowerCase();
22
+ if (lower.endsWith("es") || lower.endsWith("ies") || lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us")) {
23
+ return word;
24
+ }
25
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z")) {
26
+ return word + "es";
27
+ }
28
+ if (word.endsWith("sh") || word.endsWith("ch")) {
29
+ return word + "es";
30
+ }
31
+ if (word.endsWith("y") && word.length > 1 && !isVowel(word.charAt(word.length - 2))) {
32
+ return word.slice(0, -1) + "ies";
33
+ }
34
+ return word + "s";
35
+ }
36
+ function isVowel(ch) {
37
+ return "aeiouAEIOU".includes(ch);
38
+ }
39
+ function columnNameToLabel(name) {
40
+ const words = name.replace(/([a-z])([A-Z])/g, "$1_$2").split("_").filter(Boolean);
41
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ");
42
+ }
43
+ function introspectSchema(schema, tableOverrides) {
44
+ const result = [];
45
+ for (const [_key, table] of Object.entries(schema)) {
46
+ const tableName = getTableName(table);
47
+ const overrides = tableOverrides?.[tableName] ?? {};
48
+ if (overrides.exclude === true) {
49
+ continue;
50
+ }
51
+ if (AUTO_EXCLUDED_TABLES.has(tableName) && !tableOverrides?.[tableName]) {
52
+ continue;
53
+ }
54
+ const columns = introspectColumns(table);
55
+ if (columns.length === 0) continue;
56
+ const primaryKey = columns.find((c) => c.isPrimaryKey)?.name ?? columns[0].name;
57
+ const searchableColumns = overrides.searchable ?? defaultSearchableColumns(columns);
58
+ const listColumns = overrides.listColumns ?? columns.filter((c) => !c.isPrimaryKey).map((c) => c.name);
59
+ const defaultSort = overrides.defaultSort ?? {
60
+ column: primaryKey,
61
+ direction: "desc"
62
+ };
63
+ const label = overrides.label ?? tableNameToLabel(tableName);
64
+ result.push({
65
+ name: tableName,
66
+ label,
67
+ drizzleTable: table,
68
+ columns,
69
+ primaryKey,
70
+ searchableColumns,
71
+ listColumns,
72
+ defaultSort,
73
+ overrides
74
+ });
75
+ }
76
+ result.sort((a, b) => a.name.localeCompare(b.name));
77
+ return result;
78
+ }
79
+ function introspectColumns(table) {
80
+ const drizzleColumns = getTableColumns(table);
81
+ const tableConfig = getTableConfig(table);
82
+ const foreignKeys = tableConfig.foreignKeys;
83
+ const refMap = /* @__PURE__ */ new Map();
84
+ for (const fk of foreignKeys) {
85
+ const ref = fk.reference();
86
+ const localColumns = ref.columns;
87
+ const foreignColumns = ref.foreignColumns;
88
+ const foreignTable = ref.foreignTable;
89
+ const foreignTableName = getTableName(foreignTable);
90
+ for (let i = 0; i < localColumns.length; i++) {
91
+ const localCol = localColumns[i];
92
+ const foreignCol = foreignColumns[i];
93
+ if (localCol && foreignCol) {
94
+ refMap.set(localCol.name, {
95
+ table: foreignTableName,
96
+ column: foreignCol.name
97
+ });
98
+ }
99
+ }
100
+ }
101
+ const result = [];
102
+ for (const [_fieldName, col] of Object.entries(drizzleColumns)) {
103
+ const ref = refMap.get(col.name);
104
+ const columnConfig = {
105
+ name: col.name,
106
+ label: columnNameToLabel(col.name),
107
+ dataType: col.dataType,
108
+ columnType: col.columnType,
109
+ required: col.notNull && !col.hasDefault,
110
+ hasDefault: col.hasDefault,
111
+ isPrimaryKey: col.primary
112
+ };
113
+ if (col.enumValues && col.enumValues.length > 0) {
114
+ columnConfig.enumValues = [...col.enumValues];
115
+ }
116
+ if (ref) {
117
+ columnConfig.referencesTable = ref.table;
118
+ columnConfig.referencesColumn = ref.column;
119
+ }
120
+ result.push(columnConfig);
121
+ }
122
+ return result;
123
+ }
124
+ function defaultSearchableColumns(columns) {
125
+ const textCol = columns.find((c) => c.dataType === "string");
126
+ return textCol ? [textCol.name] : [];
127
+ }
128
+
129
+ // src/loader.ts
130
+ import { getTableColumns as getTableColumns2, getTableName as getTableName2, eq, like, or, asc, desc } from "drizzle-orm";
131
+
132
+ // src/utils.ts
133
+ var USERS_VIEW = "_users";
134
+ function parseAdminParams(url) {
135
+ const view = url.searchParams.get("view");
136
+ if (!view || view === "dashboard") {
137
+ return { kind: "dashboard" };
138
+ }
139
+ if (view === USERS_VIEW) {
140
+ const id2 = url.searchParams.get("id");
141
+ if (id2) {
142
+ return { kind: "user-detail", id: id2 };
143
+ }
144
+ return {
145
+ kind: "user-list",
146
+ page: parsePageParam(url.searchParams.get("page")),
147
+ search: url.searchParams.get("search") ?? ""
148
+ };
149
+ }
150
+ const table = view;
151
+ const id = url.searchParams.get("id");
152
+ const mode = url.searchParams.get("mode");
153
+ if (mode === "create") {
154
+ return { kind: "create", table };
155
+ }
156
+ if (id && mode === "edit") {
157
+ return { kind: "edit", table, id };
158
+ }
159
+ if (id) {
160
+ return { kind: "detail", table, id };
161
+ }
162
+ const dirParam = url.searchParams.get("dir");
163
+ const direction = dirParam === "asc" ? "asc" : "desc";
164
+ return {
165
+ kind: "list",
166
+ table,
167
+ page: parsePageParam(url.searchParams.get("page")),
168
+ sort: url.searchParams.get("sort"),
169
+ direction,
170
+ search: url.searchParams.get("search") ?? ""
171
+ };
172
+ }
173
+ function buildAdminUrl(params) {
174
+ const sp = new URLSearchParams();
175
+ switch (params.kind) {
176
+ case "dashboard":
177
+ return "";
178
+ case "list":
179
+ sp.set("view", params.table);
180
+ if (params.page > 1) {
181
+ sp.set("page", String(params.page));
182
+ }
183
+ if (params.sort) {
184
+ sp.set("sort", params.sort);
185
+ }
186
+ if (params.direction !== "desc") {
187
+ sp.set("dir", params.direction);
188
+ }
189
+ if (params.search) {
190
+ sp.set("search", params.search);
191
+ }
192
+ break;
193
+ case "detail":
194
+ sp.set("view", params.table);
195
+ sp.set("id", params.id);
196
+ break;
197
+ case "create":
198
+ sp.set("view", params.table);
199
+ sp.set("mode", "create");
200
+ break;
201
+ case "edit":
202
+ sp.set("view", params.table);
203
+ sp.set("id", params.id);
204
+ sp.set("mode", "edit");
205
+ break;
206
+ case "user-list":
207
+ sp.set("view", USERS_VIEW);
208
+ if (params.page > 1) {
209
+ sp.set("page", String(params.page));
210
+ }
211
+ if (params.search) {
212
+ sp.set("search", params.search);
213
+ }
214
+ break;
215
+ case "user-detail":
216
+ sp.set("view", USERS_VIEW);
217
+ sp.set("id", params.id);
218
+ break;
219
+ }
220
+ const str = sp.toString();
221
+ return str ? `?${str}` : "";
222
+ }
223
+ function parsePageParam(value) {
224
+ if (!value) return 1;
225
+ const parsed = parseInt(value, 10);
226
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : 1;
227
+ }
228
+
229
+ // src/loader.ts
230
+ var PAGE_SIZE = 20;
231
+ function asRecords(value) {
232
+ if (!Array.isArray(value)) {
233
+ throw new TypeError(
234
+ `Expected an array of records, got ${typeof value}`
235
+ );
236
+ }
237
+ for (let i = 0; i < value.length; i++) {
238
+ if (typeof value[i] !== "object" || value[i] === null || Array.isArray(value[i])) {
239
+ throw new TypeError(
240
+ `Expected record at index ${i}, got ${Array.isArray(value[i]) ? "array" : typeof value[i]}`
241
+ );
242
+ }
243
+ }
244
+ return value;
245
+ }
246
+ function asRecord(value) {
247
+ if (value === void 0 || value === null) {
248
+ return void 0;
249
+ }
250
+ if (typeof value !== "object" || Array.isArray(value)) {
251
+ throw new TypeError(
252
+ `Expected a record, got ${Array.isArray(value) ? "array" : typeof value}`
253
+ );
254
+ }
255
+ return value;
256
+ }
257
+ function tableList(tableMetas) {
258
+ return tableMetas.filter((t) => t.name !== "users" && t.name !== "user").map((t) => ({ name: t.name, label: t.label }));
259
+ }
260
+ function findTableMeta(tableMetas, name) {
261
+ return tableMetas.find((t) => t.name === name);
262
+ }
263
+ function getColumn(drizzleTable, columnName) {
264
+ const columns = getTableColumns2(drizzleTable);
265
+ return columns[columnName];
266
+ }
267
+ function findUsersTable(schema) {
268
+ for (const table of Object.values(schema)) {
269
+ const name = getTableName2(table);
270
+ if (name === "user" || name === "users") {
271
+ return table;
272
+ }
273
+ }
274
+ return void 0;
275
+ }
276
+ function buildSearchWhere(drizzleTable, searchableColumns, search) {
277
+ if (!search || searchableColumns.length === 0) {
278
+ return void 0;
279
+ }
280
+ const pattern = `%${search}%`;
281
+ const conditions = searchableColumns.map((colName) => {
282
+ const col = getColumn(drizzleTable, colName);
283
+ if (!col) return void 0;
284
+ return like(col, pattern);
285
+ }).filter((c) => c != null);
286
+ if (conditions.length === 0) return void 0;
287
+ if (conditions.length === 1) return conditions[0];
288
+ return or(...conditions);
289
+ }
290
+ function buildOrderBy(drizzleTable, sortColumn, direction) {
291
+ const col = getColumn(drizzleTable, sortColumn);
292
+ if (!col) return void 0;
293
+ return direction === "asc" ? asc(col) : desc(col);
294
+ }
295
+ async function loadDashboard(config, db, tableMetas, user) {
296
+ const tables = tableList(tableMetas);
297
+ const stats = [];
298
+ const recentItems = [];
299
+ if (config.dashboard?.widgets) {
300
+ for (const widget of config.dashboard.widgets) {
301
+ const meta = findTableMeta(tableMetas, widget.table);
302
+ if (!meta) continue;
303
+ if (widget.type === "count") {
304
+ const rows = await db.query(meta.drizzleTable).findMany().run({});
305
+ stats.push({ label: widget.label, value: rows.length });
306
+ } else if (widget.type === "recent") {
307
+ const limit = widget.limit ?? 5;
308
+ const items = asRecords(await db.query(meta.drizzleTable).findMany({
309
+ limit,
310
+ orderBy: buildOrderBy(
311
+ meta.drizzleTable,
312
+ meta.primaryKey,
313
+ "desc"
314
+ )
315
+ }).run({}));
316
+ recentItems.push({
317
+ table: meta.name,
318
+ label: widget.label,
319
+ items,
320
+ columns: meta.listColumns
321
+ });
322
+ }
323
+ }
324
+ } else {
325
+ for (const meta of tableMetas) {
326
+ const rows = await db.query(meta.drizzleTable).findMany().run({});
327
+ stats.push({ label: meta.label, value: rows.length });
328
+ }
329
+ const firstMeta = tableMetas[0];
330
+ if (firstMeta) {
331
+ const items = asRecords(await db.query(firstMeta.drizzleTable).findMany({
332
+ limit: 5,
333
+ orderBy: buildOrderBy(
334
+ firstMeta.drizzleTable,
335
+ firstMeta.primaryKey,
336
+ "desc"
337
+ )
338
+ }).run({}));
339
+ recentItems.push({
340
+ table: firstMeta.name,
341
+ label: firstMeta.label,
342
+ items,
343
+ columns: firstMeta.listColumns
344
+ });
345
+ }
346
+ }
347
+ return {
348
+ view: "dashboard",
349
+ user,
350
+ tables,
351
+ stats,
352
+ recentItems
353
+ };
354
+ }
355
+ async function loadList(db, tableMetas, user, tableName, page, sort, direction, search) {
356
+ const tables = tableList(tableMetas);
357
+ const meta = findTableMeta(tableMetas, tableName);
358
+ if (!meta) {
359
+ return {
360
+ view: "error",
361
+ user,
362
+ tables,
363
+ message: `Table "${tableName}" not found.`
364
+ };
365
+ }
366
+ const sortColumn = sort ?? meta.defaultSort.column;
367
+ const sortDirection = sort ? direction : meta.defaultSort.direction;
368
+ const where = buildSearchWhere(
369
+ meta.drizzleTable,
370
+ meta.searchableColumns,
371
+ search
372
+ );
373
+ const orderBy = buildOrderBy(meta.drizzleTable, sortColumn, sortDirection);
374
+ const allRows = await db.query(meta.drizzleTable).findMany({ where }).run({});
375
+ const total = allRows.length;
376
+ const offset = (page - 1) * PAGE_SIZE;
377
+ const items = asRecords(await db.query(meta.drizzleTable).findMany({
378
+ where,
379
+ orderBy,
380
+ limit: PAGE_SIZE,
381
+ offset
382
+ }).run({}));
383
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
384
+ return {
385
+ view: "list",
386
+ user,
387
+ tables,
388
+ tableName: meta.name,
389
+ tableLabel: meta.label,
390
+ items,
391
+ total,
392
+ page,
393
+ totalPages,
394
+ columns: meta.columns,
395
+ searchable: meta.searchableColumns,
396
+ sort: { column: sortColumn, direction: sortDirection },
397
+ search
398
+ };
399
+ }
400
+ async function loadDetail(db, tableMetas, user, tableName, id) {
401
+ const tables = tableList(tableMetas);
402
+ const meta = findTableMeta(tableMetas, tableName);
403
+ if (!meta) {
404
+ return {
405
+ view: "error",
406
+ user,
407
+ tables,
408
+ message: `Table "${tableName}" not found.`
409
+ };
410
+ }
411
+ const pkCol = getColumn(meta.drizzleTable, meta.primaryKey);
412
+ if (!pkCol) {
413
+ return {
414
+ view: "error",
415
+ user,
416
+ tables,
417
+ message: `Primary key column "${meta.primaryKey}" not found in table "${tableName}".`
418
+ };
419
+ }
420
+ const item = asRecord(await db.query(meta.drizzleTable).findFirst({ where: eq(pkCol, id) }).run({}));
421
+ if (!item) {
422
+ return {
423
+ view: "error",
424
+ user,
425
+ tables,
426
+ message: `Record with id "${id}" not found in table "${tableName}".`
427
+ };
428
+ }
429
+ return {
430
+ view: "detail",
431
+ user,
432
+ tables,
433
+ tableName: meta.name,
434
+ tableLabel: meta.label,
435
+ item,
436
+ columns: meta.columns
437
+ };
438
+ }
439
+ function loadCreate(tableMetas, user, tableName) {
440
+ const tables = tableList(tableMetas);
441
+ const meta = findTableMeta(tableMetas, tableName);
442
+ if (!meta) {
443
+ return {
444
+ view: "error",
445
+ user,
446
+ tables,
447
+ message: `Table "${tableName}" not found.`
448
+ };
449
+ }
450
+ return {
451
+ view: "create",
452
+ user,
453
+ tables,
454
+ tableName: meta.name,
455
+ tableLabel: meta.label,
456
+ columns: meta.columns
457
+ };
458
+ }
459
+ async function loadEdit(db, tableMetas, user, tableName, id) {
460
+ const tables = tableList(tableMetas);
461
+ const meta = findTableMeta(tableMetas, tableName);
462
+ if (!meta) {
463
+ return {
464
+ view: "error",
465
+ user,
466
+ tables,
467
+ message: `Table "${tableName}" not found.`
468
+ };
469
+ }
470
+ const pkCol = getColumn(meta.drizzleTable, meta.primaryKey);
471
+ if (!pkCol) {
472
+ return {
473
+ view: "error",
474
+ user,
475
+ tables,
476
+ message: `Primary key column "${meta.primaryKey}" not found in table "${tableName}".`
477
+ };
478
+ }
479
+ const item = asRecord(await db.query(meta.drizzleTable).findFirst({ where: eq(pkCol, id) }).run({}));
480
+ if (!item) {
481
+ return {
482
+ view: "error",
483
+ user,
484
+ tables,
485
+ message: `Record with id "${id}" not found in table "${tableName}".`
486
+ };
487
+ }
488
+ return {
489
+ view: "edit",
490
+ user,
491
+ tables,
492
+ tableName: meta.name,
493
+ tableLabel: meta.label,
494
+ item,
495
+ columns: meta.columns
496
+ };
497
+ }
498
+ async function loadUserList(config, db, tableMetas, user, page, search) {
499
+ const tables = tableList(tableMetas);
500
+ const usersTable = findUsersTable(config.schema);
501
+ if (!usersTable) {
502
+ return {
503
+ view: "error",
504
+ user,
505
+ tables,
506
+ message: "Users table not found in schema."
507
+ };
508
+ }
509
+ const unsafeDb = db.unsafe();
510
+ const columns = getTableColumns2(usersTable);
511
+ const searchableUserCols = ["name", "email"].filter((c) => columns[c] != null);
512
+ const where = buildSearchWhere(usersTable, searchableUserCols, search);
513
+ const allRows = await unsafeDb.query(usersTable).findMany({ where }).run({});
514
+ const total = allRows.length;
515
+ const offset = (page - 1) * PAGE_SIZE;
516
+ const rawUsers = asRecords(await unsafeDb.query(usersTable).findMany({
517
+ where,
518
+ limit: PAGE_SIZE,
519
+ offset
520
+ }).run({}));
521
+ const items = [];
522
+ for (const raw of rawUsers) {
523
+ const userId = String(raw["id"] ?? "");
524
+ const roles = await config.auth.getRoles(userId);
525
+ items.push({
526
+ id: userId,
527
+ email: String(raw["email"] ?? ""),
528
+ name: String(raw["name"] ?? ""),
529
+ avatarUrl: raw["avatarUrl"] != null ? String(raw["avatarUrl"]) : raw["avatar_url"] != null ? String(raw["avatar_url"]) : null,
530
+ roles,
531
+ createdAt: raw["createdAt"] != null ? String(raw["createdAt"]) : raw["created_at"] != null ? String(raw["created_at"]) : void 0
532
+ });
533
+ }
534
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
535
+ const assignableRoles = config.users?.assignableRoles ?? [];
536
+ return {
537
+ view: "users",
538
+ user,
539
+ tables,
540
+ items,
541
+ total,
542
+ page,
543
+ totalPages,
544
+ search,
545
+ assignableRoles
546
+ };
547
+ }
548
+ async function loadUserDetail(config, db, tableMetas, user, targetId) {
549
+ const tables = tableList(tableMetas);
550
+ const usersTable = findUsersTable(config.schema);
551
+ if (!usersTable) {
552
+ return {
553
+ view: "error",
554
+ user,
555
+ tables,
556
+ message: "Users table not found in schema."
557
+ };
558
+ }
559
+ const unsafeDb = db.unsafe();
560
+ const pkCol = getColumn(usersTable, "id");
561
+ if (!pkCol) {
562
+ return {
563
+ view: "error",
564
+ user,
565
+ tables,
566
+ message: 'Users table has no "id" column.'
567
+ };
568
+ }
569
+ const raw = asRecord(await unsafeDb.query(usersTable).findFirst({ where: eq(pkCol, targetId) }).run({}));
570
+ if (!raw) {
571
+ return {
572
+ view: "error",
573
+ user,
574
+ tables,
575
+ message: `User with id "${targetId}" not found.`
576
+ };
577
+ }
578
+ const roles = await config.auth.getRoles(targetId);
579
+ const assignableRoles = config.users?.assignableRoles ?? [];
580
+ const targetUser = {
581
+ id: String(raw["id"] ?? ""),
582
+ email: String(raw["email"] ?? ""),
583
+ name: String(raw["name"] ?? ""),
584
+ avatarUrl: raw["avatarUrl"] != null ? String(raw["avatarUrl"]) : raw["avatar_url"] != null ? String(raw["avatar_url"]) : null,
585
+ roles,
586
+ createdAt: raw["createdAt"] != null ? String(raw["createdAt"]) : raw["created_at"] != null ? String(raw["created_at"]) : void 0
587
+ };
588
+ return {
589
+ view: "user-detail",
590
+ user,
591
+ tables,
592
+ targetUser,
593
+ assignableRoles
594
+ };
595
+ }
596
+ function createAdminLoader(config, tableMetas) {
597
+ const requiredRole = config.requiredRole ?? "admin";
598
+ return async function adminLoader(request) {
599
+ const { user, grants } = await config.auth.requireUser(request);
600
+ if (!config.auth.hasRole(user, requiredRole)) {
601
+ throw new Response(null, {
602
+ status: 302,
603
+ headers: { Location: "/" }
604
+ });
605
+ }
606
+ const db = config.db(grants, user);
607
+ const url = new URL(request.url);
608
+ const params = parseAdminParams(url);
609
+ switch (params.kind) {
610
+ case "dashboard":
611
+ return loadDashboard(config, db, tableMetas, user);
612
+ case "list":
613
+ return loadList(
614
+ db,
615
+ tableMetas,
616
+ user,
617
+ params.table,
618
+ params.page,
619
+ params.sort,
620
+ params.direction,
621
+ params.search
622
+ );
623
+ case "detail":
624
+ return loadDetail(db, tableMetas, user, params.table, params.id);
625
+ case "create":
626
+ return loadCreate(tableMetas, user, params.table);
627
+ case "edit":
628
+ return loadEdit(db, tableMetas, user, params.table, params.id);
629
+ case "user-list":
630
+ return loadUserList(
631
+ config,
632
+ db,
633
+ tableMetas,
634
+ user,
635
+ params.page,
636
+ params.search
637
+ );
638
+ case "user-detail":
639
+ return loadUserDetail(
640
+ config,
641
+ db,
642
+ tableMetas,
643
+ user,
644
+ params.id
645
+ );
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/action.ts
651
+ import { getTableColumns as getTableColumns3, eq as eq2 } from "drizzle-orm";
652
+ var INTERNAL_FIELDS = /* @__PURE__ */ new Set([
653
+ "_action",
654
+ "_table",
655
+ "_id",
656
+ "_actionName"
657
+ ]);
658
+ function getColumn2(drizzleTable, columnName) {
659
+ const columns = getTableColumns3(drizzleTable);
660
+ return columns[columnName];
661
+ }
662
+ function findTableMeta2(tableMetas, name) {
663
+ return tableMetas.find((t) => t.name === name);
664
+ }
665
+ function extractFormValues(formData, columns) {
666
+ const values = {};
667
+ for (const col of columns) {
668
+ if (col.isPrimaryKey) continue;
669
+ if (!formData.has(col.name)) continue;
670
+ const rawValue = formData.get(col.name);
671
+ if (INTERNAL_FIELDS.has(col.name)) continue;
672
+ if (col.columnType === "SQLiteBoolean") {
673
+ values[col.name] = rawValue === "true" || rawValue === "on" || rawValue === "1";
674
+ continue;
675
+ }
676
+ const strValue = rawValue != null ? String(rawValue) : "";
677
+ if (strValue === "" && !col.required) {
678
+ values[col.name] = null;
679
+ continue;
680
+ }
681
+ if (col.dataType === "number") {
682
+ const num = Number(strValue);
683
+ values[col.name] = Number.isFinite(num) ? num : strValue;
684
+ continue;
685
+ }
686
+ values[col.name] = strValue;
687
+ }
688
+ return values;
689
+ }
690
+ function createAdminAction(config, tableMetas) {
691
+ const requiredRole = config.requiredRole ?? "admin";
692
+ return async function adminAction(request) {
693
+ const { user, grants } = await config.auth.requireUser(request);
694
+ if (!config.auth.hasRole(user, requiredRole)) {
695
+ throw new Response("Forbidden", { status: 403 });
696
+ }
697
+ const db = config.db(grants, user);
698
+ const formData = await request.formData();
699
+ const action = formData.get("_action");
700
+ if (typeof action !== "string" || !action) {
701
+ return { error: "Missing _action field." };
702
+ }
703
+ switch (action) {
704
+ case "create":
705
+ return handleCreate(db, tableMetas, formData);
706
+ case "update":
707
+ return handleUpdate(db, tableMetas, formData);
708
+ case "delete":
709
+ return handleDelete(db, tableMetas, formData);
710
+ case "setRole":
711
+ return handleSetRole(config, formData);
712
+ case "removeRole":
713
+ return handleRemoveRole(config, formData);
714
+ case "impersonate":
715
+ if (!config.auth.impersonate) {
716
+ return { error: "Impersonation is not configured." };
717
+ }
718
+ return handleImpersonate(config, user.id, formData, request);
719
+ case "stopImpersonation":
720
+ if (!config.auth.stopImpersonation) {
721
+ return { error: "Impersonation is not configured." };
722
+ }
723
+ return config.auth.stopImpersonation(request);
724
+ case "custom":
725
+ return handleCustomAction(config, tableMetas, formData);
726
+ default:
727
+ return { error: `Unknown action: "${action}".` };
728
+ }
729
+ };
730
+ }
731
+ async function handleCreate(db, tableMetas, formData) {
732
+ const tableName = formData.get("_table");
733
+ if (typeof tableName !== "string" || !tableName) {
734
+ return { error: "Missing _table field." };
735
+ }
736
+ const meta = findTableMeta2(tableMetas, tableName);
737
+ if (!meta) {
738
+ return { error: `Table "${tableName}" not found.` };
739
+ }
740
+ const values = extractFormValues(formData, meta.columns);
741
+ try {
742
+ await db.insert(meta.drizzleTable).values(values).run({});
743
+ return { success: `Created new ${meta.label} record.` };
744
+ } catch (err) {
745
+ const message = err instanceof Error ? err.message : "Unknown error during create.";
746
+ return { error: message };
747
+ }
748
+ }
749
+ async function handleUpdate(db, tableMetas, formData) {
750
+ const tableName = formData.get("_table");
751
+ if (typeof tableName !== "string" || !tableName) {
752
+ return { error: "Missing _table field." };
753
+ }
754
+ const id = formData.get("_id");
755
+ if (typeof id !== "string" || !id) {
756
+ return { error: "Missing _id field." };
757
+ }
758
+ const meta = findTableMeta2(tableMetas, tableName);
759
+ if (!meta) {
760
+ return { error: `Table "${tableName}" not found.` };
761
+ }
762
+ const pkCol = getColumn2(meta.drizzleTable, meta.primaryKey);
763
+ if (!pkCol) {
764
+ return { error: `Primary key column "${meta.primaryKey}" not found.` };
765
+ }
766
+ const values = extractFormValues(formData, meta.columns);
767
+ try {
768
+ await db.update(meta.drizzleTable).set(values).where(eq2(pkCol, id)).run({});
769
+ return { success: `Updated ${meta.label} record.` };
770
+ } catch (err) {
771
+ const message = err instanceof Error ? err.message : "Unknown error during update.";
772
+ return { error: message };
773
+ }
774
+ }
775
+ async function handleDelete(db, tableMetas, formData) {
776
+ const tableName = formData.get("_table");
777
+ if (typeof tableName !== "string" || !tableName) {
778
+ return { error: "Missing _table field." };
779
+ }
780
+ const id = formData.get("_id");
781
+ if (typeof id !== "string" || !id) {
782
+ return { error: "Missing _id field." };
783
+ }
784
+ const meta = findTableMeta2(tableMetas, tableName);
785
+ if (!meta) {
786
+ return { error: `Table "${tableName}" not found.` };
787
+ }
788
+ const pkCol = getColumn2(meta.drizzleTable, meta.primaryKey);
789
+ if (!pkCol) {
790
+ return { error: `Primary key column "${meta.primaryKey}" not found.` };
791
+ }
792
+ try {
793
+ await db.delete(meta.drizzleTable).where(eq2(pkCol, id)).run({});
794
+ return { success: `Deleted ${meta.label} record.` };
795
+ } catch (err) {
796
+ const message = err instanceof Error ? err.message : "Unknown error during delete.";
797
+ return { error: message };
798
+ }
799
+ }
800
+ async function handleSetRole(config, formData) {
801
+ const userId = formData.get("_id");
802
+ if (typeof userId !== "string" || !userId) {
803
+ return { error: "Missing _id field." };
804
+ }
805
+ const role = formData.get("role");
806
+ if (typeof role !== "string" || !role) {
807
+ return { error: "Missing role field." };
808
+ }
809
+ try {
810
+ await config.auth.setRole(userId, role);
811
+ return { success: `Role "${role}" added to user.` };
812
+ } catch (err) {
813
+ const message = err instanceof Error ? err.message : "Unknown error setting role.";
814
+ return { error: message };
815
+ }
816
+ }
817
+ async function handleRemoveRole(config, formData) {
818
+ const userId = formData.get("_id");
819
+ if (typeof userId !== "string" || !userId) {
820
+ return { error: "Missing _id field." };
821
+ }
822
+ const role = formData.get("role");
823
+ if (typeof role !== "string" || !role) {
824
+ return { error: "Missing role field." };
825
+ }
826
+ try {
827
+ await config.auth.removeRole(userId, role);
828
+ return { success: `Role "${role}" removed from user.` };
829
+ } catch (err) {
830
+ const message = err instanceof Error ? err.message : "Unknown error removing role.";
831
+ return { error: message };
832
+ }
833
+ }
834
+ async function handleImpersonate(config, adminId, formData, request) {
835
+ const targetId = formData.get("_id");
836
+ if (typeof targetId !== "string" || !targetId) {
837
+ throw new Response("Missing _id for impersonation", { status: 400 });
838
+ }
839
+ return config.auth.impersonate(adminId, targetId, request);
840
+ }
841
+ async function handleCustomAction(_config, tableMetas, formData) {
842
+ const tableName = formData.get("_table");
843
+ if (typeof tableName !== "string" || !tableName) {
844
+ return { error: "Missing _table field." };
845
+ }
846
+ const actionName = formData.get("_actionName");
847
+ if (typeof actionName !== "string" || !actionName) {
848
+ return { error: "Missing _actionName field." };
849
+ }
850
+ const id = formData.get("_id");
851
+ if (typeof id !== "string" || !id) {
852
+ return { error: "Missing _id field." };
853
+ }
854
+ const meta = findTableMeta2(tableMetas, tableName);
855
+ if (!meta) {
856
+ return { error: `Table "${tableName}" not found.` };
857
+ }
858
+ const rowActions = meta.overrides.actions?.row;
859
+ if (!rowActions) {
860
+ return { error: `No custom actions defined for table "${tableName}".` };
861
+ }
862
+ const rowAction = rowActions.find((a) => a.label === actionName);
863
+ if (!rowAction) {
864
+ return { error: `Action "${actionName}" not found for table "${tableName}".` };
865
+ }
866
+ try {
867
+ await rowAction.action(id, formData);
868
+ return { success: `Action "${actionName}" completed.` };
869
+ } catch (err) {
870
+ const message = err instanceof Error ? err.message : "Unknown error during custom action.";
871
+ return { error: message };
872
+ }
873
+ }
874
+
875
+ // src/components/admin-root.tsx
876
+ import "react";
877
+ import { useLoaderData, useActionData, useSubmit as useSubmit6 } from "react-router";
878
+ import Box8 from "@mui/joy/Box";
879
+ import Button5 from "@mui/joy/Button";
880
+ import Typography8 from "@mui/joy/Typography";
881
+ import { ConfirmProvider } from "@cfast/ui";
882
+
883
+ // src/components/sidebar.tsx
884
+ import "react";
885
+ import { Link, useSearchParams } from "react-router";
886
+ import Sheet from "@mui/joy/Sheet";
887
+ import List from "@mui/joy/List";
888
+ import ListItem from "@mui/joy/ListItem";
889
+ import ListItemButton from "@mui/joy/ListItemButton";
890
+ import Typography from "@mui/joy/Typography";
891
+ import Divider from "@mui/joy/Divider";
892
+ import { jsx, jsxs } from "react/jsx-runtime";
893
+ function Sidebar({ tables }) {
894
+ const [searchParams] = useSearchParams();
895
+ const currentView = searchParams.get("view");
896
+ return /* @__PURE__ */ jsxs(
897
+ Sheet,
898
+ {
899
+ sx: {
900
+ width: 240,
901
+ minHeight: "100vh",
902
+ borderRight: "1px solid",
903
+ borderColor: "divider",
904
+ p: 2
905
+ },
906
+ children: [
907
+ /* @__PURE__ */ jsx(Typography, { level: "h4", sx: { mb: 2 }, children: "Admin" }),
908
+ /* @__PURE__ */ jsxs(List, { size: "sm", children: [
909
+ /* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
910
+ ListItemButton,
911
+ {
912
+ selected: currentView === null,
913
+ component: Link,
914
+ to: buildAdminUrl({ kind: "dashboard" }) || ".",
915
+ children: "Dashboard"
916
+ }
917
+ ) }),
918
+ /* @__PURE__ */ jsx(Divider, { sx: { my: 1 } }),
919
+ tables.map((table) => /* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
920
+ ListItemButton,
921
+ {
922
+ selected: currentView === table.name,
923
+ component: Link,
924
+ to: buildAdminUrl({
925
+ kind: "list",
926
+ table: table.name,
927
+ page: 1,
928
+ sort: null,
929
+ direction: "desc",
930
+ search: ""
931
+ }),
932
+ children: table.label
933
+ }
934
+ ) }, table.name)),
935
+ /* @__PURE__ */ jsx(Divider, { sx: { my: 1 } }),
936
+ /* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
937
+ ListItemButton,
938
+ {
939
+ selected: currentView === "_users",
940
+ component: Link,
941
+ to: buildAdminUrl({ kind: "user-list", page: 1, search: "" }),
942
+ children: "Users"
943
+ }
944
+ ) })
945
+ ] })
946
+ ]
947
+ }
948
+ );
949
+ }
950
+
951
+ // src/components/dashboard.tsx
952
+ import "react";
953
+ import { Link as Link2 } from "react-router";
954
+ import Box from "@mui/joy/Box";
955
+ import Card from "@mui/joy/Card";
956
+ import Typography2 from "@mui/joy/Typography";
957
+ import Table from "@mui/joy/Table";
958
+ import Sheet2 from "@mui/joy/Sheet";
959
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
960
+ function Dashboard({ stats, recentItems }) {
961
+ return /* @__PURE__ */ jsxs2(Box, { children: [
962
+ /* @__PURE__ */ jsx2(Typography2, { level: "h2", sx: { mb: 3 }, children: "Dashboard" }),
963
+ stats.length > 0 && /* @__PURE__ */ jsx2(
964
+ Box,
965
+ {
966
+ sx: {
967
+ display: "grid",
968
+ gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
969
+ gap: 2,
970
+ mb: 4
971
+ },
972
+ children: stats.map((stat) => /* @__PURE__ */ jsxs2(Card, { variant: "outlined", children: [
973
+ /* @__PURE__ */ jsx2(Typography2, { level: "body-sm", textColor: "text.secondary", children: stat.label }),
974
+ /* @__PURE__ */ jsx2(Typography2, { level: "h3", children: stat.value })
975
+ ] }, stat.label))
976
+ }
977
+ ),
978
+ recentItems.map((section) => /* @__PURE__ */ jsxs2(Box, { sx: { mb: 4 }, children: [
979
+ /* @__PURE__ */ jsxs2(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }, children: [
980
+ /* @__PURE__ */ jsx2(Typography2, { level: "title-lg", children: section.label }),
981
+ /* @__PURE__ */ jsx2(
982
+ Typography2,
983
+ {
984
+ component: Link2,
985
+ to: buildAdminUrl({
986
+ kind: "list",
987
+ table: section.table,
988
+ page: 1,
989
+ sort: null,
990
+ direction: "desc",
991
+ search: ""
992
+ }),
993
+ level: "body-sm",
994
+ children: "View all"
995
+ }
996
+ )
997
+ ] }),
998
+ /* @__PURE__ */ jsx2(Sheet2, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs2(Table, { hoverRow: true, children: [
999
+ /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { children: section.columns.map((col) => /* @__PURE__ */ jsx2("th", { children: col }, col)) }) }),
1000
+ /* @__PURE__ */ jsxs2("tbody", { children: [
1001
+ section.items.map((item, idx) => /* @__PURE__ */ jsx2("tr", { children: section.columns.map((col) => /* @__PURE__ */ jsx2("td", { children: formatCellValue(item[col]) }, col)) }, idx)),
1002
+ section.items.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: section.columns.length, children: /* @__PURE__ */ jsx2(Typography2, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No items yet." }) }) })
1003
+ ] })
1004
+ ] }) })
1005
+ ] }, section.table))
1006
+ ] });
1007
+ }
1008
+ function formatCellValue(value) {
1009
+ if (value === null || value === void 0) return "-";
1010
+ if (typeof value === "boolean") return value ? "Yes" : "No";
1011
+ return String(value);
1012
+ }
1013
+
1014
+ // src/components/table-list.tsx
1015
+ import { useState } from "react";
1016
+ import { Link as Link3, useSubmit, useNavigate } from "react-router";
1017
+ import Box3 from "@mui/joy/Box";
1018
+ import Typography3 from "@mui/joy/Typography";
1019
+ import Button from "@mui/joy/Button";
1020
+ import Input from "@mui/joy/Input";
1021
+ import Table2 from "@mui/joy/Table";
1022
+ import Sheet3 from "@mui/joy/Sheet";
1023
+ import Stack from "@mui/joy/Stack";
1024
+ import { useConfirm } from "@cfast/ui";
1025
+
1026
+ // src/components/action-result.tsx
1027
+ import "react";
1028
+ import Box2 from "@mui/joy/Box";
1029
+ import Chip from "@mui/joy/Chip";
1030
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1031
+ function ActionResultDisplay({
1032
+ result
1033
+ }) {
1034
+ if (!result) return null;
1035
+ if ("success" in result) {
1036
+ return /* @__PURE__ */ jsx3(Chip, { color: "success", variant: "soft", sx: { mb: 2 }, children: result.success });
1037
+ }
1038
+ if ("error" in result) {
1039
+ return /* @__PURE__ */ jsx3(Chip, { color: "danger", variant: "soft", sx: { mb: 2 }, children: result.error });
1040
+ }
1041
+ if ("fieldErrors" in result) {
1042
+ return /* @__PURE__ */ jsx3(Box2, { sx: { mb: 2 }, children: Object.entries(result.fieldErrors).map(([field, error]) => /* @__PURE__ */ jsxs3(Chip, { color: "danger", variant: "soft", sx: { mr: 1, mb: 1 }, children: [
1043
+ field,
1044
+ ": ",
1045
+ error
1046
+ ] }, field)) });
1047
+ }
1048
+ return null;
1049
+ }
1050
+
1051
+ // src/components/table-list.tsx
1052
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1053
+ function TableList({
1054
+ tableName,
1055
+ tableLabel,
1056
+ items,
1057
+ total,
1058
+ page,
1059
+ totalPages,
1060
+ columns,
1061
+ searchable,
1062
+ sort,
1063
+ search,
1064
+ primaryKey,
1065
+ actionResult
1066
+ }) {
1067
+ const submit = useSubmit();
1068
+ const navigate = useNavigate();
1069
+ const confirm = useConfirm();
1070
+ const [searchValue, setSearchValue] = useState(search);
1071
+ const listColumns = columns.filter((c) => !c.isPrimaryKey);
1072
+ function sortUrl(columnName) {
1073
+ const isSameColumn = sort.column === columnName;
1074
+ const newDirection = isSameColumn && sort.direction === "asc" ? "desc" : "asc";
1075
+ return buildAdminUrl({
1076
+ kind: "list",
1077
+ table: tableName,
1078
+ page: 1,
1079
+ sort: columnName,
1080
+ direction: newDirection,
1081
+ search
1082
+ });
1083
+ }
1084
+ function handleSearchSubmit(e) {
1085
+ e.preventDefault();
1086
+ const url = buildAdminUrl({
1087
+ kind: "list",
1088
+ table: tableName,
1089
+ page: 1,
1090
+ sort: sort.column,
1091
+ direction: sort.direction,
1092
+ search: searchValue
1093
+ });
1094
+ navigate({ search: url.startsWith("?") ? url : "" });
1095
+ }
1096
+ async function handleDelete2(id) {
1097
+ const confirmed = await confirm({
1098
+ title: "Delete record",
1099
+ description: `Are you sure you want to delete this ${tableLabel} record?`,
1100
+ variant: "danger",
1101
+ confirmLabel: "Delete"
1102
+ });
1103
+ if (!confirmed) return;
1104
+ const formData = new FormData();
1105
+ formData.set("_action", "delete");
1106
+ formData.set("_table", tableName);
1107
+ formData.set("_id", id);
1108
+ submit(formData, { method: "post" });
1109
+ }
1110
+ return /* @__PURE__ */ jsxs4(Box3, { children: [
1111
+ /* @__PURE__ */ jsxs4(Box3, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }, children: [
1112
+ /* @__PURE__ */ jsx4(Typography3, { level: "h2", children: tableLabel }),
1113
+ /* @__PURE__ */ jsx4(
1114
+ Button,
1115
+ {
1116
+ component: Link3,
1117
+ to: buildAdminUrl({ kind: "create", table: tableName }),
1118
+ children: "Create"
1119
+ }
1120
+ )
1121
+ ] }),
1122
+ /* @__PURE__ */ jsx4(ActionResultDisplay, { result: actionResult }),
1123
+ searchable.length > 0 && /* @__PURE__ */ jsx4(Box3, { component: "form", onSubmit: handleSearchSubmit, sx: { mb: 2 }, children: /* @__PURE__ */ jsx4(
1124
+ Input,
1125
+ {
1126
+ placeholder: `Search ${tableLabel.toLowerCase()}...`,
1127
+ value: searchValue,
1128
+ onChange: (e) => setSearchValue(e.target.value),
1129
+ endDecorator: /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "soft", size: "sm", children: "Search" })
1130
+ }
1131
+ ) }),
1132
+ /* @__PURE__ */ jsx4(Sheet3, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs4(Table2, { hoverRow: true, children: [
1133
+ /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1134
+ listColumns.map((col) => /* @__PURE__ */ jsx4("th", { children: /* @__PURE__ */ jsxs4(
1135
+ Typography3,
1136
+ {
1137
+ component: Link3,
1138
+ to: sortUrl(col.name),
1139
+ level: "body-sm",
1140
+ fontWeight: "lg",
1141
+ sx: { textDecoration: "none", color: "inherit" },
1142
+ children: [
1143
+ col.label,
1144
+ sort.column === col.name ? sort.direction === "asc" ? " \u2191" : " \u2193" : ""
1145
+ ]
1146
+ }
1147
+ ) }, col.name)),
1148
+ /* @__PURE__ */ jsx4("th", { style: { width: 120 }, children: "Actions" })
1149
+ ] }) }),
1150
+ /* @__PURE__ */ jsxs4("tbody", { children: [
1151
+ items.map((item) => {
1152
+ const id = String(item[primaryKey] ?? "");
1153
+ return /* @__PURE__ */ jsxs4("tr", { children: [
1154
+ listColumns.map((col) => /* @__PURE__ */ jsx4("td", { children: formatCellValue2(item[col.name]) }, col.name)),
1155
+ /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsxs4(Stack, { direction: "row", spacing: 1, children: [
1156
+ /* @__PURE__ */ jsx4(
1157
+ Button,
1158
+ {
1159
+ component: Link3,
1160
+ to: buildAdminUrl({ kind: "detail", table: tableName, id }),
1161
+ size: "sm",
1162
+ variant: "plain",
1163
+ children: "View"
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsx4(
1167
+ Button,
1168
+ {
1169
+ size: "sm",
1170
+ variant: "plain",
1171
+ color: "danger",
1172
+ onClick: () => {
1173
+ void handleDelete2(id);
1174
+ },
1175
+ children: "Delete"
1176
+ }
1177
+ )
1178
+ ] }) })
1179
+ ] }, id);
1180
+ }),
1181
+ items.length === 0 && /* @__PURE__ */ jsx4("tr", { children: /* @__PURE__ */ jsx4("td", { colSpan: listColumns.length + 1, children: /* @__PURE__ */ jsx4(Typography3, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No records found." }) }) })
1182
+ ] })
1183
+ ] }) }),
1184
+ totalPages > 1 && /* @__PURE__ */ jsxs4(Stack, { direction: "row", spacing: 1, justifyContent: "center", alignItems: "center", sx: { mt: 2 }, children: [
1185
+ page > 1 && /* @__PURE__ */ jsx4(
1186
+ Button,
1187
+ {
1188
+ component: Link3,
1189
+ to: buildAdminUrl({
1190
+ kind: "list",
1191
+ table: tableName,
1192
+ page: page - 1,
1193
+ sort: sort.column,
1194
+ direction: sort.direction,
1195
+ search
1196
+ }),
1197
+ size: "sm",
1198
+ variant: "outlined",
1199
+ children: "Previous"
1200
+ }
1201
+ ),
1202
+ /* @__PURE__ */ jsxs4(Typography3, { level: "body-sm", children: [
1203
+ "Page ",
1204
+ page,
1205
+ " of ",
1206
+ totalPages,
1207
+ " (",
1208
+ total,
1209
+ " total)"
1210
+ ] }),
1211
+ page < totalPages && /* @__PURE__ */ jsx4(
1212
+ Button,
1213
+ {
1214
+ component: Link3,
1215
+ to: buildAdminUrl({
1216
+ kind: "list",
1217
+ table: tableName,
1218
+ page: page + 1,
1219
+ sort: sort.column,
1220
+ direction: sort.direction,
1221
+ search
1222
+ }),
1223
+ size: "sm",
1224
+ variant: "outlined",
1225
+ children: "Next"
1226
+ }
1227
+ )
1228
+ ] })
1229
+ ] });
1230
+ }
1231
+ function formatCellValue2(value) {
1232
+ if (value === null || value === void 0) return "-";
1233
+ if (typeof value === "boolean") return value ? "Yes" : "No";
1234
+ return String(value);
1235
+ }
1236
+
1237
+ // src/components/table-detail.tsx
1238
+ import "react";
1239
+ import { Link as Link4, useSubmit as useSubmit2 } from "react-router";
1240
+ import Box4 from "@mui/joy/Box";
1241
+ import Typography4 from "@mui/joy/Typography";
1242
+ import Button2 from "@mui/joy/Button";
1243
+ import Card2 from "@mui/joy/Card";
1244
+ import Stack2 from "@mui/joy/Stack";
1245
+ import { useConfirm as useConfirm2 } from "@cfast/ui";
1246
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1247
+ function TableDetail({
1248
+ tableName,
1249
+ tableLabel,
1250
+ item,
1251
+ columns,
1252
+ primaryKey
1253
+ }) {
1254
+ const submit = useSubmit2();
1255
+ const confirm = useConfirm2();
1256
+ const id = String(item[primaryKey] ?? "");
1257
+ async function handleDelete2() {
1258
+ const confirmed = await confirm({
1259
+ title: "Delete record",
1260
+ description: `Are you sure you want to delete this ${tableLabel} record?`,
1261
+ variant: "danger",
1262
+ confirmLabel: "Delete"
1263
+ });
1264
+ if (!confirmed) return;
1265
+ const formData = new FormData();
1266
+ formData.set("_action", "delete");
1267
+ formData.set("_table", tableName);
1268
+ formData.set("_id", id);
1269
+ submit(formData, { method: "post" });
1270
+ }
1271
+ return /* @__PURE__ */ jsxs5(Box4, { children: [
1272
+ /* @__PURE__ */ jsxs5(Stack2, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
1273
+ /* @__PURE__ */ jsx5(
1274
+ Typography4,
1275
+ {
1276
+ component: Link4,
1277
+ to: buildAdminUrl({
1278
+ kind: "list",
1279
+ table: tableName,
1280
+ page: 1,
1281
+ sort: null,
1282
+ direction: "desc",
1283
+ search: ""
1284
+ }),
1285
+ level: "body-sm",
1286
+ children: tableLabel
1287
+ }
1288
+ ),
1289
+ /* @__PURE__ */ jsx5(Typography4, { level: "body-sm", children: "/" }),
1290
+ /* @__PURE__ */ jsx5(Typography4, { level: "body-sm", children: id })
1291
+ ] }),
1292
+ /* @__PURE__ */ jsxs5(Box4, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 3 }, children: [
1293
+ /* @__PURE__ */ jsxs5(Typography4, { level: "h2", children: [
1294
+ tableLabel,
1295
+ " Detail"
1296
+ ] }),
1297
+ /* @__PURE__ */ jsxs5(Stack2, { direction: "row", spacing: 1, children: [
1298
+ /* @__PURE__ */ jsx5(
1299
+ Button2,
1300
+ {
1301
+ component: Link4,
1302
+ to: buildAdminUrl({ kind: "edit", table: tableName, id }),
1303
+ variant: "outlined",
1304
+ children: "Edit"
1305
+ }
1306
+ ),
1307
+ /* @__PURE__ */ jsx5(
1308
+ Button2,
1309
+ {
1310
+ variant: "outlined",
1311
+ color: "danger",
1312
+ onClick: () => {
1313
+ void handleDelete2();
1314
+ },
1315
+ children: "Delete"
1316
+ }
1317
+ )
1318
+ ] })
1319
+ ] }),
1320
+ /* @__PURE__ */ jsx5(Card2, { variant: "outlined", children: /* @__PURE__ */ jsx5(
1321
+ Box4,
1322
+ {
1323
+ sx: {
1324
+ display: "grid",
1325
+ gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
1326
+ gap: 2
1327
+ },
1328
+ children: columns.map((col) => /* @__PURE__ */ jsxs5(Box4, { children: [
1329
+ /* @__PURE__ */ jsx5(Typography4, { level: "body-xs", textColor: "text.secondary", fontWeight: "lg", children: col.label }),
1330
+ /* @__PURE__ */ jsx5(Typography4, { level: "body-md", children: formatFieldValue(item[col.name]) })
1331
+ ] }, col.name))
1332
+ }
1333
+ ) })
1334
+ ] });
1335
+ }
1336
+ function formatFieldValue(value) {
1337
+ if (value === null || value === void 0) return "-";
1338
+ if (typeof value === "boolean") return value ? "Yes" : "No";
1339
+ if (value instanceof Date) return value.toLocaleString();
1340
+ return String(value);
1341
+ }
1342
+
1343
+ // src/components/table-form.tsx
1344
+ import "react";
1345
+ import { Link as Link5, useSubmit as useSubmit3 } from "react-router";
1346
+ import Box5 from "@mui/joy/Box";
1347
+ import Typography5 from "@mui/joy/Typography";
1348
+ import Stack3 from "@mui/joy/Stack";
1349
+ import { AutoForm } from "@cfast/forms/joy";
1350
+ import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1351
+ var AUTO_TIMESTAMP_NAMES = /* @__PURE__ */ new Set([
1352
+ "created_at",
1353
+ "createdAt",
1354
+ "updated_at",
1355
+ "updatedAt"
1356
+ ]);
1357
+ function TableForm({
1358
+ tableName,
1359
+ tableLabel,
1360
+ mode,
1361
+ drizzleTable,
1362
+ item,
1363
+ columns,
1364
+ primaryKey,
1365
+ actionResult
1366
+ }) {
1367
+ const submit = useSubmit3();
1368
+ const id = item ? String(item[primaryKey] ?? "") : void 0;
1369
+ const excludeFields = columns.filter((c) => c.isPrimaryKey || AUTO_TIMESTAMP_NAMES.has(c.name)).map((c) => c.name);
1370
+ function handleSubmit(values) {
1371
+ const formData = new FormData();
1372
+ formData.set("_action", mode === "create" ? "create" : "update");
1373
+ formData.set("_table", tableName);
1374
+ if (id) {
1375
+ formData.set("_id", id);
1376
+ }
1377
+ for (const [key, value] of Object.entries(values)) {
1378
+ if (value !== null && value !== void 0) {
1379
+ formData.set(key, String(value));
1380
+ }
1381
+ }
1382
+ submit(formData, { method: "post" });
1383
+ }
1384
+ const listUrl = buildAdminUrl({
1385
+ kind: "list",
1386
+ table: tableName,
1387
+ page: 1,
1388
+ sort: null,
1389
+ direction: "desc",
1390
+ search: ""
1391
+ });
1392
+ return /* @__PURE__ */ jsxs6(Box5, { children: [
1393
+ /* @__PURE__ */ jsxs6(Stack3, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
1394
+ /* @__PURE__ */ jsx6(Typography5, { component: Link5, to: listUrl, level: "body-sm", children: tableLabel }),
1395
+ /* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "/" }),
1396
+ mode === "edit" && id ? /* @__PURE__ */ jsxs6(Fragment, { children: [
1397
+ /* @__PURE__ */ jsx6(
1398
+ Typography5,
1399
+ {
1400
+ component: Link5,
1401
+ to: buildAdminUrl({ kind: "detail", table: tableName, id }),
1402
+ level: "body-sm",
1403
+ children: id
1404
+ }
1405
+ ),
1406
+ /* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "/" }),
1407
+ /* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "Edit" })
1408
+ ] }) : /* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "Create" })
1409
+ ] }),
1410
+ /* @__PURE__ */ jsx6(Typography5, { level: "h2", sx: { mb: 3 }, children: mode === "create" ? `Create ${tableLabel}` : `Edit ${tableLabel}` }),
1411
+ /* @__PURE__ */ jsx6(ActionResultDisplay, { result: actionResult }),
1412
+ /* @__PURE__ */ jsx6(
1413
+ AutoForm,
1414
+ {
1415
+ table: drizzleTable,
1416
+ mode,
1417
+ data: item,
1418
+ onSubmit: handleSubmit,
1419
+ exclude: excludeFields
1420
+ }
1421
+ )
1422
+ ] });
1423
+ }
1424
+
1425
+ // src/components/user-list.tsx
1426
+ import { useState as useState2 } from "react";
1427
+ import { Link as Link6, useSubmit as useSubmit4, useNavigate as useNavigate2 } from "react-router";
1428
+ import Box6 from "@mui/joy/Box";
1429
+ import Typography6 from "@mui/joy/Typography";
1430
+ import Button3 from "@mui/joy/Button";
1431
+ import Input2 from "@mui/joy/Input";
1432
+ import Table3 from "@mui/joy/Table";
1433
+ import Sheet4 from "@mui/joy/Sheet";
1434
+ import Stack4 from "@mui/joy/Stack";
1435
+ import { RoleBadge } from "@cfast/ui/joy";
1436
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1437
+ function UserList({
1438
+ items,
1439
+ total,
1440
+ page,
1441
+ totalPages,
1442
+ search,
1443
+ currentUser,
1444
+ actionResult
1445
+ }) {
1446
+ const submit = useSubmit4();
1447
+ const navigate = useNavigate2();
1448
+ const [searchValue, setSearchValue] = useState2(search);
1449
+ function handleSearchSubmit(e) {
1450
+ e.preventDefault();
1451
+ const url = buildAdminUrl({
1452
+ kind: "user-list",
1453
+ page: 1,
1454
+ search: searchValue
1455
+ });
1456
+ navigate({ search: url.startsWith("?") ? url : "" });
1457
+ }
1458
+ function handleImpersonate2(userId) {
1459
+ const formData = new FormData();
1460
+ formData.set("_action", "impersonate");
1461
+ formData.set("_id", userId);
1462
+ submit(formData, { method: "post" });
1463
+ }
1464
+ return /* @__PURE__ */ jsxs7(Box6, { children: [
1465
+ /* @__PURE__ */ jsx7(Typography6, { level: "h2", sx: { mb: 2 }, children: "Users" }),
1466
+ /* @__PURE__ */ jsx7(ActionResultDisplay, { result: actionResult }),
1467
+ /* @__PURE__ */ jsx7(Box6, { component: "form", onSubmit: handleSearchSubmit, sx: { mb: 2 }, children: /* @__PURE__ */ jsx7(
1468
+ Input2,
1469
+ {
1470
+ placeholder: "Search users...",
1471
+ value: searchValue,
1472
+ onChange: (e) => setSearchValue(e.target.value),
1473
+ endDecorator: /* @__PURE__ */ jsx7(Button3, { type: "submit", variant: "soft", size: "sm", children: "Search" })
1474
+ }
1475
+ ) }),
1476
+ /* @__PURE__ */ jsx7(Sheet4, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs7(Table3, { hoverRow: true, children: [
1477
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
1478
+ /* @__PURE__ */ jsx7("th", { children: "Name" }),
1479
+ /* @__PURE__ */ jsx7("th", { children: "Email" }),
1480
+ /* @__PURE__ */ jsx7("th", { children: "Roles" }),
1481
+ /* @__PURE__ */ jsx7("th", { children: "Joined" }),
1482
+ /* @__PURE__ */ jsx7("th", { style: { width: 160 }, children: "Actions" })
1483
+ ] }) }),
1484
+ /* @__PURE__ */ jsxs7("tbody", { children: [
1485
+ items.map((user) => /* @__PURE__ */ jsxs7("tr", { children: [
1486
+ /* @__PURE__ */ jsx7("td", { children: user.name }),
1487
+ /* @__PURE__ */ jsx7("td", { children: user.email }),
1488
+ /* @__PURE__ */ jsx7("td", { children: /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 0.5, flexWrap: "wrap", children: [
1489
+ user.roles.map((role) => /* @__PURE__ */ jsx7(RoleBadge, { role }, role)),
1490
+ user.roles.length === 0 && /* @__PURE__ */ jsx7(Typography6, { level: "body-xs", textColor: "text.tertiary", children: "No roles" })
1491
+ ] }) }),
1492
+ /* @__PURE__ */ jsx7("td", { children: user.createdAt ?? "-" }),
1493
+ /* @__PURE__ */ jsx7("td", { children: /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 1, children: [
1494
+ /* @__PURE__ */ jsx7(
1495
+ Button3,
1496
+ {
1497
+ component: Link6,
1498
+ to: buildAdminUrl({ kind: "user-detail", id: user.id }),
1499
+ size: "sm",
1500
+ variant: "plain",
1501
+ children: "View"
1502
+ }
1503
+ ),
1504
+ user.id !== currentUser.id && /* @__PURE__ */ jsx7(
1505
+ Button3,
1506
+ {
1507
+ size: "sm",
1508
+ variant: "plain",
1509
+ color: "warning",
1510
+ onClick: () => handleImpersonate2(user.id),
1511
+ children: "Impersonate"
1512
+ }
1513
+ )
1514
+ ] }) })
1515
+ ] }, user.id)),
1516
+ items.length === 0 && /* @__PURE__ */ jsx7("tr", { children: /* @__PURE__ */ jsx7("td", { colSpan: 5, children: /* @__PURE__ */ jsx7(Typography6, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No users found." }) }) })
1517
+ ] })
1518
+ ] }) }),
1519
+ totalPages > 1 && /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 1, justifyContent: "center", alignItems: "center", sx: { mt: 2 }, children: [
1520
+ page > 1 && /* @__PURE__ */ jsx7(
1521
+ Button3,
1522
+ {
1523
+ component: Link6,
1524
+ to: buildAdminUrl({
1525
+ kind: "user-list",
1526
+ page: page - 1,
1527
+ search
1528
+ }),
1529
+ size: "sm",
1530
+ variant: "outlined",
1531
+ children: "Previous"
1532
+ }
1533
+ ),
1534
+ /* @__PURE__ */ jsxs7(Typography6, { level: "body-sm", children: [
1535
+ "Page ",
1536
+ page,
1537
+ " of ",
1538
+ totalPages,
1539
+ " (",
1540
+ total,
1541
+ " total)"
1542
+ ] }),
1543
+ page < totalPages && /* @__PURE__ */ jsx7(
1544
+ Button3,
1545
+ {
1546
+ component: Link6,
1547
+ to: buildAdminUrl({
1548
+ kind: "user-list",
1549
+ page: page + 1,
1550
+ search
1551
+ }),
1552
+ size: "sm",
1553
+ variant: "outlined",
1554
+ children: "Next"
1555
+ }
1556
+ )
1557
+ ] })
1558
+ ] });
1559
+ }
1560
+
1561
+ // src/components/user-detail.tsx
1562
+ import { useState as useState3 } from "react";
1563
+ import { Link as Link7, useSubmit as useSubmit5 } from "react-router";
1564
+ import Box7 from "@mui/joy/Box";
1565
+ import Typography7 from "@mui/joy/Typography";
1566
+ import Button4 from "@mui/joy/Button";
1567
+ import Card3 from "@mui/joy/Card";
1568
+ import Stack5 from "@mui/joy/Stack";
1569
+ import Chip2 from "@mui/joy/Chip";
1570
+ import Select from "@mui/joy/Select";
1571
+ import Option from "@mui/joy/Option";
1572
+ import Divider2 from "@mui/joy/Divider";
1573
+ import { RoleBadge as RoleBadge2, AvatarWithInitials } from "@cfast/ui/joy";
1574
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1575
+ function UserDetail({
1576
+ targetUser,
1577
+ assignableRoles,
1578
+ currentUser,
1579
+ actionResult
1580
+ }) {
1581
+ const submit = useSubmit5();
1582
+ const [selectedRole, setSelectedRole] = useState3(null);
1583
+ const availableRoles = assignableRoles.filter(
1584
+ (role) => !targetUser.roles.includes(role)
1585
+ );
1586
+ function handleAddRole() {
1587
+ if (!selectedRole) return;
1588
+ const formData = new FormData();
1589
+ formData.set("_action", "setRole");
1590
+ formData.set("_id", targetUser.id);
1591
+ formData.set("role", selectedRole);
1592
+ submit(formData, { method: "post" });
1593
+ setSelectedRole(null);
1594
+ }
1595
+ function handleRemoveRole2(role) {
1596
+ const formData = new FormData();
1597
+ formData.set("_action", "removeRole");
1598
+ formData.set("_id", targetUser.id);
1599
+ formData.set("role", role);
1600
+ submit(formData, { method: "post" });
1601
+ }
1602
+ function handleImpersonate2() {
1603
+ const formData = new FormData();
1604
+ formData.set("_action", "impersonate");
1605
+ formData.set("_id", targetUser.id);
1606
+ submit(formData, { method: "post" });
1607
+ }
1608
+ return /* @__PURE__ */ jsxs8(Box7, { children: [
1609
+ /* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
1610
+ /* @__PURE__ */ jsx8(
1611
+ Typography7,
1612
+ {
1613
+ component: Link7,
1614
+ to: buildAdminUrl({ kind: "user-list", page: 1, search: "" }),
1615
+ level: "body-sm",
1616
+ children: "Users"
1617
+ }
1618
+ ),
1619
+ /* @__PURE__ */ jsx8(Typography7, { level: "body-sm", children: "/" }),
1620
+ /* @__PURE__ */ jsx8(Typography7, { level: "body-sm", children: targetUser.name })
1621
+ ] }),
1622
+ /* @__PURE__ */ jsx8(ActionResultDisplay, { result: actionResult }),
1623
+ /* @__PURE__ */ jsx8(Card3, { variant: "outlined", sx: { mb: 3 }, children: /* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 2, alignItems: "center", children: [
1624
+ /* @__PURE__ */ jsx8(
1625
+ AvatarWithInitials,
1626
+ {
1627
+ src: targetUser.avatarUrl,
1628
+ name: targetUser.name,
1629
+ size: "lg"
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ jsxs8(Box7, { children: [
1633
+ /* @__PURE__ */ jsx8(Typography7, { level: "h3", children: targetUser.name }),
1634
+ /* @__PURE__ */ jsx8(Typography7, { level: "body-md", textColor: "text.secondary", children: targetUser.email }),
1635
+ targetUser.createdAt && /* @__PURE__ */ jsxs8(Typography7, { level: "body-xs", textColor: "text.tertiary", children: [
1636
+ "Joined ",
1637
+ targetUser.createdAt
1638
+ ] })
1639
+ ] }),
1640
+ targetUser.id !== currentUser.id && /* @__PURE__ */ jsx8(Box7, { sx: { ml: "auto" }, children: /* @__PURE__ */ jsx8(
1641
+ Button4,
1642
+ {
1643
+ variant: "outlined",
1644
+ color: "warning",
1645
+ onClick: handleImpersonate2,
1646
+ children: "Impersonate"
1647
+ }
1648
+ ) })
1649
+ ] }) }),
1650
+ /* @__PURE__ */ jsxs8(Card3, { variant: "outlined", children: [
1651
+ /* @__PURE__ */ jsx8(Typography7, { level: "title-lg", sx: { mb: 2 }, children: "Roles" }),
1652
+ /* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, flexWrap: "wrap", sx: { mb: 2 }, children: [
1653
+ targetUser.roles.map((role) => /* @__PURE__ */ jsx8(
1654
+ Chip2,
1655
+ {
1656
+ variant: "soft",
1657
+ endDecorator: /* @__PURE__ */ jsx8(
1658
+ Button4,
1659
+ {
1660
+ size: "sm",
1661
+ variant: "plain",
1662
+ color: "danger",
1663
+ sx: { minWidth: 0, px: 0.5 },
1664
+ onClick: () => handleRemoveRole2(role),
1665
+ children: "x"
1666
+ }
1667
+ ),
1668
+ children: /* @__PURE__ */ jsx8(RoleBadge2, { role })
1669
+ },
1670
+ role
1671
+ )),
1672
+ targetUser.roles.length === 0 && /* @__PURE__ */ jsx8(Typography7, { level: "body-sm", textColor: "text.tertiary", children: "No roles assigned." })
1673
+ ] }),
1674
+ availableRoles.length > 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
1675
+ /* @__PURE__ */ jsx8(Divider2, { sx: { my: 2 } }),
1676
+ /* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, alignItems: "center", children: [
1677
+ /* @__PURE__ */ jsx8(
1678
+ Select,
1679
+ {
1680
+ placeholder: "Select role...",
1681
+ value: selectedRole,
1682
+ onChange: (_e, value) => setSelectedRole(value),
1683
+ sx: { minWidth: 200 },
1684
+ children: availableRoles.map((role) => /* @__PURE__ */ jsx8(Option, { value: role, children: role }, role))
1685
+ }
1686
+ ),
1687
+ /* @__PURE__ */ jsx8(
1688
+ Button4,
1689
+ {
1690
+ onClick: handleAddRole,
1691
+ disabled: !selectedRole,
1692
+ size: "sm",
1693
+ children: "Add Role"
1694
+ }
1695
+ )
1696
+ ] })
1697
+ ] })
1698
+ ] })
1699
+ ] });
1700
+ }
1701
+
1702
+ // src/components/admin-root.tsx
1703
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1704
+ function createAdminComponent(tableMetas) {
1705
+ const tableMetaMap = new Map(tableMetas.map((m) => [m.name, m]));
1706
+ function AdminRoot() {
1707
+ const data = useLoaderData();
1708
+ const actionData = useActionData();
1709
+ return /* @__PURE__ */ jsxs9(ConfirmProvider, { children: [
1710
+ data.user.isImpersonating && /* @__PURE__ */ jsx9(ImpersonationBar, { user: data.user }),
1711
+ /* @__PURE__ */ jsxs9(Box8, { sx: { display: "flex", minHeight: "100vh" }, children: [
1712
+ /* @__PURE__ */ jsx9(Sidebar, { tables: data.tables }),
1713
+ /* @__PURE__ */ jsx9(Box8, { sx: { flex: 1, p: 3, overflow: "auto" }, children: /* @__PURE__ */ jsx9(
1714
+ AdminContent,
1715
+ {
1716
+ data,
1717
+ actionResult: actionData,
1718
+ tableMetaMap
1719
+ }
1720
+ ) })
1721
+ ] })
1722
+ ] });
1723
+ }
1724
+ return AdminRoot;
1725
+ }
1726
+ function AdminContent({ data, actionResult, tableMetaMap }) {
1727
+ switch (data.view) {
1728
+ case "dashboard":
1729
+ return /* @__PURE__ */ jsx9(Dashboard, { stats: data.stats, recentItems: data.recentItems });
1730
+ case "list": {
1731
+ const meta = tableMetaMap.get(data.tableName);
1732
+ const primaryKey = meta?.primaryKey ?? "id";
1733
+ return /* @__PURE__ */ jsx9(
1734
+ TableList,
1735
+ {
1736
+ tableName: data.tableName,
1737
+ tableLabel: data.tableLabel,
1738
+ items: data.items,
1739
+ total: data.total,
1740
+ page: data.page,
1741
+ totalPages: data.totalPages,
1742
+ columns: data.columns,
1743
+ searchable: data.searchable,
1744
+ sort: data.sort,
1745
+ search: data.search,
1746
+ primaryKey,
1747
+ actionResult
1748
+ }
1749
+ );
1750
+ }
1751
+ case "detail": {
1752
+ const meta = tableMetaMap.get(data.tableName);
1753
+ const primaryKey = meta?.primaryKey ?? "id";
1754
+ return /* @__PURE__ */ jsx9(
1755
+ TableDetail,
1756
+ {
1757
+ tableName: data.tableName,
1758
+ tableLabel: data.tableLabel,
1759
+ item: data.item,
1760
+ columns: data.columns,
1761
+ primaryKey
1762
+ }
1763
+ );
1764
+ }
1765
+ case "create": {
1766
+ const meta = tableMetaMap.get(data.tableName);
1767
+ if (!meta) {
1768
+ return /* @__PURE__ */ jsx9(Box8, { sx: { p: 4 }, children: /* @__PURE__ */ jsxs9(Typography8, { color: "danger", children: [
1769
+ "Table \u201C",
1770
+ data.tableName,
1771
+ "\u201D not found in schema."
1772
+ ] }) });
1773
+ }
1774
+ return /* @__PURE__ */ jsx9(
1775
+ TableForm,
1776
+ {
1777
+ tableName: data.tableName,
1778
+ tableLabel: data.tableLabel,
1779
+ mode: "create",
1780
+ drizzleTable: meta.drizzleTable,
1781
+ columns: data.columns,
1782
+ primaryKey: meta.primaryKey,
1783
+ actionResult
1784
+ }
1785
+ );
1786
+ }
1787
+ case "edit": {
1788
+ const meta = tableMetaMap.get(data.tableName);
1789
+ if (!meta) {
1790
+ return /* @__PURE__ */ jsx9(Box8, { sx: { p: 4 }, children: /* @__PURE__ */ jsxs9(Typography8, { color: "danger", children: [
1791
+ "Table \u201C",
1792
+ data.tableName,
1793
+ "\u201D not found in schema."
1794
+ ] }) });
1795
+ }
1796
+ return /* @__PURE__ */ jsx9(
1797
+ TableForm,
1798
+ {
1799
+ tableName: data.tableName,
1800
+ tableLabel: data.tableLabel,
1801
+ mode: "edit",
1802
+ drizzleTable: meta.drizzleTable,
1803
+ item: data.item,
1804
+ columns: data.columns,
1805
+ primaryKey: meta.primaryKey,
1806
+ actionResult
1807
+ }
1808
+ );
1809
+ }
1810
+ case "users":
1811
+ return /* @__PURE__ */ jsx9(
1812
+ UserList,
1813
+ {
1814
+ items: data.items,
1815
+ total: data.total,
1816
+ page: data.page,
1817
+ totalPages: data.totalPages,
1818
+ search: data.search,
1819
+ currentUser: data.user,
1820
+ actionResult
1821
+ }
1822
+ );
1823
+ case "user-detail":
1824
+ return /* @__PURE__ */ jsx9(
1825
+ UserDetail,
1826
+ {
1827
+ targetUser: data.targetUser,
1828
+ assignableRoles: data.assignableRoles,
1829
+ currentUser: data.user,
1830
+ actionResult
1831
+ }
1832
+ );
1833
+ case "error":
1834
+ return /* @__PURE__ */ jsxs9(Box8, { sx: { p: 4, textAlign: "center" }, children: [
1835
+ /* @__PURE__ */ jsx9(Typography8, { level: "h3", color: "danger", children: "Error" }),
1836
+ /* @__PURE__ */ jsx9(Typography8, { level: "body-md", sx: { mt: 1 }, children: data.message })
1837
+ ] });
1838
+ }
1839
+ }
1840
+ function ImpersonationBar({ user }) {
1841
+ const submit = useSubmit6();
1842
+ function handleStopImpersonation() {
1843
+ const formData = new FormData();
1844
+ formData.set("_action", "stopImpersonation");
1845
+ submit(formData, { method: "post" });
1846
+ }
1847
+ return /* @__PURE__ */ jsxs9(
1848
+ Box8,
1849
+ {
1850
+ sx: {
1851
+ bgcolor: "warning.softBg",
1852
+ color: "warning.softColor",
1853
+ py: 1,
1854
+ px: 3,
1855
+ display: "flex",
1856
+ alignItems: "center",
1857
+ justifyContent: "center",
1858
+ gap: 2
1859
+ },
1860
+ children: [
1861
+ /* @__PURE__ */ jsxs9(Typography8, { level: "body-sm", fontWeight: "lg", children: [
1862
+ "Impersonating ",
1863
+ user.name,
1864
+ " (",
1865
+ user.email,
1866
+ ")",
1867
+ user.realUser && ` \u2014 logged in as ${user.realUser.name}`
1868
+ ] }),
1869
+ /* @__PURE__ */ jsx9(
1870
+ Button5,
1871
+ {
1872
+ size: "sm",
1873
+ variant: "solid",
1874
+ color: "warning",
1875
+ onClick: handleStopImpersonation,
1876
+ children: "Stop Impersonation"
1877
+ }
1878
+ )
1879
+ ]
1880
+ }
1881
+ );
1882
+ }
1883
+
1884
+ // src/create-admin.ts
1885
+ function createAdmin(config) {
1886
+ const tableMetas = introspectSchema(config.schema, config.tables);
1887
+ const loader = createAdminLoader(config, tableMetas);
1888
+ const action = createAdminAction(config, tableMetas);
1889
+ const Component = createAdminComponent(tableMetas);
1890
+ return { loader, action, Component };
1891
+ }
1892
+ export {
1893
+ createAdmin,
1894
+ createAdminAction,
1895
+ createAdminComponent,
1896
+ createAdminLoader,
1897
+ introspectSchema
1898
+ };