sveltekit-admin 0.6.0 → 0.8.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.
Files changed (45) hide show
  1. package/README.md +61 -4
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +1 -1
  4. package/dist/server/adapters/drizzle/dataAdapter.js +121 -36
  5. package/dist/server/adapters/drizzle/index.d.ts +10 -1
  6. package/dist/server/adapters/drizzle/index.js +4 -0
  7. package/dist/server/adapters/prisma/dataAdapter.js +52 -10
  8. package/dist/server/adapters/prisma/handler.d.ts +14 -0
  9. package/dist/server/adapters/prisma/handler.js +37 -0
  10. package/dist/server/adapters/retry.d.ts +27 -0
  11. package/dist/server/adapters/retry.js +53 -0
  12. package/dist/server/adapters/types.d.ts +9 -2
  13. package/dist/server/audit.d.ts +65 -0
  14. package/dist/server/audit.js +106 -0
  15. package/dist/server/csrf.d.ts +33 -0
  16. package/dist/server/csrf.js +55 -0
  17. package/dist/server/errors.d.ts +47 -0
  18. package/dist/server/errors.js +90 -0
  19. package/dist/server/handler.d.ts +57 -26
  20. package/dist/server/handler.js +212 -561
  21. package/dist/server/mutations.d.ts +9 -0
  22. package/dist/server/mutations.js +295 -0
  23. package/dist/server/plugin.d.ts +47 -0
  24. package/dist/server/plugin.js +1 -0
  25. package/dist/server/pluginAccess.d.ts +7 -0
  26. package/dist/server/pluginAccess.js +79 -0
  27. package/dist/server/pluginRegistry.d.ts +12 -0
  28. package/dist/server/pluginRegistry.js +72 -0
  29. package/dist/server/query/listQuery.d.ts +1 -1
  30. package/dist/server/relationLoaders.d.ts +42 -0
  31. package/dist/server/relationLoaders.js +188 -0
  32. package/dist/server/router.d.ts +10 -0
  33. package/dist/server/router.js +42 -19
  34. package/dist/server/runtime.d.ts +46 -0
  35. package/dist/server/runtime.js +210 -0
  36. package/dist/server/search.d.ts +14 -0
  37. package/dist/server/search.js +78 -0
  38. package/dist/server/views/Form.svelte +26 -2
  39. package/dist/server/views/Form.svelte.d.ts +2 -1
  40. package/dist/server/views/Layout.svelte +27 -2
  41. package/dist/server/views/Layout.svelte.d.ts +2 -0
  42. package/dist/server/views/List.svelte +28 -3
  43. package/dist/server/views/List.svelte.d.ts +2 -2
  44. package/dist/server/views/types.d.ts +8 -0
  45. package/package.json +23 -21
@@ -1,23 +1,26 @@
1
1
  <script lang="ts">
2
2
  import type { AdminHandlerConfig } from '../handler.js';
3
- import type { ViewModel } from './types.js';
3
+ import type { RecordAction, ViewModel } from './types.js';
4
4
  import FieldInput from './FieldInput.svelte';
5
5
  import RelationSelect from './RelationSelect.svelte';
6
6
  import RelationCheckboxes from './RelationCheckboxes.svelte';
7
7
  import RelatedBlock from './RelatedBlock.svelte';
8
+ import { escapeHtml } from './html.js';
8
9
 
9
10
  let {
10
11
  mode,
11
12
  model,
12
13
  basePath,
13
14
  config,
14
- item
15
+ item,
16
+ recordActions = []
15
17
  }: {
16
18
  mode: 'create' | 'edit';
17
19
  model: ViewModel;
18
20
  basePath: string;
19
21
  config: AdminHandlerConfig;
20
22
  item?: any;
23
+ recordActions?: RecordAction[];
21
24
  } = $props();
22
25
 
23
26
  const modelConfig = $derived(config.models?.[model.name] || {});
@@ -77,6 +80,24 @@
77
80
  : []
78
81
  );
