firstly 0.10.1 → 0.10.3

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 (29) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/esm/sqlAdmin/SqlDriftController.d.ts +54 -0
  3. package/esm/sqlAdmin/SqlDriftController.js +64 -0
  4. package/esm/sqlAdmin/index.d.ts +2 -0
  5. package/esm/sqlAdmin/index.js +2 -0
  6. package/esm/sqlAdmin/server/drift/createIndex.d.ts +29 -0
  7. package/esm/sqlAdmin/server/drift/createIndex.js +32 -0
  8. package/esm/sqlAdmin/server/drift/ident.d.ts +2 -0
  9. package/esm/sqlAdmin/server/drift/ident.js +2 -0
  10. package/esm/sqlAdmin/server/drift/nullable.d.ts +31 -0
  11. package/esm/sqlAdmin/server/drift/nullable.js +79 -0
  12. package/esm/sqlAdmin/server/drift/orphanColumns.d.ts +29 -0
  13. package/esm/sqlAdmin/server/drift/orphanColumns.js +76 -0
  14. package/esm/sqlAdmin/server/drift/planDropColumns.d.ts +27 -0
  15. package/esm/sqlAdmin/server/drift/planDropColumns.js +39 -0
  16. package/esm/sqlAdmin/server/drift/planNullableDrift.d.ts +46 -0
  17. package/esm/sqlAdmin/server/drift/planNullableDrift.js +35 -0
  18. package/esm/sqlAdmin/server/drift/planPkSync.d.ts +36 -0
  19. package/esm/sqlAdmin/server/drift/planPkSync.js +43 -0
  20. package/esm/sqlAdmin/server/drift/primaryKeys.d.ts +23 -0
  21. package/esm/sqlAdmin/server/drift/primaryKeys.js +60 -0
  22. package/esm/sqlAdmin/server/drift/relationIndexes.d.ts +43 -0
  23. package/esm/sqlAdmin/server/drift/relationIndexes.js +91 -0
  24. package/esm/sqlAdmin/server/index.d.ts +18 -1
  25. package/esm/sqlAdmin/server/index.js +9 -1
  26. package/esm/sqlAdmin/ui/SqlDrift.svelte +480 -0
  27. package/esm/sqlAdmin/ui/SqlDrift.svelte.d.ts +3 -0
  28. package/esm/svelte/ff.svelte.js +4 -3
  29. package/package.json +3 -3
