@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.
@@ -0,0 +1,735 @@
1
+ import * as react from 'react';
2
+ import { ReactElement } from 'react';
3
+ import { SQLiteTable } from 'drizzle-orm/sqlite-core';
4
+ import { Db } from '@cfast/db';
5
+ import { FieldConfig } from '@cfast/forms';
6
+ import { Grant } from '@cfast/permissions';
7
+
8
+ /**
9
+ * A user representation for the admin panel, decoupled from `@cfast/auth`.
10
+ *
11
+ * This is the shape the admin expects from your auth adapter. It includes
12
+ * impersonation state so the admin UI can display banners and restore the
13
+ * real admin session.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const adminUser: AdminUser = {
18
+ * id: "usr_123",
19
+ * email: "admin@example.com",
20
+ * name: "Jane Admin",
21
+ * avatarUrl: null,
22
+ * roles: ["admin"],
23
+ * };
24
+ * ```
25
+ */
26
+ type AdminUser = {
27
+ /** Unique user identifier (typically from the `user` table primary key). */
28
+ id: string;
29
+ /** The user's email address, displayed in the admin sidebar and user views. */
30
+ email: string;
31
+ /** Display name shown in the admin header and user management views. */
32
+ name: string;
33
+ /** URL to the user's avatar image, or `null` if none is set. */
34
+ avatarUrl: string | null;
35
+ /** List of role names assigned to this user (e.g., `["admin", "editor"]`). */
36
+ roles: string[];
37
+ /** Whether this session is an impersonation session started by another admin. */
38
+ isImpersonating?: boolean;
39
+ /** The real admin user behind an impersonation session. Present only when {@link isImpersonating} is `true`. */
40
+ realUser?: {
41
+ id: string;
42
+ name: string;
43
+ };
44
+ };
45
+ /**
46
+ * Auth adapter interface that bridges your app's authentication to the admin panel.
47
+ *
48
+ * The admin panel does not depend on `@cfast/auth` directly. Instead, you provide
49
+ * callback functions that the admin calls for authentication, role management,
50
+ * and impersonation. This keeps the admin decoupled from any specific auth library.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const auth: AdminAuthConfig = {
55
+ * requireUser: async (request) => {
56
+ * const session = await getSession(request);
57
+ * return { user: session.user, grants: session.grants };
58
+ * },
59
+ * hasRole: (user, role) => user.roles.includes(role),
60
+ * getRoles: (userId) => authInstance.getRoles(userId),
61
+ * setRole: (userId, role) => authInstance.setRole(userId, role),
62
+ * removeRole: (userId, role) => authInstance.removeRole(userId, role),
63
+ * setRoles: (userId, roles) => authInstance.setRoles(userId, roles),
64
+ * impersonate: (adminId, targetId, request) =>
65
+ * authInstance.impersonate(adminId, targetId, request),
66
+ * stopImpersonation: (request) =>
67
+ * authInstance.stopImpersonation(request),
68
+ * };
69
+ * ```
70
+ */
71
+ type AdminAuthConfig = {
72
+ /** Extracts the authenticated user and their permission grants from the request. Throws or redirects if unauthenticated. */
73
+ requireUser: (request: Request) => Promise<{
74
+ user: AdminUser;
75
+ grants: Grant[];
76
+ }>;
77
+ /** Checks whether the given user has a specific role. Used for the admin access guard. */
78
+ hasRole: (user: AdminUser, role: string) => boolean;
79
+ /** Fetches all roles assigned to a user by ID. Used in user management views. */
80
+ getRoles: (userId: string) => Promise<string[]>;
81
+ /** Assigns a single role to a user. Called from the user detail role management UI. */
82
+ setRole: (userId: string, role: string) => Promise<void>;
83
+ /** Removes a single role from a user. Called from the user detail role management UI. */
84
+ removeRole: (userId: string, role: string) => Promise<void>;
85
+ /** Replaces all roles for a user with the given set. Used for bulk role assignment. */
86
+ setRoles: (userId: string, roles: string[]) => Promise<void>;
87
+ /** Starts an impersonation session. Should return a redirect `Response` that sets the impersonation cookie/session. */
88
+ impersonate?: (adminId: string, targetId: string, request: Request) => Promise<Response>;
89
+ /** Ends the current impersonation session. Should return a redirect `Response` that restores the admin session. */
90
+ stopImpersonation?: (request: Request) => Promise<Response>;
91
+ };
92
+ /**
93
+ * Factory function that creates a permission-scoped {@link Db} instance per request.
94
+ *
95
+ * The admin calls this on every request with the authenticated user's grants
96
+ * and identity so that all CRUD operations respect your permission system.
97
+ *
98
+ * @param grants - The permission grants resolved for the current user.
99
+ * @param user - The authenticated user, or `null` for unauthenticated access.
100
+ * @returns A {@link Db} instance scoped to the user's permissions.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const db: CreateDbFn = (grants, user) =>
105
+ * createDb({ d1: env.DB, schema, grants, user });
106
+ * ```
107
+ */
108
+ type CreateDbFn = (grants: Grant[], user: {
109
+ id: string;
110
+ } | null) => Db;
111
+ /**
112
+ * A custom action that appears on each row in a table's list view.
113
+ *
114
+ * Row actions are invoked with the record's primary key and the form data
115
+ * from the admin action handler.
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const publishAction: RowAction = {
120
+ * label: "Publish",
121
+ * action: async (id, formData) => {
122
+ * await db.update(posts).set({ published: true }).where(eq(posts.id, id));
123
+ * },
124
+ * confirm: "Are you sure you want to publish this post?",
125
+ * variant: "default",
126
+ * };
127
+ * ```
128
+ */
129
+ type RowAction = {
130
+ /** Display label for the action button. */
131
+ label: string;
132
+ /** Async handler called with the record ID and the submitted form data. */
133
+ action: (id: string, formData: FormData) => Promise<unknown>;
134
+ /** Optional confirmation message. When set, the admin shows a confirm dialog before executing. */
135
+ confirm?: string;
136
+ /** Button styling variant. `"danger"` renders a destructive-style button. Defaults to `"default"`. */
137
+ variant?: "danger" | "default";
138
+ };
139
+ /**
140
+ * A bulk action that operates on multiple selected rows in a table's list view.
141
+ *
142
+ * Table actions appear in the bulk action bar when one or more rows are selected.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const exportAction: TableAction = {
147
+ * label: "Export CSV",
148
+ * handler: async (selectedIds) => {
149
+ * await exportToCsv(selectedIds);
150
+ * },
151
+ * };
152
+ * ```
153
+ */
154
+ type TableAction = {
155
+ /** Display label for the bulk action button. */
156
+ label: string;
157
+ /** Async handler called with the array of selected record IDs. */
158
+ handler: (selectedIds: string[]) => Promise<unknown>;
159
+ };
160
+ /**
161
+ * Per-table customization for how a table appears and behaves in the admin panel.
162
+ *
163
+ * Any table not listed in the `tables` config uses sensible defaults derived
164
+ * from schema introspection. Auth-internal tables (session, account, verification,
165
+ * passkey) are auto-excluded unless explicitly configured.
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const postsOverrides: TableOverrides = {
170
+ * label: "Blog Posts",
171
+ * listColumns: ["title", "author", "published", "createdAt"],
172
+ * searchable: ["title", "content"],
173
+ * defaultSort: { column: "createdAt", direction: "desc" },
174
+ * fields: {
175
+ * content: { component: RichTextEditor },
176
+ * },
177
+ * };
178
+ * ```
179
+ */
180
+ type TableOverrides = {
181
+ /** Custom display label for the table in the sidebar and views. Defaults to a pluralized title-case version of the table name. */
182
+ label?: string;
183
+ /** Column names to show in the list view. Defaults to all non-primary-key columns. */
184
+ listColumns?: string[];
185
+ /** Column names that support text search in the list view. Defaults to the first text column. */
186
+ searchable?: string[];
187
+ /** Default sort order for the list view. Defaults to primary key descending. */
188
+ defaultSort?: {
189
+ column: string;
190
+ direction: "asc" | "desc";
191
+ };
192
+ /** Custom field configurations for create/edit forms, keyed by column name. Passed to `@cfast/forms`. */
193
+ fields?: Record<string, FieldConfig>;
194
+ /** Custom row-level and table-level actions. See {@link RowAction} and {@link TableAction}. */
195
+ actions?: {
196
+ row?: RowAction[];
197
+ table?: TableAction[];
198
+ };
199
+ /** Set to `true` to hide this table from the admin panel entirely. */
200
+ exclude?: boolean;
201
+ };
202
+ /**
203
+ * Configuration for the built-in user management views.
204
+ *
205
+ * Controls which roles can be assigned through the admin UI. Role assignment
206
+ * respects your auth adapter's `setRole` and `removeRole` callbacks.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const users: UserManagementConfig = {
211
+ * assignableRoles: ["user", "editor", "moderator", "admin"],
212
+ * };
213
+ * ```
214
+ */
215
+ type UserManagementConfig = {
216
+ /** The list of role names that can be assigned/removed via the admin user management UI. Defaults to an empty array (no role management). */
217
+ assignableRoles?: string[];
218
+ };
219
+ /**
220
+ * A widget definition for the admin dashboard.
221
+ *
222
+ * Widgets are rendered on the admin index page. The `"count"` type shows a
223
+ * stat card with the total number of records. The `"recent"` type shows a
224
+ * list of the most recent records from a table.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const widgets: DashboardWidget[] = [
229
+ * { type: "count", table: "users", label: "Total Users" },
230
+ * { type: "count", table: "posts", label: "Published Posts", where: { published: true } },
231
+ * { type: "recent", table: "posts", label: "Recent Posts", limit: 5 },
232
+ * ];
233
+ * ```
234
+ */
235
+ type DashboardWidget = {
236
+ /** Renders a count stat card for the given table. */
237
+ type: "count";
238
+ /** The Drizzle table name to count records from. */
239
+ table: string;
240
+ /** Display label for the stat card. */
241
+ label: string;
242
+ /** Optional filter conditions applied before counting. */
243
+ where?: Record<string, unknown>;
244
+ } | {
245
+ /** Renders a list of recent records from the given table. */
246
+ type: "recent";
247
+ /** The Drizzle table name to fetch recent records from. */
248
+ table: string;
249
+ /** Display label for the recent items section. */
250
+ label: string;
251
+ /** Maximum number of recent records to show. Defaults to `5`. */
252
+ limit?: number;
253
+ };
254
+ /**
255
+ * Configuration for the admin dashboard (the admin index page).
256
+ *
257
+ * When no widgets are configured, the dashboard shows a count for every
258
+ * visible table and recent items from the first table.
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * const dashboard: DashboardConfig = {
263
+ * widgets: [
264
+ * { type: "count", table: "users", label: "Total Users" },
265
+ * { type: "recent", table: "posts", label: "Recent Posts", limit: 10 },
266
+ * ],
267
+ * };
268
+ * ```
269
+ */
270
+ type DashboardConfig = {
271
+ /** Dashboard widget definitions. When omitted, the admin auto-generates widgets from the schema. */
272
+ widgets?: DashboardWidget[];
273
+ };
274
+ /**
275
+ * Top-level configuration for {@link createAdmin}.
276
+ *
277
+ * Combines the database factory, auth adapter, Drizzle schema, and optional
278
+ * customizations for tables, user management, and the dashboard.
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * const admin = createAdmin({
283
+ * db: (grants, user) => createDb({ d1: env.DB, schema, grants, user }),
284
+ * auth,
285
+ * schema,
286
+ * requiredRole: "admin",
287
+ * tables: { posts: { label: "Blog Posts" } },
288
+ * users: { assignableRoles: ["user", "editor", "admin"] },
289
+ * dashboard: {
290
+ * widgets: [{ type: "count", table: "users", label: "Total Users" }],
291
+ * },
292
+ * });
293
+ * ```
294
+ */
295
+ type AdminConfig = {
296
+ /** Factory function that creates a permission-scoped DB instance per request. See {@link CreateDbFn}. */
297
+ db: CreateDbFn;
298
+ /** Auth adapter that provides user authentication, role management, and impersonation. See {@link AdminAuthConfig}. */
299
+ auth: AdminAuthConfig;
300
+ /** Your Drizzle schema object (e.g., `import * as schema from "~/schema"`). Tables are introspected from this. */
301
+ schema: Record<string, SQLiteTable>;
302
+ /** Per-table display and behavior overrides, keyed by table name. See {@link TableOverrides}. */
303
+ tables?: Record<string, TableOverrides>;
304
+ /** Configuration for the built-in user management views. See {@link UserManagementConfig}. */
305
+ users?: UserManagementConfig;
306
+ /** Configuration for the admin dashboard index page. See {@link DashboardConfig}. */
307
+ dashboard?: DashboardConfig;
308
+ /** Role required to access the admin panel. Defaults to `"admin"`. */
309
+ requiredRole?: string;
310
+ };
311
+ /**
312
+ * Metadata for a single column, produced by {@link introspectSchema}.
313
+ *
314
+ * Contains the information needed to render list columns, form fields,
315
+ * and detail views for a database column.
316
+ */
317
+ type AdminColumnConfig = {
318
+ /** The column name as defined in the Drizzle schema (e.g., `"created_at"`). */
319
+ name: string;
320
+ /** Human-readable label auto-generated from the column name (e.g., `"Created At"`). */
321
+ label: string;
322
+ /** The Drizzle data type (e.g., `"string"`, `"number"`, `"boolean"`). */
323
+ dataType: string;
324
+ /** The Drizzle column type (e.g., `"SQLiteText"`, `"SQLiteInteger"`, `"SQLiteBoolean"`). */
325
+ columnType: string;
326
+ /** Whether this column is required in create/edit forms (non-null and no default). */
327
+ required: boolean;
328
+ /** Whether this column has a default value defined in the schema. */
329
+ hasDefault: boolean;
330
+ /** Whether this column is the table's primary key. */
331
+ isPrimaryKey: boolean;
332
+ /** Allowed enum values, if the column is an enum/text with constrained values. */
333
+ enumValues?: string[];
334
+ /** The name of the foreign table this column references, if it's a foreign key. */
335
+ referencesTable?: string;
336
+ /** The name of the foreign column this column references, if it's a foreign key. */
337
+ referencesColumn?: string;
338
+ };
339
+ /**
340
+ * Complete metadata for a database table, produced by {@link introspectSchema}.
341
+ *
342
+ * Combines introspected column information with user-provided overrides.
343
+ * Used by the admin loader, action, and component to render views.
344
+ */
345
+ type AdminTableMeta = {
346
+ /** The Drizzle table name (e.g., `"posts"`). */
347
+ name: string;
348
+ /** Human-readable label for display in the sidebar and views (e.g., `"Blog Posts"`). */
349
+ label: string;
350
+ /** The original Drizzle table object, used for queries and form generation. */
351
+ drizzleTable: SQLiteTable;
352
+ /** Introspected column metadata for every column in this table. */
353
+ columns: AdminColumnConfig[];
354
+ /** The name of the primary key column (e.g., `"id"`). */
355
+ primaryKey: string;
356
+ /** Column names that support text search in the list view. */
357
+ searchableColumns: string[];
358
+ /** Column names shown in the list view. */
359
+ listColumns: string[];
360
+ /** Default sort order for the list view. */
361
+ defaultSort: {
362
+ column: string;
363
+ direction: "asc" | "desc";
364
+ };
365
+ /** User-provided overrides applied to this table. See {@link TableOverrides}. */
366
+ overrides: TableOverrides;
367
+ };
368
+ /**
369
+ * A single stat card displayed on the admin dashboard.
370
+ *
371
+ * Produced by the admin loader when processing `"count"` type {@link DashboardWidget | dashboard widgets}.
372
+ */
373
+ type DashboardStat = {
374
+ /** Display label for the stat card (e.g., `"Total Users"`). */
375
+ label: string;
376
+ /** The numeric count value. */
377
+ value: number;
378
+ };
379
+ /**
380
+ * A recent-items section displayed on the admin dashboard.
381
+ *
382
+ * Produced by the admin loader when processing `"recent"` type {@link DashboardWidget | dashboard widgets}.
383
+ */
384
+ type RecentItem = {
385
+ /** The table name these items belong to. Used for linking to detail views. */
386
+ table: string;
387
+ /** Display label for the section heading (e.g., `"Recent Posts"`). */
388
+ label: string;
389
+ /** The fetched records, as plain key-value objects. */
390
+ items: Record<string, unknown>[];
391
+ /** Column names to display for each item in the list. */
392
+ columns: string[];
393
+ };
394
+ /**
395
+ * Discriminated union of all data shapes returned by the admin loader.
396
+ *
397
+ * The `view` field determines which admin view to render:
398
+ * - `"dashboard"` -- the admin index page with stats and recent items
399
+ * - `"list"` -- paginated table list with search and sorting
400
+ * - `"detail"` -- single record detail view
401
+ * - `"create"` -- new record form
402
+ * - `"edit"` -- edit existing record form
403
+ * - `"users"` -- user management list
404
+ * - `"user-detail"` -- single user detail with role management
405
+ * - `"error"` -- error message display
406
+ *
407
+ * Every variant includes the current {@link AdminUser} and the sidebar table list.
408
+ */
409
+ type AdminLoaderData = {
410
+ /** Dashboard view identifier. */
411
+ view: "dashboard";
412
+ /** The authenticated admin user. */
413
+ user: AdminUser;
414
+ /** Table list for the sidebar navigation. */
415
+ tables: Array<{
416
+ name: string;
417
+ label: string;
418
+ }>;
419
+ /** Stat cards to display on the dashboard. */
420
+ stats: DashboardStat[];
421
+ /** Recent item sections to display on the dashboard. */
422
+ recentItems: RecentItem[];
423
+ } | {
424
+ /** Table list view identifier. */
425
+ view: "list";
426
+ /** The authenticated admin user. */
427
+ user: AdminUser;
428
+ /** Table list for the sidebar navigation. */
429
+ tables: Array<{
430
+ name: string;
431
+ label: string;
432
+ }>;
433
+ /** The Drizzle table name being listed. */
434
+ tableName: string;
435
+ /** Human-readable table label. */
436
+ tableLabel: string;
437
+ /** The page of records to display. */
438
+ items: Record<string, unknown>[];
439
+ /** Total number of records matching the current search/filter. */
440
+ total: number;
441
+ /** Current page number (1-based). */
442
+ page: number;
443
+ /** Total number of pages. */
444
+ totalPages: number;
445
+ /** Column metadata for rendering the list. */
446
+ columns: AdminColumnConfig[];
447
+ /** Column names that support search. */
448
+ searchable: string[];
449
+ /** Current sort column and direction. */
450
+ sort: {
451
+ column: string;
452
+ direction: "asc" | "desc";
453
+ };
454
+ /** Current search query string. */
455
+ search: string;
456
+ } | {
457
+ /** Record detail view identifier. */
458
+ view: "detail";
459
+ /** The authenticated admin user. */
460
+ user: AdminUser;
461
+ /** Table list for the sidebar navigation. */
462
+ tables: Array<{
463
+ name: string;
464
+ label: string;
465
+ }>;
466
+ /** The Drizzle table name of the record. */
467
+ tableName: string;
468
+ /** Human-readable table label. */
469
+ tableLabel: string;
470
+ /** The record data as a key-value object. */
471
+ item: Record<string, unknown>;
472
+ /** Column metadata for rendering the detail fields. */
473
+ columns: AdminColumnConfig[];
474
+ } | {
475
+ /** Create form view identifier. */
476
+ view: "create";
477
+ /** The authenticated admin user. */
478
+ user: AdminUser;
479
+ /** Table list for the sidebar navigation. */
480
+ tables: Array<{
481
+ name: string;
482
+ label: string;
483
+ }>;
484
+ /** The Drizzle table name for the new record. */
485
+ tableName: string;
486
+ /** Human-readable table label. */
487
+ tableLabel: string;
488
+ /** Column metadata for rendering form fields. */
489
+ columns: AdminColumnConfig[];
490
+ } | {
491
+ /** Edit form view identifier. */
492
+ view: "edit";
493
+ /** The authenticated admin user. */
494
+ user: AdminUser;
495
+ /** Table list for the sidebar navigation. */
496
+ tables: Array<{
497
+ name: string;
498
+ label: string;
499
+ }>;
500
+ /** The Drizzle table name of the record being edited. */
501
+ tableName: string;
502
+ /** Human-readable table label. */
503
+ tableLabel: string;
504
+ /** The existing record data to pre-fill the form. */
505
+ item: Record<string, unknown>;
506
+ /** Column metadata for rendering form fields. */
507
+ columns: AdminColumnConfig[];
508
+ } | {
509
+ /** User list view identifier. */
510
+ view: "users";
511
+ /** The authenticated admin user. */
512
+ user: AdminUser;
513
+ /** Table list for the sidebar navigation. */
514
+ tables: Array<{
515
+ name: string;
516
+ label: string;
517
+ }>;
518
+ /** The page of user records enriched with role data. */
519
+ items: Array<AdminUser & {
520
+ createdAt?: string;
521
+ }>;
522
+ /** Total number of users matching the search. */
523
+ total: number;
524
+ /** Current page number (1-based). */
525
+ page: number;
526
+ /** Total number of pages. */
527
+ totalPages: number;
528
+ /** Current search query string. */
529
+ search: string;
530
+ /** Roles that can be assigned via the admin UI. */
531
+ assignableRoles: string[];
532
+ } | {
533
+ /** User detail view identifier. */
534
+ view: "user-detail";
535
+ /** The authenticated admin user. */
536
+ user: AdminUser;
537
+ /** Table list for the sidebar navigation. */
538
+ tables: Array<{
539
+ name: string;
540
+ label: string;
541
+ }>;
542
+ /** The user being viewed, enriched with role and creation date data. */
543
+ targetUser: AdminUser & {
544
+ createdAt?: string;
545
+ };
546
+ /** Roles that can be assigned via the admin UI. */
547
+ assignableRoles: string[];
548
+ } | {
549
+ /** Error view identifier. */
550
+ view: "error";
551
+ /** The authenticated admin user. */
552
+ user: AdminUser;
553
+ /** Table list for the sidebar navigation. */
554
+ tables: Array<{
555
+ name: string;
556
+ label: string;
557
+ }>;
558
+ /** The error message to display. */
559
+ message: string;
560
+ };
561
+ /**
562
+ * Discriminated union returned by the admin action handler.
563
+ *
564
+ * The admin component uses this to display success messages, error banners,
565
+ * or per-field validation errors on forms.
566
+ *
567
+ * - `{ success: string }` -- operation completed; display a success toast
568
+ * - `{ error: string }` -- operation failed; display an error message
569
+ * - `{ fieldErrors: Record<string, string> }` -- validation failed; highlight individual fields
570
+ */
571
+ type AdminActionResult = {
572
+ success: string;
573
+ } | {
574
+ error: string;
575
+ } | {
576
+ fieldErrors: Record<string, string>;
577
+ };
578
+
579
+ /**
580
+ * Create a complete admin panel from your Drizzle schema.
581
+ *
582
+ * Introspects your schema, applies any {@link TableOverrides}, and produces a
583
+ * `{ loader, action, Component }` triple that you mount on a single React Router
584
+ * route. The admin panel includes list views, detail views, create/edit forms,
585
+ * user management, and a dashboard -- all derived from your schema and permissions.
586
+ *
587
+ * For server/client code splitting, use the individual factories
588
+ * ({@link createAdminLoader}, {@link createAdminAction}, {@link createAdminComponent})
589
+ * instead.
590
+ *
591
+ * @param config - The admin configuration including DB factory, auth adapter, and schema.
592
+ * @returns An object with `loader`, `action`, and `Component` to mount on a React Router route.
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * // app/routes/admin.tsx
597
+ * import { createAdmin } from "@cfast/admin";
598
+ * import * as schema from "~/schema";
599
+ *
600
+ * const admin = createAdmin({
601
+ * db: (grants, user) => createDb({ d1: env.DB, schema, grants, user }),
602
+ * auth,
603
+ * schema,
604
+ * requiredRole: "admin",
605
+ * });
606
+ *
607
+ * export const loader = admin.loader;
608
+ * export const action = admin.action;
609
+ * export default admin.Component;
610
+ * ```
611
+ */
612
+ declare function createAdmin(config: AdminConfig): {
613
+ loader: (request: Request) => Promise<AdminLoaderData>;
614
+ action: (request: Request) => Promise<AdminActionResult | Response>;
615
+ Component: () => react.ReactElement;
616
+ };
617
+
618
+ /**
619
+ * Introspect a Drizzle schema and produce {@link AdminTableMeta} for each visible table.
620
+ *
621
+ * Reads every `SQLiteTable` in the schema, extracts column types, foreign keys,
622
+ * and primary keys, then applies user-provided {@link TableOverrides}. Auth-internal
623
+ * tables (`session`, `account`, `verification`, `passkey`) are auto-excluded unless
624
+ * you explicitly provide overrides for them.
625
+ *
626
+ * Use this directly when you need server/client code splitting (the result is
627
+ * JSON-serializable minus the `drizzleTable` references, which stay on the server).
628
+ *
629
+ * @param schema - Your Drizzle schema object (e.g., `import * as schema from "~/schema"`).
630
+ * @param tableOverrides - Optional per-table display and behavior overrides, keyed by table name.
631
+ * @returns An array of {@link AdminTableMeta} sorted alphabetically by table name.
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * import { introspectSchema } from "@cfast/admin";
636
+ * import * as schema from "~/schema";
637
+ *
638
+ * const tableMetas = introspectSchema(schema, {
639
+ * posts: { label: "Blog Posts", listColumns: ["title", "createdAt"] },
640
+ * });
641
+ * ```
642
+ */
643
+ declare function introspectSchema(schema: Record<string, SQLiteTable>, tableOverrides?: Record<string, TableOverrides>): AdminTableMeta[];
644
+
645
+ /**
646
+ * Create an admin loader function from config and introspected table metadata.
647
+ *
648
+ * The returned loader handles all admin views: dashboard, table list, detail,
649
+ * create, edit, user list, and user detail. It guards access using the
650
+ * {@link AdminAuthConfig.requireUser} and {@link AdminAuthConfig.hasRole} callbacks,
651
+ * creates a permission-scoped DB instance via {@link CreateDbFn}, and parses the
652
+ * URL to determine which view data to fetch.
653
+ *
654
+ * Use this instead of {@link createAdmin} when you need server/client code splitting.
655
+ *
656
+ * @param config - The admin configuration (same object passed to {@link createAdmin}).
657
+ * @param tableMetas - Table metadata from {@link introspectSchema}.
658
+ * @returns An async function that takes a `Request` and returns {@link AdminLoaderData}.
659
+ *
660
+ * @example
661
+ * ```typescript
662
+ * // app/admin.server.ts
663
+ * import { createAdminLoader, introspectSchema } from "@cfast/admin";
664
+ * import * as schema from "~/schema";
665
+ *
666
+ * const tableMetas = introspectSchema(schema);
667
+ * export const adminLoader = createAdminLoader(config, tableMetas);
668
+ * ```
669
+ */
670
+ declare function createAdminLoader(config: AdminConfig, tableMetas: AdminTableMeta[]): (request: Request) => Promise<AdminLoaderData>;
671
+
672
+ /**
673
+ * Create an admin action function from config and introspected table metadata.
674
+ *
675
+ * The returned action handles all admin mutations: record create, update, delete,
676
+ * role assignment/removal, impersonation start/stop, and custom row actions.
677
+ * It reads the `_action` field from the submitted `FormData` to dispatch to the
678
+ * appropriate handler.
679
+ *
680
+ * Like the loader, it guards access using the auth adapter and creates a
681
+ * permission-scoped DB instance per request.
682
+ *
683
+ * Use this instead of {@link createAdmin} when you need server/client code splitting.
684
+ *
685
+ * @param config - The admin configuration (same object passed to {@link createAdmin}).
686
+ * @param tableMetas - Table metadata from {@link introspectSchema}.
687
+ * @returns An async function that takes a `Request` and returns an {@link AdminActionResult} or a `Response` (for impersonation redirects).
688
+ *
689
+ * @example
690
+ * ```typescript
691
+ * // app/admin.server.ts
692
+ * import { createAdminAction, introspectSchema } from "@cfast/admin";
693
+ * import * as schema from "~/schema";
694
+ *
695
+ * const tableMetas = introspectSchema(schema);
696
+ * export const adminAction = createAdminAction(config, tableMetas);
697
+ * ```
698
+ */
699
+ declare function createAdminAction(config: AdminConfig, tableMetas: AdminTableMeta[]): (request: Request) => Promise<AdminActionResult | Response>;
700
+
701
+ /**
702
+ * Create the root admin React component from introspected table metadata.
703
+ *
704
+ * Returns a React component that reads {@link AdminLoaderData} from React Router's
705
+ * `useLoaderData` and {@link AdminActionResult} from `useActionData`, then renders
706
+ * the appropriate admin view (dashboard, list, detail, create, edit, users, or error).
707
+ *
708
+ * The component includes a sidebar, impersonation banner, and wraps everything in
709
+ * a `ConfirmProvider` from `@cfast/ui`.
710
+ *
711
+ * Use this instead of {@link createAdmin} when you need server/client code splitting
712
+ * (this function is safe for client bundles since it only depends on table metadata,
713
+ * not on DB or auth server code).
714
+ *
715
+ * @param tableMetas - Table metadata from {@link introspectSchema}. Used to resolve Drizzle table references for forms and primary key lookups.
716
+ * @returns A React component to use as the default export of your admin route.
717
+ *
718
+ * @example
719
+ * ```typescript
720
+ * // app/routes/admin.tsx
721
+ * import { createAdminComponent, introspectSchema } from "@cfast/admin";
722
+ * import { adminLoader, adminAction } from "~/admin.server";
723
+ * import * as schema from "~/schema";
724
+ *
725
+ * const tableMetas = introspectSchema(schema);
726
+ * const AdminComponent = createAdminComponent(tableMetas);
727
+ *
728
+ * export const loader = adminLoader;
729
+ * export const action = adminAction;
730
+ * export default AdminComponent;
731
+ * ```
732
+ */
733
+ declare function createAdminComponent(tableMetas: AdminTableMeta[]): () => ReactElement;
734
+
735
+ export { type AdminActionResult, type AdminAuthConfig, type AdminColumnConfig, type AdminConfig, type AdminLoaderData, type AdminTableMeta, type AdminUser, type CreateDbFn, type DashboardConfig, type DashboardStat, type DashboardWidget, type RecentItem, type RowAction, type TableAction, type TableOverrides, type UserManagementConfig, createAdmin, createAdminAction, createAdminComponent, createAdminLoader, introspectSchema };