79
82
 
83
+ // Built as a single string (rather than {#if}/{#each}) so an empty/create-mode
84
+ // render stays a single @html call: Svelte 5's SSR wraps every {#if}/{#each} node
85
+ // in its own hydration-boundary comment regardless of the branch/array taken, so
86
+ // nesting recordActions in its own control-flow blocks would add bytes to every
87
+ // edit-form render even when recordActions is []. `label` and `href` are both
88
+ // escaped manually since this goes through @html instead of Svelte's
89
+ // auto-escaped text/attributes.
90
+ const recordActionsHtml = $derived(
91
+ mode === 'edit' && recordActions.length > 0
92
+ ? `<div class="ska-record-actions">${recordActions
93
+ .map(
94
+ (action) =>
95
+ `<a href="${escapeHtml(action.href)}" class="ska-btn ska-btn--secondary ska-btn--sm">${escapeHtml(action.label)}</a>`
96
+ )
97
+ .join('')}</div>`
98
+ : ''
99
+ );
100
+
80
101
  const inverseEdges = $derived(
81
102
  model.relationGraph
82
103
  ? [...model.relationGraph.edges.values()].filter(
@@ -92,6 +113,9 @@
92
113
  <p class="ska-subtitle">ID: {item[model.primaryKey]}</p>
93
114
  {/if}
94
115
 
116
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- recordActionsHtml escapes both action.label and action.href via escapeHtml -->
117
+ {@html recordActionsHtml}
118
+
95
119
  <div class="ska-card">
96
120
  <form method="POST" class="ska-form">
97
121
  <input type="hidden" name="_action" value={mode === 'create' ? 'create' : 'update'} />
@@ -1,11 +1,12 @@
1
1
  import type { AdminHandlerConfig } from '../handler.js';
2
- import type { ViewModel } from './types.js';
2
+ import type { RecordAction, ViewModel } from './types.js';
3
3
  type $$ComponentProps = {
4
4
  mode: 'create' | 'edit';
5
5
  model: ViewModel;
6
6
  basePath: string;
7
7
  config: AdminHandlerConfig;
8
8
  item?: any;
9
+ recordActions?: RecordAction[];
9
10
  };
10
11
  declare const Form: import("svelte").Component<$$ComponentProps, {}, "">;
11
12
  type Form = ReturnType<typeof Form>;
@@ -6,12 +6,16 @@
6
6
  content,
7
7
  config,
8
8
  modelList,
9
- currentModel
9
+ currentModel,
10
+ extraStyles = '',
11
+ extraScripts = ''
10
12
  }: {
11
13
  content: string;
12
14
  config: AdminHandlerConfig;
13
15
  modelList: Array<{ name: string; label: string }>;
14
16
  currentModel?: string;
17
+ extraStyles?: string;
18
+ extraScripts?: string;
15
19
  } = $props();
16
20
 
17
21
  const branding = $derived(config.branding ?? {});
@@ -21,6 +25,25 @@
21
25
  // No button at all if `logout` isn't configured — an admin that never
22
26
  // opted into this option looks exactly as it did before it existed.
23
27
  const showLogout = $derived(Boolean(config.logout));
28
+
29
+ // extraStyles is concatenated into the SAME @html expression as the theme <style>
30
+ // below, rather than a sibling {#if}/{@html} block: Svelte 5's SSR unconditionally
31
+ // wraps every {#if}/{#each}/{@html} node in its own hydration-boundary comment, even
32
+ // for a false/empty branch (see svelte/internal/server's `html()` helper) — a sibling
33
+ // block would add bytes to every render regardless of extraStyles being set.
34
+ // Concatenating keeps this ONE @html call, byte-identical to the pre-plugin-slots
35
+ // template when extraStyles is ''. extraScripts (bottom of <body>) has no such
36
+ // pre-existing @html call to fold into, so it stays its own @html — the smallest
37
+ // achievable footprint, though it still adds a fixed hydration-boundary comment
38
+ // pair even when empty (see task-6-report.md fix-round-1 notes).
39
+ //
40
+ // Built as a $derived here (rather than a nested template literal inline in the
41
+ // markup below) so tooling that tag-sniffs {@html} expressions for literal
42
+ // <style>/<script> text doesn't misparse the nested backticks.
43
+ const headStyleHtml = $derived(
44
+ `<style>${styles(primaryColor)}</style>${extraStyles ? `<style>${extraStyles}</style>` : ''}`
45
+ );
46
+ const bodyScriptHtml = $derived(extraScripts ? '<script>' + extraScripts + '</scr' + 'ipt>' : '');
24
47
  </script>
25
48
 
26
49
  <!doctype html>
@@ -31,7 +54,7 @@
31
54
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
32
55
  <title>{title}</title>
33
56
  <!-- eslint-disable-next-line svelte/no-at-html-tags -- CSS injected as raw text; a literal <style> block can't take a dynamic value; primaryColor is developer-supplied config, not request/database data, and this raw interpolation is unchanged from the original layout.ts implementation, not a new injection point introduced by this migration -->
34
- {@html `<style>${styles(primaryColor)}</style>`}
57
+ {@html headStyleHtml}
35
58
  </head>
36
59
  <!-- eslint-disable-next-line svelte/no-raw-special-elements -- server-only full-document template, never mounted client-side -->
37
60
  <body>
@@ -74,5 +97,7 @@
74
97
  {@html content}
75
98
  </main>
76
99
  </div>
100
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- plugin JS is developer-supplied, same trust as branding.primaryColor -->
101
+ {@html bodyScriptHtml}
77
102
  </body>
78
103
  </html>
@@ -7,6 +7,8 @@ type $$ComponentProps = {
7
7
  label: string;
8
8
  }>;
9
9
  currentModel?: string;
10
+ extraStyles?: string;
11
+ extraScripts?: string;
10
12
  };