@@ -0,0 +1,480 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Schema drift UI: one section per flag enabled in `sqlAdmin({ drift })`.
4
+ *
5
+ * Styled against the host app's semantic theme tokens, like `<SqlAdmin />`.
6
+ * Results are logged to the console as `for AI:` after each run.
7
+ */
8
+ import { onMount } from 'svelte'
9
+ import { SvelteSet } from 'svelte/reactivity'
10
+
11
+ import { SqlDriftController, type SqlDriftFlag } from '../SqlDriftController'
12
+
13
+ // Inferred from the BackendMethods, so this never imports from `server/`.
14
+ type PkResult = Awaited<ReturnType<typeof SqlDriftController.primaryKeys>>
15
+ type RelResult = Awaited<ReturnType<typeof SqlDriftController.relationIndexes>>
16
+ type DropResult = Awaited<ReturnType<typeof SqlDriftController.orphanColumns>>
17
+ type NullResult = Awaited<ReturnType<typeof SqlDriftController.nullable>>
18
+
19
+ let flags = $state<Record<SqlDriftFlag, boolean> | null>(null)
20
+ let flagsError = $state('')
21
+ onMount(async () => {
22
+ try {
23
+ flags = await SqlDriftController.driftFlags()
24
+ } catch (e) {
25
+ flagsError = e instanceof Error ? e.message : String(e)
26
+ }
27
+ })
28
+
29
+ // --- Flow 1: primary key drift (dry run & upsert) ---
30
+ let pkLoading = $state(false)
31
+ let pkError = $state('')
32
+ let pkResult = $state<PkResult | null>(null)
33
+
34
+ const pkPlans = $derived(
35
+ (pkResult?.plans ?? []).toSorted((a, b) => {
36
+ if (a.action === 'ok' && b.action !== 'ok') return 1
37
+ if (a.action !== 'ok' && b.action === 'ok') return -1
38
+ return a.table.localeCompare(b.table)
39
+ }),
40
+ )
41
+ const pkDrift = $derived(pkPlans.filter((p) => p.action !== 'ok').length)
42
+
43
+ async function runPk(apply: boolean) {
44
+ if (
45
+ apply &&
46
+ !window.confirm(`Apply ${pkDrift} migration(s)? This briefly locks the affected tables.`)
47
+ )
48
+ return
49
+ pkLoading = true
50
+ pkError = ''
51
+ try {
52
+ pkResult = await SqlDriftController.primaryKeys({ apply })
53
+ console.info('for AI:', JSON.stringify(pkResult))
54
+ } catch (e) {
55
+ pkError = e instanceof Error ? e.message : String(e)
56
+ } finally {
57
+ pkLoading = false
58
+ }
59
+ }
60
+
61
+ // --- Flow 2: relation indexes (dry run & create) ---
62
+ let relLoading = $state(false)
63
+ let relError = $state('')
64
+ let relResult = $state<RelResult | null>(null)
65
+
66
+ const relPlans = $derived(
67
+ (relResult?.plans ?? []).toSorted((a, b) => {
68
+ if (a.action === 'ok' && b.action !== 'ok') return 1
69
+ if (a.action !== 'ok' && b.action === 'ok') return -1
70
+ return a.name.localeCompare(b.name)
71
+ }),
72
+ )
73
+ const relMissing = $derived(relPlans.filter((p) => p.action === 'create').length)
74
+
75
+ async function runRel(apply: boolean) {
76
+ if (apply && !window.confirm(`Create ${relMissing} relation index(es)?`)) return
77
+ relLoading = true
78
+ relError = ''
79
+ try {
80
+ relResult = await SqlDriftController.relationIndexes({ apply })
81
+ console.info('for AI:', JSON.stringify(relResult))
82
+ } catch (e) {
83
+ relError = e instanceof Error ? e.message : String(e)
84
+ } finally {
85
+ relLoading = false
86
+ }
87
+ }
88
+
89
+ // --- Flow 3: drop orphan columns (dry run & selective drop) ---
90
+ // Destructive, so unlike flows 1 & 2 the dry run lists every orphan, nothing is
91
+ // selected by default, and apply drops only the ticked subset.
92
+ let dropLoading = $state(false)
93
+ let dropError = $state('')
94
+ let dropResult = $state<DropResult | null>(null)
95
+ const selected = new SvelteSet<string>()
96
+
97
+ const keyOf = (p: { table: string; column: string }) => `${p.table}.${p.column}`
98
+
99
+ const dropPlans = $derived(
100
+ (dropResult?.plans ?? []).toSorted(
101
+ (a, b) => a.table.localeCompare(b.table) || a.column.localeCompare(b.column),
102
+ ),
103
+ )
104
+
105
+ function toggle(key: string) {
106
+ if (selected.has(key)) selected.delete(key)
107
+ else selected.add(key)
108
+ }
109
+
110
+ async function runDrop(apply: boolean) {
111
+ const cols = dropPlans.filter((p) => selected.has(keyOf(p)))
112
+ if (apply) {
113
+ if (cols.length === 0) return
114
+ if (
115
+ !window.confirm(
116
+ `Drop ${cols.length} column${cols.length === 1 ? '' : 's'}? This briefly locks the affected tables and cannot be undone.\n\n${cols.map(keyOf).join('\n')}`,
117
+ )
118
+ )
119
+ return
120
+ }
121
+ dropLoading = true
122
+ dropError = ''
123
+ try {
124
+ dropResult = await SqlDriftController.orphanColumns(
125
+ apply ? { apply: true, columns: cols } : { apply: false },
126
+ )
127
+ selected.clear()
128
+ console.info('for AI:', JSON.stringify(dropResult))
129
+ } catch (e) {
130
+ dropError = e instanceof Error ? e.message : String(e)
131
+ } finally {
132
+ dropLoading = false
133
+ }
134
+ }
135
+ // --- Flow 4: nullability drift (dry run & selective fix) ---
136
+ // Same shape as flow 3 and for the same reason: rewriting '' to NULL cannot be
137
+ // undone, so nothing is ticked by default.
138
+ let nullLoading = $state(false)
139
+ let nullError = $state('')
140
+ let nullResult = $state<NullResult | null>(null)
141
+ const nullSelected = new SvelteSet<string>()
142
+
143
+ const nullPlans = $derived(
144
+ (nullResult?.plans ?? []).toSorted(
145
+ (a, b) => a.table.localeCompare(b.table) || a.column.localeCompare(b.column),
146
+ ),
147
+ )
148
+
149
+ function toggleNull(key: string) {
150
+ if (nullSelected.has(key)) nullSelected.delete(key)
151
+ else nullSelected.add(key)
152
+ }
153
+
154
+ async function runNull(apply: boolean) {
155
+ const cols = nullPlans.filter((p) => nullSelected.has(keyOf(p)))
156
+ if (apply) {
157
+ if (cols.length === 0) return
158
+ const blanking = cols.filter((p) => p.blankToNull)
159
+ if (
160
+ !window.confirm(
161
+ `Relax ${cols.length} column${cols.length === 1 ? '' : 's'}?` +
162
+ (blanking.length
163
+ ? `\n\n${blanking.length} of them will also rewrite every '' row to NULL, which cannot be undone:\n${blanking.map(keyOf).join('\n')}`
164
+ : ''),
165
+ )
166
+ )
167
+ return
168
+ }
169
+ nullLoading = true
170
+ nullError = ''
171
+ try {
172
+ nullResult = await SqlDriftController.nullable(
173
+ apply ? { apply: true, columns: cols } : { apply: false },
174
+ )
175
+ nullSelected.clear()
176
+ console.info('for AI:', JSON.stringify(nullResult))
177
+ } catch (e) {
178
+ nullError = e instanceof Error ? e.message : String(e)
179
+ } finally {
180
+ nullLoading = false
181
+ }
182
+ }
183
+ </script>
184
+
185
+ {#snippet runButton(label: string, loading: boolean, onclick: () => void)}
186
+ <button
187
+ type="button"
188
+ {onclick}
189
+ disabled={loading}
190
+ class="bg-secondary text-secondary-foreground hover:bg-secondary/80 px-3 py-1.5 text-sm font-medium disabled:opacity-50"
191
+ >
192
+ {loading ? 'Running…' : label}
193
+ </button>
194
+ {/snippet}
195
+
196
+ {#snippet applyButton(label: string, disabled: boolean, onclick: () => void)}
197
+ <button
198
+ type="button"
199
+ {onclick}
200
+ {disabled}
201
+ class="bg-primary text-primary-foreground hover:bg-primary/90 px-3 py-1.5 text-sm font-medium disabled:opacity-50"
202
+ >
203
+ {label}
204
+ </button>
205
+ {/snippet}
206
+
207
+ {#snippet resultTable(headers: string[], children: import('svelte').Snippet)}
208
+ <div class="border-border overflow-auto border">
209
+ <table class="w-full border-collapse text-sm">
210
+ <thead class="bg-muted">
211
+ <tr>
212
+ {#each headers as h (h)}
213
+ <th class="border-border border-b px-3 py-2 text-left font-semibold">{h}</th>
214
+ {/each}
215
+ </tr>
216
+ </thead>
217
+ <tbody>
218
+ {@render children()}
219
+ </tbody>
220
+ </table>
221
+ </div>
222
+ {/snippet}
223
+
224
+ <div class="border-border bg-card text-card-foreground border">
225
+ <header class="px-5 py-4">
226
+ <h2 class="text-lg font-semibold">Schema drift</h2>
227
+ <p class="text-muted-foreground mt-1 text-sm">
228
+ Remult creates tables and adds columns, but never revisits them. Each check below is a dry run
229
+ first; applying runs in one transaction.
230
+ </p>
231
+ {#if flagsError}
232
+ <pre
233
+ class="border-destructive/40 bg-destructive/10 text-destructive mt-3 overflow-auto border p-3 text-sm">{flagsError}</pre>
234
+ {:else if flags && !Object.values(flags).some(Boolean)}
235
+ <p class="text-muted-foreground mt-3 text-sm">
236
+ No check enabled. Opt in with <code class="font-mono"
237
+ >sqlAdmin(&#123; drift: &#123; entities, relationIndexes: true, ... &#125; &#125;)</code
238
+ >.
239
+ </p>
240
+ {/if}
241
+ </header>
242
+
243
+ <!-- Flow 1: primary keys -->
244
+ {#if flags?.primaryKeys}
245
+ <section class="border-border border-t p-5">
246
+ <h3 class="text-sm font-semibold">Primary key drift</h3>
247
+ <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
248
+ Postgres only sets a PK on first table creation and never ALTERs it, so a later
249
+ <code class="font-mono">id</code> change is silently ignored. Applying runs every migration atomically.
250
+ </p>
251
+
252
+ <div class="mb-4 flex flex-wrap gap-2">
253
+ {@render runButton('Audit (dry run)', pkLoading, () => runPk(false))}
254
+ {@render applyButton(
255
+ `Apply ${pkDrift} migration${pkDrift === 1 ? '' : 's'}`,
256
+ pkLoading || pkDrift === 0,
257
+ () => runPk(true),
258
+ )}
259
+ </div>
260
+
261
+ {#if pkError}
262
+ <pre
263
+ class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{pkError}</pre>
264
+ {/if}
265
+
266
+ {#if pkResult}
267
+ {#if pkResult.applied}
268
+ <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
269
+ Applied · {pkDrift} table{pkDrift === 1 ? '' : 's'} migrated
270
+ </p>
271
+ {/if}
272
+
273
+ {#snippet pkRows()}
274
+ {#each pkPlans as p (p.table)}
275
+ <tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
276
+ <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
277
+ <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
278
+ {p.before.join(', ') || '-'}
279
+ </td>
280
+ <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
281
+ {p.after.join(', ')}
282
+ </td>
283
+ <td
284
+ class="border-border border-b px-3 py-2 font-mono"
285
+ class:text-primary={p.action !== 'ok'}
286
+ >
287
+ {p.action}
288
+ </td>
289
+ </tr>
290
+ {/each}
291
+ {/snippet}
292
+ {@render resultTable(['Table', 'Current PK', 'Desired PK', 'Action'], pkRows)}
293
+ {/if}
294
+ </section>
295
+ {/if}
296
+
297
+ <!-- Flow 2: relation indexes -->
298
+ {#if flags?.relationIndexes}
299
+ <section class="border-border border-t p-5">
300
+ <h3 class="text-sm font-semibold">Relation indexes</h3>
301
+ <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
302
+ Postgres doesn't auto-index foreign keys. This indexes the FK columns of
303
+ <code class="font-mono">toOne</code> relations - unless they're already a leftmost prefix of an
304
+ existing index (the PK counts), so <code class="font-mono">a</code> on a
305
+ <code class="font-mono">(a, b)</code> table is skipped. Creates are idempotent (<code
306
+ class="font-mono">FF_IX_</code
307
+ >).
308
+ </p>
309
+
310
+ <div class="mb-4 flex flex-wrap gap-2">
311
+ {@render runButton('Check (dry run)', relLoading, () => runRel(false))}
312
+ {@render applyButton(
313
+ `Create ${relMissing} index${relMissing === 1 ? '' : 'es'}`,
314
+ relLoading || relMissing === 0,
315
+ () => runRel(true),
316
+ )}
317
+ </div>
318
+
319
+ {#if relError}
320
+ <pre
321
+ class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{relError}</pre>
322
+ {/if}
323
+
324
+ {#if relResult}
325
+ {#if relResult.applied}
326
+ <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
327
+ Created · {relMissing} index{relMissing === 1 ? '' : 'es'}
328
+ </p>
329
+ {/if}
330
+
331
+ {#snippet relRows()}
332
+ {#each relPlans as p (p.name)}
333
+ <tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
334
+ <td class="border-border border-b px-3 py-2 font-mono">{p.name}</td>
335
+ <td class="border-border text-muted-foreground border-b px-3 py-2 font-mono">
336
+ {p.table} ({p.columns.join(', ')})
337
+ </td>
338
+ <td
339
+ class="border-border border-b px-3 py-2 font-mono"
340
+ class:text-primary={p.action === 'create'}
341
+ >
342
+ {p.action === 'create' ? 'create' : `covered · ${p.coveredBy}`}
343
+ </td>
344
+ </tr>
345
+ {/each}
346
+ {/snippet}
347
+ {@render resultTable(['Index', 'Columns', 'Status'], relRows)}
348
+ {/if}
349
+ </section>
350
+ {/if}
351
+
352
+ <!-- Flow 3: drop orphan columns -->
353
+ {#if flags?.orphanColumns}
354
+ <section class="border-border border-t p-5">
355
+ <h3 class="text-sm font-semibold">Orphan columns</h3>
356
+ <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
357
+ Remult only ever ADDs columns, never drops them, so a field removed from an entity leaves its
358
+ column behind. This lists live columns no entity field declares - e.g. a
359
+ <code class="font-mono">createdAt</code> now covered by the changelog. Destructive, so nothing
360
+ is selected by default: tick the columns to <code class="font-mono">DROP</code>.
361
+ </p>
362
+
363
+ <div class="mb-4 flex flex-wrap gap-2">
364
+ {@render runButton('Check (dry run)', dropLoading, () => runDrop(false))}
365
+ {@render applyButton(
366
+ `Drop ${selected.size} column${selected.size === 1 ? '' : 's'}`,
367
+ dropLoading || selected.size === 0,
368
+ () => runDrop(true),
369
+ )}
370
+ </div>
371
+
372
+ {#if dropError}
373
+ <pre
374
+ class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{dropError}</pre>
375
+ {/if}
376
+
377
+ {#if dropResult}
378
+ {#if dropResult.applied}
379
+ <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
380
+ Dropped · {dropResult.dropped.length} column{dropResult.dropped.length === 1 ? '' : 's'}
381
+ </p>
382
+ {/if}
383
+
384
+ {#if dropPlans.length === 0}
385
+ <p class="text-muted-foreground text-sm">No orphan columns - every column maps to a field.</p>
386
+ {:else}
387
+ {#snippet dropRows()}
388
+ {#each dropPlans as p (keyOf(p))}
389
+ <tr class="even:bg-muted/40">
390
+ <td class="border-border border-b px-3 py-2">
391
+ <input
392
+ type="checkbox"
393
+ checked={selected.has(keyOf(p))}
394
+ onchange={() => toggle(keyOf(p))}
395
+ aria-label={`Drop ${keyOf(p)}`}
396
+ />
397
+ </td>
398
+ <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
399
+ <td class="border-border text-primary border-b px-3 py-2 font-mono">{p.column}</td>
400
+ </tr>
401
+ {/each}
402
+ {/snippet}
403
+ {@render resultTable(['', 'Table', 'Column'], dropRows)}
404
+ {/if}
405
+ {/if}
406
+ </section>
407
+ {/if}
408
+
409
+ <!-- Flow 4: nullability drift -->
410
+ {#if flags?.nullable}
411
+ <section class="border-border border-t p-5">
412
+ <h3 class="text-sm font-semibold">Nullability drift</h3>
413
+ <p class="text-muted-foreground mt-1 mb-4 max-w-[80ch] text-sm">
414
+ A field that is not <code class="font-mono">allowNull</code> makes Remult create the column
415
+ <code class="font-mono">default '' not null</code>, and it never relaxes that afterwards. So a
416
+ column added before its field learned <code class="font-mono">allowNull</code> still hands every
417
+ pre-existing row an empty string the entity now types as <code class="font-mono">null</code>.
418
+ This lists those columns: applying drops the default and the NOT NULL, and - for text columns
419
+ only - rewrites <code class="font-mono">''</code> to <code class="font-mono">NULL</code>. A
420
+ numeric or boolean empty value (<code class="font-mono">0</code>,
421
+ <code class="font-mono">false</code>) may be real data, so those keep their rows and only lose
422
+ the constraint. Destructive, so nothing is selected by default.
423
+ </p>
424
+
425
+ <div class="mb-4 flex flex-wrap gap-2">
426
+ {@render runButton('Check (dry run)', nullLoading, () => runNull(false))}
427
+ {@render applyButton(
428
+ `Fix ${nullSelected.size} column${nullSelected.size === 1 ? '' : 's'}`,
429
+ nullLoading || nullSelected.size === 0,
430
+ () => runNull(true),
431
+ )}
432
+ </div>
433
+
434
+ {#if nullError}
435
+ <pre
436
+ class="border-destructive/40 bg-destructive/10 text-destructive mb-4 overflow-auto border p-3 text-sm">{nullError}</pre>
437
+ {/if}
438
+
439
+ {#if nullResult}
440
+ {#if nullResult.applied}
441
+ <p class="border-primary/30 bg-primary/10 text-primary mb-3 border p-2 text-sm">
442
+ Fixed · {nullResult.fixed.length} column{nullResult.fixed.length === 1 ? '' : 's'}
443
+ </p>
444
+ {/if}
445
+
446
+ {#if nullPlans.length === 0}
447
+ <p class="text-muted-foreground text-sm">
448
+ No drift - every nullable field has a plain nullable column.
449
+ </p>
450
+ {:else}
451
+ {#snippet nullRows()}
452
+ {#each nullPlans as p (keyOf(p))}
453
+ <tr class="even:bg-muted/40">
454
+ <td class="border-border border-b px-3 py-2">
455
+ <input
456
+ type="checkbox"
457
+ checked={nullSelected.has(keyOf(p))}
458
+ onchange={() => toggleNull(keyOf(p))}
459
+ aria-label={`Fix ${keyOf(p)}`}
460
+ />
461
+ </td>
462
+ <td class="border-border border-b px-3 py-2 font-mono">{p.table}</td>
463
+ <td class="border-border text-primary border-b px-3 py-2 font-mono">{p.column}</td>
464
+ <td class="border-border border-b px-3 py-2 text-sm">
465
+ {[p.dropDefault && 'drop default', p.dropNotNull && 'drop not null']
466
+ .filter(Boolean)
467
+ .join(' · ')}
468
+ </td>
469
+ <td class="border-border border-b px-3 py-2 text-sm">
470
+ {p.blankToNull ? "'' → NULL" : 'rows kept'}
471
+ </td>
472
+ </tr>
473
+ {/each}
474
+ {/snippet}
475
+ {@render resultTable(['', 'Table', 'Column', 'Constraint', 'Rows'], nullRows)}
476
+ {/if}
477
+ {/if}
478
+ </section>
479
+ {/if}
480
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SqlDrift: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SqlDrift = ReturnType<typeof SqlDrift>;
3
+ export default SqlDrift;
@@ -1,4 +1,5 @@
1
1
  import { repo as remultRepo, } from 'remult';
2
+ import { errorMessage } from '../core/helper.js';
2
3
  import { dialog } from './dialog.svelte.js';
3
4
  /** Classify a thrown read error into an {@link FF_Issue} (best-effort status sniffing). */
4
5
  function toIssue(e) {
@@ -70,7 +71,7 @@ class FF_RepoHandle {
70
71
  this.#fireItems();
71
72
  },
72
73
  error: (e) => {
73
- this.error = e instanceof Error ? e.message : String(e);
74
+ this.error = errorMessage(e);
74
75
  this.loading.init = false;
75
76
  this.#fireIssue(toIssue(e));
76
77
  },
@@ -144,7 +145,7 @@ class FF_RepoHandle {
144
145
  }
145
146
  catch (e) {
146
147
  if (seq === this.#seq) {
147
- this.error = e instanceof Error ? e.message : String(e);
148
+ this.error = errorMessage(e);
148
149
  this.#fireIssue(toIssue(e));
149
150
  }
150
151
  }
@@ -280,7 +281,7 @@ class FF_RepoHandle {
280
281
  return res;
281
282
  }
282
283
  catch (e) {
283
- this.error = e instanceof Error ? e.message : String(e);
284
+ this.error = errorMessage(e);
284
285
  throw e;
285
286
  }
286
287
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "bin": {
@@ -42,8 +42,8 @@
42
42
  "cron": "4.4.0",
43
43
  "esm-env": "1.2.2",
44
44
  "nodemailer": "8.0.5",
45
- "svelte-sonner": "1.1.1",
46
- "tailwind-merge": "3.6.0",
45
+ "svelte-sonner": "1.2.1",
46
+ "tailwind-merge": "3.7.0",
47
47
  "tailwindcss": "4.3.0",
48
48
  "vite-plugin-kit-routes": "1.0.6",
49
49
  "vite-plugin-stripper": "0.10.4"