sveltekit-admin 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +80 -2
  2. package/dist/index.d.ts +2 -7
  3. package/dist/index.js +2 -13
  4. package/dist/server/auth.d.ts +7 -0
  5. package/dist/server/auth.js +19 -0
  6. package/dist/server/data.d.ts +38 -0
  7. package/dist/server/data.js +116 -0
  8. package/dist/server/handler.d.ts +123 -4
  9. package/dist/server/handler.js +590 -819
  10. package/dist/server/introspection/parser.d.ts +19 -12
  11. package/dist/server/introspection/parser.js +71 -61
  12. package/dist/server/introspection/relations.d.ts +49 -0
  13. package/dist/server/introspection/relations.js +128 -0
  14. package/dist/server/query/filterDetection.d.ts +71 -0
  15. package/dist/server/query/filterDetection.js +153 -0
  16. package/dist/server/query/listQuery.d.ts +89 -0
  17. package/dist/server/query/listQuery.js +428 -0
  18. package/dist/server/query/urls.d.ts +33 -0
  19. package/dist/server/query/urls.js +58 -0
  20. package/dist/server/router.d.ts +6 -0
  21. package/dist/server/router.js +27 -0
  22. package/dist/server/views/Dashboard.svelte +29 -0
  23. package/dist/server/views/Dashboard.svelte.d.ts +15 -0
  24. package/dist/server/views/FieldInput.svelte +63 -0
  25. package/dist/server/views/FieldInput.svelte.d.ts +9 -0
  26. package/dist/server/views/Form.svelte +127 -0
  27. package/dist/server/views/Form.svelte.d.ts +12 -0
  28. package/dist/server/views/Layout.svelte +78 -0
  29. package/dist/server/views/Layout.svelte.d.ts +13 -0
  30. package/dist/server/views/List.svelte +240 -0
  31. package/dist/server/views/List.svelte.d.ts +27 -0
  32. package/dist/server/views/ListFilters.svelte +257 -0
  33. package/dist/server/views/ListFilters.svelte.d.ts +18 -0
  34. package/dist/server/views/ModelCard.svelte +11 -0
  35. package/dist/server/views/ModelCard.svelte.d.ts +8 -0
  36. package/dist/server/views/NotFound.svelte +7 -0
  37. package/dist/server/views/NotFound.svelte.d.ts +7 -0
  38. package/dist/server/views/RelatedBlock.svelte +74 -0
  39. package/dist/server/views/RelatedBlock.svelte.d.ts +11 -0
  40. package/dist/server/views/RelationCheckboxes.svelte +53 -0
  41. package/dist/server/views/RelationCheckboxes.svelte.d.ts +9 -0
  42. package/dist/server/views/RelationSelect.svelte +53 -0
  43. package/dist/server/views/RelationSelect.svelte.d.ts +12 -0
  44. package/dist/server/views/StatCard.svelte +18 -0
  45. package/dist/server/views/StatCard.svelte.d.ts +8 -0
  46. package/dist/server/views/html.d.ts +5 -0
  47. package/dist/server/views/html.js +41 -0
  48. package/dist/server/views/theme.d.ts +1 -0
  49. package/dist/server/views/theme.js +484 -0
  50. package/dist/server/views/types.d.ts +57 -0
  51. package/dist/server/views/types.js +1 -0
  52. package/package.json +24 -26
  53. package/dist/admin.d.ts +0 -227
  54. package/dist/admin.js +0 -369
  55. package/dist/components/AdminForm.svelte +0 -423
  56. package/dist/components/AdminForm.svelte.d.ts +0 -30
  57. package/dist/components/AdminLayout.svelte +0 -328
  58. package/dist/components/AdminLayout.svelte.d.ts +0 -20
  59. package/dist/components/DataTable.svelte +0 -573
  60. package/dist/components/DataTable.svelte.d.ts +0 -25
  61. package/dist/components/index.d.ts +0 -3
  62. package/dist/components/index.js +0 -3
  63. package/dist/server/auth/guard.d.ts +0 -36
  64. package/dist/server/auth/guard.js +0 -38
  65. package/dist/server/auth/index.d.ts +0 -1
  66. package/dist/server/auth/index.js +0 -1
  67. package/dist/server/crud/index.d.ts +0 -1
  68. package/dist/server/crud/index.js +0 -1
  69. package/dist/server/crud/operations.d.ts +0 -87
  70. package/dist/server/crud/operations.js +0 -276
  71. package/dist/server/introspection/index.d.ts +0 -1
  72. package/dist/server/introspection/index.js +0 -1
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  ![Version](https://img.shields.io/npm/v/sveltekit-admin)
6
6
  ![License](https://img.shields.io/npm/l/sveltekit-admin)
7
7
 
8
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes and breaking changes.
9
+
8
10
  ## Features
9
11
 
10
12
  - 🔍 **Auto-introspection** of Prisma schema
@@ -75,7 +77,6 @@ createAdminHandler({
75
77
  // Custom branding
76
78
  branding: {
77
79
  title: 'My Admin',
78
- logo: '/logo.svg',
79
80
  primaryColor: '#6366f1'
80
81
  }
81
82
  });
@@ -106,6 +107,31 @@ const adminHandle = createAdminHandler({
106
107
  export const handle = sequence(authHandle, adminHandle);
107
108
  ```
108
109
 
110
+ ### Logout
111
+
112
+ Same philosophy as `authCheck`: the library has no session system of its
113
+ own, so it can't clear one for you. Provide the side effect (clear a
114
+ cookie, invalidate a session, call your auth library's sign-out...) and a
115
+ "Log out" button appears in the sidebar automatically — no button is
116
+ rendered at all if `logout` isn't set.
117
+
118
+ ```typescript
119
+ const adminHandle = createAdminHandler({
120
+ prisma,
121
+ authCheck: (event) => event.locals.session?.user?.role === 'admin',
122
+ logout: (event) => {
123
+ event.cookies.delete('session', { path: '/' });
124
+ },
125
+ logoutRedirectTo: '/login' // default: '/'
126
+ });
127
+ ```
128
+
129
+ The button submits a `POST {basePath}/_logout` form (never a bare link —
130
+ logging out must never be triggerable by a GET, unlike a crawler or link
131
+ prefetch would allow), and this route is checked *before* `authCheck`, so
132
+ a user whose session already expired can still use it to clean up
133
+ client-side state instead of being stuck behind a 401 with no way back.
134
+
109
135
  ## How It Works
110
136
 
111
137
  The admin handler intercepts all requests to `/admin/*` and:
@@ -140,6 +166,49 @@ models: {
140
166
  }
141
167
  ```
142
168
 
169
+ ## Multi-tenant / row-level scoping (`listWhere`) — read this before relying on it
170
+
171
+ ```typescript
172
+ models: {
173
+ Post: {
174
+ // Applied to the LIST VIEW ONLY: search, sidebar filters (including
175
+ // the FK filter), and pagination counts. Composed with an AND, never
176
+ // a spread, so it can never be overwritten by a user-supplied filter
177
+ // on the same field.
178
+ listWhere: ({ locals }) => ({ tenantId: locals.tenantId })
179
+ }
180
+ }
181
+ ```
182
+
183
+ **`listWhere` does NOT scope anything except the list view.** The detail
184
+ view, the edit form, the delete action, and the dashboard's per-model
185
+ counts have no equivalent scoping hook in this version and remain fully
186
+ open regardless of this config. Concretely: with only `listWhere` set, a
187
+ user who obtains another tenant's row ID through any other channel (a
188
+ referrer header, a log line, or simple enumeration on a model with an
189
+ `Int` primary key) can still view, edit, and delete that row directly —
190
+ `listWhere` only stops them from *discovering* the ID through the list or
191
+ the FK filter in the first place.
192
+
193
+ If you configure a `relations[field].where` scope for an FK filter (to
194
+ resolve the target's display options/label, see the FK filter docs), it
195
+ is a **separate function** from `listWhere` and is **not** kept in sync
196
+ automatically — you must set both if you want the FK filter's dropdown
197
+ *and* its active-value chip *and* the list rows to all be scoped
198
+ consistently for the same relation.
199
+
200
+ A `listWhere` function that returns `{}` throws instead of silently
201
+ disabling the scope — this is deliberate: an empty object composed into
202
+ an `AND` clause matches every row, which would fail *open* exactly when a
203
+ caller (e.g. one built from a session value that unexpectedly turned out
204
+ to be undefined) most needs protection. Make sure your scope function
205
+ either returns a real condition or isn't called at all for a given
206
+ request.
207
+
208
+ Real per-record scoping (detail/edit/delete) is a known gap, not an
209
+ oversight — track it separately if your application needs it; do not
210
+ assume `listWhere` covers it.
211
+
143
212
  ## Prisma Schema Introspection
144
213
 
145
214
  The admin automatically parses your Prisma schema and:
@@ -148,7 +217,16 @@ The admin automatically parses your Prisma schema and:
148
217
  - Detects field types and generates appropriate form inputs
149
218
  - Handles relations (excluded from forms for now)
150
219
  - Respects field attributes (@id, @unique, @default, @updatedAt)
151
- - Auto-hides common sensitive fields (password, hash, secret, token)
220
+ - Auto-hides common sensitive fields from the list view: any field whose name
221
+ contains `password`, `hash`, `secret` or `token` (case-insensitive), so
222
+ `hashedPassword`, `passwordHash` and `refreshToken` are all covered. Two things
223
+ to know about it:
224
+ - The match is on substrings, so it also catches ordinary names such as
225
+ `hashtag`, `tokenCount` or `secretariat`. Listing a field in
226
+ `models[].listFields` shows it regardless — that is the explicit override,
227
+ and the way to display a column the heuristic gets wrong.
228
+ - It only applies to the list view. Edit forms still render these fields, so
229
+ you can set a value; use `models[].hidden` to remove one everywhere.
152
230
 
153
231
  ## Supported Field Types
154
232
 
package/dist/index.d.ts CHANGED
@@ -3,10 +3,5 @@
3
3
  * Django-like admin panel for SvelteKit + Prisma
4
4
  */
5
5
  export { createAdminHandler, type AdminHandlerConfig } from './server/handler.js';
6
- export { createAdmin, createLayoutLoad, createDashboardLoad, createModelListLoad, createModelNewLoad, createModelNewAction, createModelEditLoad, createModelEditAction, createModelDeleteAction, createAdminGuard, type AdminConfig, type AdminContext } from './admin.js';
7
- export { parsePrismaSchema, parseSchemaContent, getDisplayFields, getEditableFields, getInputType, fieldToLabel, type PrismaSchema, type PrismaModel, type PrismaField } from './server/introspection/parser.js';
8
- export { createListOperation, createGetOperation, createCreateOperation, createUpdateOperation, createDeleteOperation, buildSearchWhere, buildFilterWhere, type ListOptions, type ListResult } from './server/crud/operations.js';
9
- export { createAuthGuard, defaultAdminCheck, type AdminAuthConfig, type AdminSession } from './server/auth/guard.js';
10
- export { default as AdminLayout } from './components/AdminLayout.svelte';
11
- export { default as DataTable } from './components/DataTable.svelte';
12
- export { default as AdminForm } from './components/AdminForm.svelte';
6
+ export { defaultAdminCheck } from './server/auth.js';
7
+ export { parsePrismaSchema, parseSchemaContent, type PrismaSchema, type PrismaModel, type PrismaField } from './server/introspection/parser.js';
package/dist/index.js CHANGED
@@ -2,17 +2,6 @@
2
2
  * SvelteKit Admin
3
3
  * Django-like admin panel for SvelteKit + Prisma
4
4
  */
5
- // Standalone handler (recommended - zero config!)
6
5
  export { createAdminHandler } from './server/handler.js';
7
- // Core admin factory and loaders (legacy)
8
- export { createAdmin, createLayoutLoad, createDashboardLoad, createModelListLoad, createModelNewLoad, createModelNewAction, createModelEditLoad, createModelEditAction, createModelDeleteAction, createAdminGuard } from './admin.js';
9
- // Prisma introspection utilities
10
- export { parsePrismaSchema, parseSchemaContent, getDisplayFields, getEditableFields, getInputType, fieldToLabel } from './server/introspection/parser.js';
11
- // CRUD operations
12
- export { createListOperation, createGetOperation, createCreateOperation, createUpdateOperation, createDeleteOperation, buildSearchWhere, buildFilterWhere } from './server/crud/operations.js';
13
- // Auth utilities
14
- export { createAuthGuard, defaultAdminCheck } from './server/auth/guard.js';
15
- // Re-export components
16
- export { default as AdminLayout } from './components/AdminLayout.svelte';
17
- export { default as DataTable } from './components/DataTable.svelte';
18
- export { default as AdminForm } from './components/AdminForm.svelte';
6
+ export { defaultAdminCheck } from './server/auth.js';
7
+ export { parsePrismaSchema, parseSchemaContent } from './server/introspection/parser.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Role-based access check for the admin panel
3
+ */
4
+ /**
5
+ * Default admin check - looks for role field
6
+ */
7
+ export declare function defaultAdminCheck(user: unknown, adminRole?: string): boolean;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Role-based access check for the admin panel
3
+ */
4
+ /**
5
+ * Default admin check - looks for role field
6
+ */
7
+ export function defaultAdminCheck(user, adminRole = 'admin') {
8
+ if (!user || typeof user !== 'object')
9
+ return false;
10
+ const u = user;
11
+ // Check common role field names
12
+ if (u.role === adminRole)
13
+ return true;
14
+ if (u.isAdmin === true)
15
+ return true;
16
+ if (Array.isArray(u.roles) && u.roles.includes(adminRole))
17
+ return true;
18
+ return false;
19
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Data access layer - derives Prisma model keys, coerces ids and form payloads,
3
+ * and wraps the CRUD calls the handler orchestrates.
4
+ */
5
+ import type { PrismaModel } from './introspection/parser.js';
6
+ export declare function toPrismaModel(name: string): string;
7
+ export declare function primaryKeyOf(model: PrismaModel): string;
8
+ /**
9
+ * Coerce l'id issu de l'URL vers le type de la clé primaire du modèle.
10
+ *
11
+ * L'ancienne implémentation appliquait `/^\d+$/.test(id) ? parseInt(id) : id`
12
+ * sans consulter le schéma : une PK String dont la valeur est entièrement
13
+ * numérique partait donc en nombre, et le vrai client Prisma rejetait la requête
14
+ * (`Argument \`id\`: Invalid value provided. Expected String, provided Int.`).
15
+ * Le mock, lui, acceptait n'importe quel type — d'où le test de régression
16
+ * côté intégration.
17
+ */
18
+ export declare function coerceId(id: string, model: PrismaModel): string | number;
19
+ /** Convertit un FormData en payload Prisma, en ignorant les champs auto-gérés. */
20
+ export declare function formDataToPrisma(formData: FormData, model: PrismaModel): Record<string, unknown>;
21
+ /**
22
+ * Traduit le paramètre `?page=` en fenêtre Prisma. Toute entrée qui n'est pas un
23
+ * entier sûr >= 1 retombe sur la première page : sans ce garde-fou, `?page=abc`
24
+ * envoyait `skip: NaN` et `?page=0` un `skip` négatif directement au client.
25
+ */
26
+ export declare function paginate(pageParam: string | null, perPage: number): {
27
+ page: number;
28
+ skip: number;
29
+ take: number;
30
+ };
31
+ export declare function listRecords(prisma: any, model: PrismaModel, page: number, perPage: number, where?: Record<string, unknown>): Promise<{
32
+ items: any[];
33
+ total: number;
34
+ }>;
35
+ export declare function getRecord(prisma: any, model: PrismaModel, id: string): Promise<any | null>;
36
+ export declare function createRecord(prisma: any, model: PrismaModel, data: Record<string, unknown>): Promise<void>;
37
+ export declare function updateRecord(prisma: any, model: PrismaModel, id: string, data: Record<string, unknown>): Promise<void>;
38
+ export declare function deleteRecord(prisma: any, model: PrismaModel, id: string): Promise<void>;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Data access layer - derives Prisma model keys, coerces ids and form payloads,
3
+ * and wraps the CRUD calls the handler orchestrates.
4
+ */
5
+ export function toPrismaModel(name) {
6
+ return name.charAt(0).toLowerCase() + name.slice(1);
7
+ }
8
+ export function primaryKeyOf(model) {
9
+ return model.fields.find((f) => f.isId)?.name || 'id';
10
+ }
11
+ /**
12
+ * Coerce l'id issu de l'URL vers le type de la clé primaire du modèle.
13
+ *
14
+ * L'ancienne implémentation appliquait `/^\d+$/.test(id) ? parseInt(id) : id`
15
+ * sans consulter le schéma : une PK String dont la valeur est entièrement
16
+ * numérique partait donc en nombre, et le vrai client Prisma rejetait la requête
17
+ * (`Argument \`id\`: Invalid value provided. Expected String, provided Int.`).
18
+ * Le mock, lui, acceptait n'importe quel type — d'où le test de régression
19
+ * côté intégration.
20
+ */
21
+ export function coerceId(id, model) {
22
+ const pkField = model.fields.find((f) => f.name === primaryKeyOf(model));
23
+ return pkField?.type === 'Int' ? parseInt(id) : id;
24
+ }
25
+ /** Convertit un FormData en payload Prisma, en ignorant les champs auto-gérés. */
26
+ export function formDataToPrisma(formData, model) {
27
+ const data = {};
28
+ for (const field of model.fields) {
29
+ if (field.isId || field.isUpdatedAt || field.isCreatedAt || field.relation)
30
+ continue;
31
+ const value = formData.get(field.name);
32
+ if (value === null) {
33
+ if (field.type === 'Boolean')
34
+ data[field.name] = false;
35
+ continue;
36
+ }
37
+ switch (field.type) {
38
+ case 'Int':
39
+ case 'BigInt':
40
+ data[field.name] = value ? parseInt(value.toString()) : null;
41
+ break;
42
+ case 'Float':
43
+ case 'Decimal':
44
+ data[field.name] = value ? parseFloat(value.toString()) : null;
45
+ break;
46
+ case 'Boolean':
47
+ data[field.name] = value === 'on' || value === 'true' || value === '1';
48
+ break;
49
+ case 'DateTime':
50
+ data[field.name] = value ? new Date(value.toString()) : null;
51
+ break;
52
+ case 'Json':
53
+ try {
54
+ data[field.name] = value ? JSON.parse(value.toString()) : null;
55
+ }
56
+ catch {
57
+ data[field.name] = null;
58
+ }
59
+ break;
60
+ default:
61
+ data[field.name] = value.toString();
62
+ }
63
+ }
64
+ return data;
65
+ }
66
+ /**
67
+ * Traduit le paramètre `?page=` en fenêtre Prisma. Toute entrée qui n'est pas un
68
+ * entier sûr >= 1 retombe sur la première page : sans ce garde-fou, `?page=abc`
69
+ * envoyait `skip: NaN` et `?page=0` un `skip` négatif directement au client.
70
+ */
71
+ export function paginate(pageParam, perPage) {
72
+ const parsed = parseInt(pageParam || '1');
73
+ const page = Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : 1;
74
+ return { page, skip: (page - 1) * perPage, take: perPage };
75
+ }
76
+ export async function listRecords(prisma, model, page, perPage, where) {
77
+ const key = toPrismaModel(model.name);
78
+ const primaryKey = primaryKeyOf(model);
79
+ // Le calcul `(page - 1) * perPage` est dupliqué avec `paginate` DÉLIBÉRÉMENT.
80
+ // Router `listRecords` vers `paginate(String(page), perPage)` serait lossy : la
81
+ // conversion aller-retour passe par `parseInt`, et `String(1e24)` donne
82
+ // `'1e+24'`, que `parseInt` ramène à `1`. `listRecords` reçoit un `number` déjà
83
+ // validé par l'appelant ; ne pas « simplifier » en réintroduisant un parse.
84
+ const [items, total] = await Promise.all([
85
+ prisma[key].findMany({
86
+ where,
87
+ skip: (page - 1) * perPage,
88
+ take: perPage,
89
+ orderBy: { [primaryKey]: 'desc' }
90
+ }),
91
+ prisma[key].count({ where })
92
+ ]);
93
+ return { items, total };
94
+ }
95
+ export function getRecord(prisma, model, id) {
96
+ const primaryKey = primaryKeyOf(model);
97
+ return prisma[toPrismaModel(model.name)].findUnique({
98
+ where: { [primaryKey]: coerceId(id, model) }
99
+ });
100
+ }
101
+ export async function createRecord(prisma, model, data) {
102
+ await prisma[toPrismaModel(model.name)].create({ data });
103
+ }
104
+ export async function updateRecord(prisma, model, id, data) {
105
+ const primaryKey = primaryKeyOf(model);
106
+ await prisma[toPrismaModel(model.name)].update({
107
+ where: { [primaryKey]: coerceId(id, model) },
108
+ data
109
+ });
110
+ }
111
+ export async function deleteRecord(prisma, model, id) {
112
+ const primaryKey = primaryKeyOf(model);
113
+ await prisma[toPrismaModel(model.name)].delete({
114
+ where: { [primaryKey]: coerceId(id, model) }
115
+ });
116
+ }
@@ -11,24 +11,143 @@ export interface AdminHandlerConfig {
11
11
  basePath?: string;
12
12
  /** Authentication check - return true if user can access admin */
13
13
  authCheck?: (event: any) => boolean | Promise<boolean>;
14
+ /**
15
+ * Logout logic — same "bring your own auth" philosophy as `authCheck`:
16
+ * this library has no session system of its own, so it can't know how
17
+ * to clear yours (a cookie, a Lucia/Better-Auth/Auth.js call, whatever).
18
+ * You provide the side effect (clear the cookie, invalidate the
19
+ * session...); the handler wires it to a POST-only route and a sidebar
20
+ * button. No button is rendered at all if this isn't set — an admin
21
+ * with no `logout` configured looks exactly as it did before this
22
+ * option existed.
23
+ *
24
+ * POST-only, never a bare link: logging out must never be triggerable
25
+ * by a GET (a crawler, a link prefetch, `<img src>`), which a plain
26
+ * `<a href="/admin/_logout">` would allow. Runs BEFORE `authCheck` — a
27
+ * user whose session already expired (so `authCheck` would now reject
28
+ * them) must still be able to hit the logout route to clean up
29
+ * client-side state (e.g. clear a stale cookie) without being stuck
30
+ * behind a 401 first.
31
+ */
32
+ logout?: (event: any) => void | Promise<void>;
33
+ /** Where to redirect after logout (default: '/') */
34
+ logoutRedirectTo?: string;
14
35
  /** Per-model configuration */
15
36
  models?: Record<string, {
16
37
  hidden?: string[];
17
38
  readonly?: string[];
18
39
  listFields?: string[];
19
40
  label?: string;
20
- icon?: string;
41
+ /**
42
+ * Scoping `where` applied to the LIST VIEW ONLY of this model
43
+ * (search, sidebar filters, FK filter, pagination count) — composed
44
+ * via `AND` with active filters, never a spread (docs/design
45
+ * §0.c/§5.2). Deliberately named `listWhere`, not `where`: a bare
46
+ * `where` invites a developer to believe it scopes every operation
47
+ * on the model (detail view, edit, delete, dashboard counts), which
48
+ * it does NOT — those have no equivalent scoping in this version and
49
+ * remain fully open regardless of this config (a real risk found in
50
+ * review: a multi-tenant app that relies on `listWhere` alone gets a
51
+ * false sense of safety while `getRecord`/`updateRecord`/
52
+ * `deleteRecord` stay unscoped for anyone who obtains a row's ID
53
+ * through another channel — a referrer, a log line, or straight
54
+ * enumeration on a model with an Int primary key).
55
+ *
56
+ * Without this scope, the FK filter this feature adds makes
57
+ * cross-tenant row discovery through the list trivial: `?f.authorId=
58
+ * 1..N` used to return another tenant's row (its label stayed
59
+ * protected via `relations[x].where`, §6.3.b, but the row itself did
60
+ * not). This config closes that specific hole for the list — nothing
61
+ * more. If you need every view scoped, you currently have to wire
62
+ * `relations[x].where` for the active-FK-label lookup separately
63
+ * (they are NOT the same function and are NOT automatically kept in
64
+ * sync), and there is no scoping hook at all yet for
65
+ * detail/edit/delete/dashboard — track that as a real gap, not an
66
+ * oversight to work around silently.
67
+ *
68
+ * A scope function that returns `{}` (e.g. because `locals.userId`
69
+ * was undefined after a session expired) is NOT treated as "no
70
+ * scope" — `{}` would silently fail open (an intersection with an
71
+ * empty clause matches everything) exactly when a caller most needs
72
+ * protection. It throws instead: fail loud on a misbehaving scope
73
+ * function, never fail open on a data leak.
74
+ */
75
+ listWhere?: (ctx: {
76
+ locals?: any;
77
+ }) => Record<string, unknown>;
78
+ relations?: Record<string, {
79
+ widget?: 'select' | 'raw-id' | 'hidden';
80
+ labelTemplate?: string;
81
+ orderBy?: Record<string, 'asc' | 'desc'>;
82
+ where?: (ctx: {
83
+ locals?: any;
84
+ }) => Record<string, unknown>;
85
+ nullLabel?: string;
86
+ }>;
87
+ /**
88
+ * Champs interrogés par la barre de recherche texte libre. Sans
89
+ * config, une heuristique conservatrice reprend `relationDefaults.labelFields`
90
+ * parmi les champs String non sensibles (voir docs/design/list-search-filters.md §2.1).
91
+ * Une config explicite gagne toujours et n'est jamais tronquée par
92
+ * l'heuristique — un champ non filtrable au sens de `isFilterableFieldType`
93
+ * (relation, liste, Json, Bytes) ou sensible (password/hash/secret/token)
94
+ * y est silencieusement ignoré.
95
+ */
96
+ searchFields?: string[];
97
+ /**
98
+ * Champs filtrables via la sidebar de la liste. Config explicite
99
+ * (forme courte `'published'` ou objet `{ field, label }`) — sinon
100
+ * une heuristique auto-détecte les champs Boolean et enum uniquement
101
+ * (domaine de valeurs connu statiquement, zéro requête pour rendre la
102
+ * sidebar ; voir docs/design/list-search-filters.md §3.5). Une config
103
+ * invalide (champ inexistant, sensible, relation, type non supporté)
104
+ * lève une erreur au démarrage — c'est une erreur de développeur, elle
105
+ * doit échouer fort plutôt que produire un filtre silencieusement mort.
106
+ */
107
+ listFilter?: import('./query/filterDetection.js').ListFilterConfigEntry[];
21
108
  }>;
22
109
  /** Models to exclude from admin */
23
110
  exclude?: string[];
111
+ /** Hide pivot/junction tables automatically (default: true) */
112
+ hidePivotTables?: boolean;
113
+ /** Relation defaults */
114
+ relationDefaults?: {
115
+ /** Au-delà de ce nombre d'options, une FK est rendue en raw-id (default: 200) */
116
+ selectThreshold?: number;
117
+ /** Champs candidats pour le label, dans l'ordre de préférence */
118
+ labelFields?: string[];
119
+ };
120
+ /**
121
+ * Défauts pour la sidebar de filtres (listFilter).
122
+ * `linkThreshold`: en dessous ou égal à ce nombre d'options, un filtre FK
123
+ * est rendu en liens dans la sidebar ; au-dessus (et ≤ relationDefaults.selectThreshold),
124
+ * en `<select>` dans un mini form GET (docs/design/list-search-filters.md §3.2).
125
+ * `autoDetect`: auto-détection des champs Boolean/enum quand pas de config
126
+ * `listFilter` explicite (default: true).
127
+ */
128
+ listFilterDefaults?: {
129
+ linkThreshold?: number;
130
+ autoDetect?: boolean;
131
+ };
132
+ /**
133
+ * Recherche texte libre : configuration globale.
134
+ * `mode`: 'auto' détecte le provider du schéma et n'émet `mode: 'insensitive'`
135
+ * que sur postgresql/cockroachdb/mongodb (les seuls où Prisma le supporte —
136
+ * l'émettre sur sqlite/mysql/sqlserver lève une erreur Prisma). 'insensitive'
137
+ * et 'default' forcent le comportement, pour un provider non détectable
138
+ * (`provider = env(...)`) ou un besoin spécifique (index `citext`, etc.).
139
+ * Voir docs/design/list-search-filters.md §2.5.
140
+ */
141
+ search?: {
142
+ mode?: 'auto' | 'insensitive' | 'default';
143
+ };
24
144
  /** Custom branding */
25
145
  branding?: {
26
146
  title?: string;
27
- logo?: string;
28
147
  primaryColor?: string;
29
148
  };
30
149
  }
31
150
  export declare function createAdminHandler(config: AdminHandlerConfig): ({ event, resolve }: {
32
151
  event: any;
33
- resolve: Function;
34
- }) => Promise<any>;
152
+ resolve: (event: any) => Response | Promise<Response>;
153
+ }) => Promise<Response>;