11
13
  declare const Layout: import("svelte").Component<$$ComponentProps, {}, "">;
12
14
  type Layout = ReturnType<typeof Layout>;
@@ -1,10 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { AdminHandlerConfig } from '../handler.js';
3
- import type { ViewModel } from './types.js';
3
+ import type { ViewModel, ListRecordAction, FkFilterMeta } from './types.js';
4
4
  import type { ListQuery } from '../query/listQuery.js';
5
5
  import type { ResolvedFilterField } from '../query/filterDetection.js';
6
6
  import { DATETIME_PRESETS } from '../query/filterDetection.js';
7
- import type { FkFilterMeta } from './types.js';
8
7
  import { getDisplayFields } from '../introspection/parser.js';
9
8
  import { buildListUrl, hiddenParams } from '../query/urls.js';
10
9
  import { escapeHtml, toLabel, formatValue } from './html.js';
@@ -19,7 +18,8 @@
19
18
  query,
20
19
  currentUrl,
21
20
  listFilters,
22
- fkFilterMeta
21
+ fkFilterMeta,
22
+ recordActions = []
23
23
  }: {
24
24
  model: ViewModel;
25
25
  items: any[];
@@ -34,6 +34,7 @@
34
34
  listFilters?: ResolvedFilterField[];
35
35
  /** Métadonnées async (options scopées + label actif) pour les filtres FK configurés. */
36
36
  fkFilterMeta?: Map<string, FkFilterMeta>;
37
+ recordActions?: ListRecordAction[];
37
38
  } = $props();
38
39
 
39
40
  const modelConfig = $derived(config.models?.[model.name] || {});
@@ -126,6 +127,28 @@
126
127
  * pour un champ sensible que pour un champ inconnu (§0.a, §5.4) — ne
127
128
  * jamais dire "champ interdit", ça confirmerait son existence.
128
129
  */
130
+ // A per-row function (rather than {#each} in the markup) so an empty recordActions
131
+ // keeps this to the smallest possible footprint: Svelte 5's SSR wraps every
132
+ // {#each}/{@html} node in its own hydration-boundary comment regardless of the
133
+ // array's length/content (verified empirically — even {@html ''} still emits
134
+ // `<!--hash--><!---->`), so there is no template-level construct that renders zero
135
+ // bytes for an empty array here. Folding this into the pre-existing delete-form
136
+ // {@html} call (right below) was considered and rejected: recordActions must render
137
+ // *before* Edit (see list.test.ts "rend le lien avant Edit"), but the delete form's
138
+ // pre-existing {@html} — and thus its hydration marker — sits *after* Edit, so
139
+ // reusing it would either reorder Edit/recordActions or move the marker in front of
140
+ // Edit for every row, not just when recordActions is non-empty. Neither is
141
+ // byte-identical to the pre-recordActions baseline (see task-6-report.md fix-round-1
142
+ // notes). `action.label` and `hrefFor`'s return value are both escaped manually
143
+ // since this goes through @html instead of Svelte's auto-escaped text/attributes.
144
+ const recordActionsHtml = (id: string | number) =>
145
+ recordActions
146
+ .map(
147
+ (action) =>
148
+ `<a href="${escapeHtml(action.hrefFor(id))}" class="ska-btn ska-btn--secondary ska-btn--sm">${escapeHtml(action.label)}</a>`
149
+ )
150
+ .join('');
151
+
129
152
  const ignoredMessages = $derived.by(() => {
130
153
  return (query?.ignored ?? []).map((entry) => {
131
154
  // `param` est soit `f.<field>` / `f.<field>__<op>` (nouveau format),
@@ -217,6 +240,8 @@
217
240
  <!-- eslint-disable-next-line svelte/no-at-html-tags -- formatValue already escapes string values itself and returns a literal <span> only for null/undefined -->
218
241
  {#each displayFields as f (f.name)}<td>{@html formatValue(item[f.name], f.type)}</td>{/each}
219
242
  <td class="ska-table__actions">
243
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- recordActionsHtml escapes both action.label and hrefFor's return value via escapeHtml -->
244
+ {@html recordActionsHtml(item[model.primaryKey])}
220
245
  <a href="{listPath}/{item[model.primaryKey]}" class="ska-btn ska-btn--secondary ska-btn--sm">Edit</a>
221
246
  <!-- eslint-disable-next-line svelte/no-at-html-tags -- Svelte 5 rejects a literal onsubmit string as an event attribute; the PK is escaped manually here since it can't go through Svelte's native attribute escaping; the whole form (not just onsubmit) is rendered as raw HTML because there's no native-Svelte way to attach a plain inline onsubmit="..." string attribute at all in Svelte 5 templates, so the whole element had to be raw text to preserve the exact prior confirm-dialog behavior in a page that's never hydrated by a Svelte runtime -->
222
247
  {@html `<form method="POST" action="${listPath}/${escapeHtml(String(item[model.primaryKey]))}" style="display:inline" onsubmit="return confirm('Delete this item?')"><input type="hidden" name="_action" value="delete"><button type="submit" class="ska-btn ska-btn--danger ska-btn--sm">Delete</button></form>`}
@@ -1,8 +1,7 @@
1
1
  import type { AdminHandlerConfig } from '../handler.js';
2
- import type { ViewModel } from './types.js';
2
+ import type { ViewModel, ListRecordAction, FkFilterMeta } from './types.js';
3
3
  import type { ListQuery } from '../query/listQuery.js';
4
4
  import type { ResolvedFilterField } from '../query/filterDetection.js';
5
- import type { FkFilterMeta } from './types.js';
6
5
  type $$ComponentProps = {
7
6
  model: ViewModel;
8
7
  items: any[];
@@ -21,6 +20,7 @@ type $$ComponentProps = {
21
20
  listFilters?: ResolvedFilterField[];
22
21
  /** Métadonnées async (options scopées + label actif) pour les filtres FK configurés. */
23
22
  fkFilterMeta?: Map<string, FkFilterMeta>;
23
+ recordActions?: ListRecordAction[];
24
24
  };
25
25
  declare const List: import("svelte").Component<$$ComponentProps, {}, "">;
26
26
  type List = ReturnType<typeof List>;
@@ -22,6 +22,14 @@ export interface ViewModel {
22
22
  /** Compteurs des relations inverses (1-N, 1-1), indexés par "Model.field" */
23
23
  relatedCounts?: Map<string, number>;
24
24
  }
25
+ export interface RecordAction {
26
+ label: string;
27
+ href: string;
28
+ }
29
+ export interface ListRecordAction {
30
+ label: string;
31
+ hrefFor: (id: string | number) => string;
32
+ }
25
33
  /**
26
34
  * Résolution async d'un filtre FK (kind 'fk' dans ResolvedFilterField) :
27
35
  * options scopées pour la sidebar + label du chip actif scopé lui aussi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltekit-admin",
3
- "version": "0.6.0",
3
+ "version": "0.8.1",
4
4
  "description": "Django-like admin panel for SvelteKit + Prisma",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -22,23 +22,8 @@
22
22
  "!dist/**/*.test.*",
23
23
  "!dist/**/*.spec.*"
24
24
  ],
25
- "scripts": {
26
- "dev": "vite dev",
27
- "build": "npm run package",
28
- "package": "svelte-kit sync && svelte-package -o dist && echo 'Package size:' && bun pm pack --dry-run 2>&1 | grep -E 'Total files|Unpacked size'",
29
- "prepublishOnly": "npm run package",
30
- "size": "bun run package && node scripts/package-size.mjs",
31
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
32
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
33
- "test:gen": "prisma generate --schema tests/fixtures/prisma/schema.prisma",
34
- "test": "svelte-kit sync && npm run test:gen && vitest run",
35
- "test:watch": "svelte-kit sync && npm run test:gen && vitest",
36
- "test:coverage": "svelte-kit sync && npm run test:gen && vitest run --coverage",
37
- "lint": "svelte-kit sync && eslint .",
38
- "format": "prettier --write .",
39
- "changeset": "changeset",
40
- "version-packages": "changeset version",
41
- "release": "npm run package && changeset publish"
25
+ "publishConfig": {
26
+ "provenance": true
42
27
  },
43
28
  "peerDependencies": {
44
29
  "@prisma/client": ">=5.0.0",
@@ -55,7 +40,7 @@
55
40
  }
56
41
  },
57
42
  "devDependencies": {
58
- "@changesets/cli": "^3.0.0",
43
+ "@changesets/cli": "^3.0.1",
59
44
  "@eslint/js": "9",
60
45
  "@prisma/client": "^6.19.3",
61
46
  "@sveltejs/adapter-auto": "^7.0.1",
@@ -98,5 +83,22 @@
98
83
  "bugs": {
99
84
  "url": "https://github.com/dotNacer/sveltekit-admin/issues"
100
85
  },
101
- "author": "dotNacer"
102
- }
86
+ "author": "dotNacer",
87
+ "scripts": {
88
+ "dev": "vite dev",
89
+ "build": "pnpm run package",
90
+ "package": "svelte-kit sync && svelte-package -o dist",
91
+ "size": "pnpm run package && node scripts/package-size.mjs",
92
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
93
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
94
+ "test:gen": "prisma generate --schema tests/fixtures/prisma/schema.prisma",
95
+ "test": "svelte-kit sync && pnpm run test:gen && vitest run",
96
+ "test:watch": "svelte-kit sync && pnpm run test:gen && vitest",
97
+ "test:coverage": "svelte-kit sync && pnpm run test:gen && vitest run --coverage",
98
+ "lint": "svelte-kit sync && eslint .",
99
+ "format": "prettier --write .",
100
+ "changeset": "changeset",
101
+ "version-packages": "changeset version",
102
+ "release": "pnpm run package && changeset publish"
103
+ }
104
+ }