firstly 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # firstly
2
2
 
3
+ ## 0.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#344](https://github.com/jycouet/firstly/pull/344) [`857d7b3`](https://github.com/jycouet/firstly/commit/857d7b3b272ae6bc6b0ca7ccff7d838c4f463bc6) Thanks [@jycouet](https://github.com/jycouet)! - - BackendMethods of `mail`, `sqlAdmin`, `feedback` and `carbone` now carry an `apiPrefix` (`/api/ff/mail/sendTest`, `/api/ff/sqlAdmin/exec`, ...) so a host app method with the same name (e.g. its own `sendTest`) can no longer shadow them.
8
+ - `WriteMail` / `LastMails` read the semantic theme tokens (`bg-card`, `border-border`, `text-primary`, ...) like `SqlAdmin` instead of a hardcoded slate palette.
9
+ - `SqlAdmin`: the write toggle is a static "Allow writes" checkbox; the label no longer flips with the state.
10
+
11
+ ## 0.9.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#341](https://github.com/jycouet/firstly/pull/341) [`ea9b4b6`](https://github.com/jycouet/firstly/commit/ea9b4b67544dc755374b3178c22082dac986472e) Thanks [@jycouet](https://github.com/jycouet)! - svelte: added `repoFetch(fetch)` and `loadRepo(loadFn)` - bind a repo to a load's `event.fetch` so API rules apply on SSR, hydration reuses the SSR response (one query), and parallel `+layout.ts`/`+page.ts` loads can't leak into each other. Removed `remultApiUniversalLoad`, `remultApiServerLoad`, `withRemultFetch`; migrate to `loadRepo` (works for universal AND server loads) or the `repoFetch(event.fetch)` primitive.
16
+
3
17
  ## 0.8.1
4
18
 
5
19
  ### Patch Changes
@@ -133,24 +133,24 @@ export class CarboneController {
133
133
  __decorate([
134
134
  BackendMethod({
135
135
  allowed: [Roles_Carbon.Carbon_Admin, Roles_Carbon.Carbon_UploadTemplate],
136
- apiPrefix: 'carbone',
136
+ apiPrefix: 'ff/carbone',
137
137
  })
138
138
  ], CarboneController, "uploadTemplate", null);
139
139
  __decorate([
140
140
  BackendMethod({
141
141
  allowed: [Roles_Carbon.Carbon_Admin, Roles_Carbon.Carbon_DownloadTemplate],
142
- apiPrefix: 'carbone',
142
+ apiPrefix: 'ff/carbone',
143
143
  })
144
144
  ], CarboneController, "downloadTemplate", null);
145
145
  __decorate([
146
146
  BackendMethod({
147
147
  allowed: [Roles_Carbon.Carbon_Admin, Roles_Carbon.Carbon_DeleteTemplate],
148
- apiPrefix: 'carbone',
148
+ apiPrefix: 'ff/carbone',
149
149
  })
150
150
  ], CarboneController, "deleteTemplate", null);
151
151
  __decorate([
152
152
  BackendMethod({
153
153
  allowed: [Roles_Carbon.Carbon_Admin, Roles_Carbon.Carbon_Render],
154
- apiPrefix: 'carbone',
154
+ apiPrefix: 'ff/carbone',
155
155
  })
156
156
  ], CarboneController, "render", null);
@@ -370,23 +370,23 @@ repository(name: $repository, owner: $owner) {
370
370
  }
371
371
  }
372
372
  __decorate([
373
- BackendMethod({ allowed: Allow.authenticated })
373
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
374
374
  ], FeedbackController, "getMilestones", null);
375
375
  __decorate([
376
- BackendMethod({ allowed: Allow.authenticated })
376
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
377
377
  ], FeedbackController, "getIssues", null);
378
378
  __decorate([
379
- BackendMethod({ allowed: Allow.authenticated })
379
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
380
380
  ], FeedbackController, "getIssue", null);
381
381
  __decorate([
382
- BackendMethod({ allowed: Allow.authenticated })
382
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
383
383
  ], FeedbackController, "createIssue", null);
384
384
  __decorate([
385
- BackendMethod({ allowed: Allow.authenticated })
385
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
386
386
  ], FeedbackController, "addCommentOnIssue", null);
387
387
  __decorate([
388
- BackendMethod({ allowed: Allow.authenticated })
388
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
389
389
  ], FeedbackController, "close", null);
390
390
  __decorate([
391
- BackendMethod({ allowed: Allow.authenticated })
391
+ BackendMethod({ allowed: Allow.authenticated, apiPrefix: 'ff/feedback' })
392
392
  ], FeedbackController, "reOpen", null);
@@ -64,5 +64,5 @@ export class MailController {
64
64
  }
65
65
  }
66
66
  __decorate([
67
- BackendMethod({ allowed: Roles_Mail.Mail_Admin })
67
+ BackendMethod({ allowed: Roles_Mail.Mail_Admin, apiPrefix: 'ff/mail' })
68
68
  ], MailController, "sendTest", null);
@@ -75,25 +75,25 @@
75
75
  }
76
76
 
77
77
  function badgeClass(status: Mail['status']): string {
78
- if (status === 'sent') return 'bg-emerald-500/10 border-emerald-500/40 text-emerald-300'
78
+ if (status === 'sent') return 'bg-emerald-500/10 border-emerald-500/40 text-emerald-700'
79
79
  if (status === 'transport_not_configured')
80
- return 'bg-amber-500/10 border-amber-500/40 text-amber-300'
81
- return 'bg-red-500/10 border-red-500/40 text-red-300'
80
+ return 'bg-amber-500/10 border-amber-500/40 text-amber-700'
81
+ return 'bg-destructive/10 border-destructive text-destructive'
82
82
  }
83
83
  </script>
84
84
 
85
- <div class="border border-slate-700 bg-slate-800 text-slate-200">
86
- <header class="flex flex-wrap items-center gap-3 border-b border-slate-700 px-5 py-4">
85
+ <div class="border border-border bg-card text-card-foreground">
86
+ <header class="flex flex-wrap items-center gap-3 border-b border-border px-5 py-4">
87
87
  <div class="flex flex-col">
88
- <h2 class="text-lg font-semibold text-slate-100">Last mails</h2>
89
- <p class="text-sm text-slate-400">Recent mails sent through this app.</p>
88
+ <h2 class="text-lg font-semibold text-foreground">Last mails</h2>
89
+ <p class="text-sm text-muted-foreground">Recent mails sent through this app.</p>
90
90
  </div>
91
91
  {#if hasAccess}
92
92
  <button
93
93
  type="button"
94
94
  onclick={refresh}
95
95
  disabled={isLoading}
96
- class="ml-auto inline-flex items-center gap-2 border border-slate-600 bg-slate-700 px-3 py-1.5 text-sm font-medium text-slate-100 hover:bg-slate-600 disabled:opacity-50"
96
+ class="ml-auto inline-flex items-center gap-2 border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
97
97
  >
98
98
  {#if isLoading}
99
99
  <svg
@@ -111,24 +111,26 @@
111
111
  {/if}
112
112
  Refresh
113
113
  </button>
114
- <span class="text-xs text-slate-500">
114
+ <span class="text-xs text-muted-foreground">
115
115
  {mails.length} mail{mails.length === 1 ? '' : 's'}
116
- {#if live}<span class="text-indigo-400">· live</span>{/if}
116
+ {#if live}<span class="text-primary">· live</span>{/if}
117
117
  </span>
118
118
  {/if}
119
119
  </header>
120
120
 
121
121
  <div class="p-5">
122
122
  {#if !hasAccess}
123
- <div class="border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-200">
123
+ <div class="border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-700">
124
124
  You need the
125
- <code class="bg-amber-500/20 px-1 py-0.5 text-xs text-amber-100">Mail.Admin</code>
125
+ <code class="bg-amber-500/20 px-1 py-0.5 text-xs text-amber-800">Mail.Admin</code>
126
126
  role to use this.
127
127
  </div>
128
128
  {:else if error}
129
- <div class="border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-200">{error}</div>
129
+ <div class="border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
130
+ {error}
131
+ </div>
130
132
  {:else if mails.length === 0}
131
- <div class="border border-slate-700 bg-slate-900 p-3 text-sm text-slate-400">No mails yet.</div>
133
+ <div class="border border-border bg-muted p-3 text-sm text-muted-foreground">No mails yet.</div>
132
134
  {:else}
133
135
  <div class="flex flex-col gap-3">
134
136
  {#each mails as m (m.id)}
@@ -137,24 +139,24 @@
137
139
  {@const bcc = formatList(m.metadata?.bcc)}
138
140
  {@const messageId = m.metadata?.transport?.messageId as string | undefined}
139
141
  {@const preview = m.metadata?.transport?.preview as string | undefined}
140
- <article class="flex flex-col gap-2 border border-slate-700 bg-slate-900 p-4">
142
+ <article class="flex flex-col gap-2 border border-border bg-background p-4">
141
143
  <div class="flex flex-wrap items-center gap-2">
142
144
  <span class="border px-2 py-0.5 text-xs font-medium {badgeClass(m.status)}">{m.status}</span>
143
145
  <span
144
- class="border border-slate-600 bg-slate-700/50 px-2 py-0.5 text-xs font-medium text-slate-200"
146
+ class="border border-border bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground"
145
147
  >{m.topic}</span
146
148
  >
147
- <span class="text-xs text-slate-400">{parseTo(m.to)}</span>
148
- <span class="ml-auto text-xs text-slate-500">{formatDate(m.createdAt)}</span>
149
+ <span class="text-xs text-muted-foreground">{parseTo(m.to)}</span>
150
+ <span class="ml-auto text-xs text-muted-foreground">{formatDate(m.createdAt)}</span>
149
151
  </div>
150
152
 
151
- <div class="text-base font-medium text-slate-100">{subject || '(no subject)'}</div>
153
+ <div class="text-base font-medium text-foreground">{subject || '(no subject)'}</div>
152
154
 
153
155
  {#if cc}
154
- <div class="text-xs text-slate-500">cc: {cc}</div>
156
+ <div class="text-xs text-muted-foreground">cc: {cc}</div>
155
157
  {/if}
156
158
  {#if bcc}
157
- <div class="text-xs text-slate-500">bcc: {bcc}</div>
159
+ <div class="text-xs text-muted-foreground">bcc: {bcc}</div>
158
160
  {/if}
159
161
 
160
162
  {#if preview}
@@ -163,18 +165,18 @@
163
165
  href={preview}
164
166
  target="_blank"
165
167
  rel="noopener noreferrer"
166
- class="text-indigo-400 underline hover:text-indigo-300">preview (mail not really sent)</a
168
+ class="text-primary underline hover:opacity-80">preview (mail not really sent)</a
167
169
  >
168
170
  </div>
169
171
  {/if}
170
172
 
171
173
  {#if messageId}
172
- <div class="text-xs break-all text-slate-500">id: <code>{messageId}</code></div>
174
+ <div class="text-xs break-all text-muted-foreground">id: <code>{messageId}</code></div>
173
175
  {/if}
174
176
 
175
177
  {#if m.status === 'error' && m.errorInfo}
176
178
  <pre
177
- class="border border-red-500/40 bg-red-500/10 p-2 text-xs whitespace-pre-wrap text-red-200">{m.errorInfo}</pre>
179
+ class="border border-destructive bg-destructive/10 p-2 text-xs whitespace-pre-wrap text-destructive">{m.errorInfo}</pre>
178
180
  {/if}
179
181
  </article>
180
182
  {/each}
@@ -47,24 +47,27 @@
47
47
  }
48
48
  </script>
49
49
 
50
- <div class="border border-slate-700 bg-slate-800 text-slate-200">
51
- <header class="border-b border-slate-700 px-5 py-4">
52
- <h2 class="text-lg font-semibold text-slate-100">Write mail</h2>
53
- <p class="mt-1 text-sm text-slate-400">Send a test mail through the configured transport.</p>
50
+ <div class="border border-border bg-card text-card-foreground">
51
+ <header class="border-b border-border px-5 py-4">
52
+ <h2 class="text-lg font-semibold text-foreground">Write mail</h2>
53
+ <p class="mt-1 text-sm text-muted-foreground">
54
+ Send a test mail through the configured transport.
55
+ </p>
54
56
  </header>
55
57
 
56
58
  <div class="p-5">
57
59
  {#if !hasAccess}
58
- <div class="border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-200">
60
+ <div class="border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-700">
59
61
  You need the
60
- <code class="bg-amber-500/20 px-1 py-0.5 text-xs text-amber-100">Mail.Admin</code>
62
+ <code class="bg-amber-500/20 px-1 py-0.5 text-xs text-amber-800">Mail.Admin</code>
61
63
  role to use this.
62
64
  </div>
63
65
  {:else}
64
66
  <form onsubmit={handleSubmit} class="flex flex-col gap-4">
65
67
  <div class="flex flex-col gap-1">
66
- <label for="write-mail-to" class="text-xs font-medium tracking-wide text-slate-400 uppercase"
67
- >To</label
68
+ <label
69
+ for="write-mail-to"
70
+ class="text-xs font-medium tracking-wide text-muted-foreground uppercase">To</label
68
71
  >
69
72
  <input
70
73
  id="write-mail-to"
@@ -72,14 +75,15 @@
72
75
  bind:value={to}
73
76
  disabled={isLoading}
74
77
  placeholder="someone@example.com, other@example.com"
75
- class="border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
78
+ class="border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-ring focus:outline-none disabled:opacity-50"
76
79
  />
77
80
  </div>
78
81
 
79
82
  <div class="grid grid-cols-2 gap-4 max-md:grid-cols-1">
80
83
  <div class="flex flex-col gap-1">
81
- <label for="write-mail-cc" class="text-xs font-medium tracking-wide text-slate-400 uppercase"
82
- >Cc</label
84
+ <label
85
+ for="write-mail-cc"
86
+ class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Cc</label
83
87
  >
84
88
  <input
85
89
  id="write-mail-cc"
@@ -87,12 +91,13 @@
87
91
  bind:value={cc}
88
92
  disabled={isLoading}
89
93
  placeholder="optional, comma-separated"
90
- class="border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
94
+ class="border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-ring focus:outline-none disabled:opacity-50"
91
95
  />
92
96
  </div>
93
97
  <div class="flex flex-col gap-1">
94
- <label for="write-mail-bcc" class="text-xs font-medium tracking-wide text-slate-400 uppercase"
95
- >Bcc</label
98
+ <label
99
+ for="write-mail-bcc"
100
+ class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Bcc</label
96
101
  >
97
102
  <input
98
103
  id="write-mail-bcc"
@@ -100,7 +105,7 @@
100
105
  bind:value={bcc}
101
106
  disabled={isLoading}
102
107
  placeholder="optional, comma-separated"
103
- class="border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
108
+ class="border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-ring focus:outline-none disabled:opacity-50"
104
109
  />
105
110
  </div>
106
111
  </div>
@@ -108,7 +113,7 @@
108
113
  <div class="flex flex-col gap-1">
109
114
  <label
110
115
  for="write-mail-subject"
111
- class="text-xs font-medium tracking-wide text-slate-400 uppercase">Subject</label
116
+ class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Subject</label
112
117
  >
113
118
  <input
114
119
  id="write-mail-subject"
@@ -117,28 +122,29 @@
117
122
  disabled={isLoading}
118
123
  required
119
124
  placeholder="Subject"
120
- class="border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
125
+ class="border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-ring focus:outline-none disabled:opacity-50"
121
126
  />
122
127
  </div>
123
128
 
124
129
  <div class="flex flex-col gap-1">
125
- <label for="write-mail-body" class="text-xs font-medium tracking-wide text-slate-400 uppercase"
126
- >Body</label
130
+ <label
131
+ for="write-mail-body"
132
+ class="text-xs font-medium tracking-wide text-muted-foreground uppercase">Body</label
127
133
  >
128
134
  <textarea
129
135
  id="write-mail-body"
130
136
  bind:value={body}
131
137
  disabled={isLoading}
132
138
  placeholder="Write your message..."
133
- class="h-40 w-full border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
139
+ class="h-40 w-full border border-input bg-background px-3 py-2 text-sm text-foreground placeholder-muted-foreground focus:border-ring focus:outline-none disabled:opacity-50"
134
140
  ></textarea>
135
141
  </div>
136
142
 
137
- <div class="flex items-center gap-4 border-t border-slate-700 pt-4">
143
+ <div class="flex items-center gap-4 border-t border-border pt-4">
138
144
  <button
139
145
  type="submit"
140
146
  disabled={isLoading || !canSend}
141
- class="inline-flex items-center gap-2 bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400 disabled:opacity-50"
147
+ class="inline-flex items-center gap-2 bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
142
148
  >
143
149
  {#if isLoading}
144
150
  <svg
@@ -158,12 +164,12 @@
158
164
  </button>
159
165
 
160
166
  {#if !canSend && !result && !error}
161
- <span class="text-xs text-slate-500"> Add a subject and at least one recipient. </span>
167
+ <span class="text-xs text-muted-foreground"> Add a subject and at least one recipient. </span>
162
168
  {/if}
163
169
 
164
170
  {#if result}
165
171
  <div
166
- class="flex flex-1 items-center gap-2 border border-emerald-500/40 bg-emerald-500/10 px-3 py-1.5 text-sm text-emerald-200"
172
+ class="flex flex-1 items-center gap-2 border border-emerald-500/40 bg-emerald-500/10 px-3 py-1.5 text-sm text-emerald-700"
167
173
  >
168
174
  <span class="font-medium">Sent</span>
169
175
  {#if result.messageId}
@@ -174,7 +180,7 @@
174
180
 
175
181
  {#if error}
176
182
  <pre
177
- class="flex-1 overflow-auto border border-red-500/40 bg-red-500/10 px-3 py-1.5 text-xs whitespace-pre-wrap text-red-200">{error}</pre>
183
+ class="flex-1 overflow-auto border border-destructive bg-destructive/10 px-3 py-1.5 text-xs whitespace-pre-wrap text-destructive">{error}</pre>
178
184
  {/if}
179
185
  </div>
180
186
  </form>
@@ -37,5 +37,8 @@ export class SqlAdminController {
37
37
  }
38
38
  }
39
39
  __decorate([
40
- BackendMethod({ allowed: [Roles_SqlAdmin.SqlAdmin_Admin, FF_Role.FF_Role_Admin] })
40
+ BackendMethod({
41
+ allowed: [Roles_SqlAdmin.SqlAdmin_Admin, FF_Role.FF_Role_Admin],
42
+ apiPrefix: 'ff/sqlAdmin',
43
+ })
41
44
  ], SqlAdminController, "exec", null);
@@ -21,7 +21,7 @@ LIMIT 10`
21
21
  let result: { rows: any[]; rowCount: number; took: number } | undefined = $state()
22
22
  let error = $state('')
23
23
  let isLoading = $state(false)
24
- let notReadOnly = $state(false)
24
+ let allowWrites = $state(false)
25
25
  let copied = $state(false)
26
26
 
27
27
  const queries = {
@@ -64,7 +64,7 @@ ORDER BY tablename, indexname`,
64
64
  try {
65
65
  error = ''
66
66
  isLoading = true
67
- result = await SqlAdminController.exec(sqlInput, notReadOnly)
67
+ result = await SqlAdminController.exec(sqlInput, allowWrites)
68
68
  log.info('for AI:', JSON.stringify(result.rows))
69
69
  log.info('for humans:', result)
70
70
  } catch (e) {
@@ -159,14 +159,16 @@ ORDER BY tablename, indexname`,
159
159
  Execute SQL
160
160
  </button>
161
161
 
162
+ <!-- Static label: the checkbox state is the signal, the text never
163
+ flips so the user always knows what ticking it does. -->
162
164
  <label
163
165
  class="inline-flex items-center gap-2 text-sm font-medium select-none"
164
- class:text-destructive={notReadOnly}
165
- class:text-muted-foreground={!notReadOnly}
166
- title="Read-only runs your query in a READ ONLY transaction so the database rejects any write. Tick this only when you know what you are doing."
166
+ class:text-destructive={allowWrites}
167
+ class:text-muted-foreground={!allowWrites}
168
+ title="Unchecked: the query runs in a READ ONLY transaction, so the database rejects any write. Tick only when you know what you are doing."
167
169
  >
168
- <input type="checkbox" bind:checked={notReadOnly} class="accent-destructive" />
169
- {notReadOnly ? 'Writes enabled (I know what I am doing)' : 'Read-only'}
170
+ <input type="checkbox" bind:checked={allowWrites} class="accent-destructive" />
171
+ Allow writes
170
172
  </label>
171
173
 
172
174
  {#if error}
@@ -18,7 +18,7 @@ export { default as FF_ToastManager } from './FF_ToastManager.svelte';
18
18
  export { SP } from './class/SP.svelte';
19
19
  export type { ParamDefinition } from './class/SP.svelte';
20
20
  export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
21
- export { remultApiUniversalLoad } from './remultApiLoad.js';
21
+ export { loadRepo, repoFetch, type Repo } from './remultApiLoad.js';
22
22
  export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
23
23
  export type { Cell, CellInput, CellComponent, CellUI, MetaKind, CellConfig, CellElementConfig, CellProps, CellContentProps, CellElementProps, CellMode, HubConfig, ActionConfig, } from './grid/index.js';
24
24
  export { default as Icon } from './ui/Icon.svelte';
@@ -10,7 +10,7 @@ export { toast } from './toast.js';
10
10
  export { default as FF_ToastManager } from './FF_ToastManager.svelte';
11
11
  export { SP } from './class/SP.svelte';
12
12
  export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
13
- export { remultApiUniversalLoad } from './remultApiLoad.js';
13
+ export { loadRepo, repoFetch } from './remultApiLoad.js';
14
14
  // Headless cell primitives (FF_Grid / FF_Group are boutique-only — copy from src/boutique/grid).
15
15
  export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
16
16
  export { default as Icon } from './ui/Icon.svelte';
@@ -1,25 +1,41 @@
1
- import type { LoadEvent } from '@sveltejs/kit';
1
+ import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
2
+ import { repo as globalRepo } from 'remult';
3
+ export type Repo = typeof globalRepo;
2
4
  /**
3
- * Run `body` in a scoped `withRemult` whose reads go through the API via `fetch`
4
- * (`allowApiRead` / `apiPrefilter` apply); a concurrent load keeps its own
5
- * provider. `remult.user` is carried in so the body can read it.
5
+ * A `repo` whose data access goes through the API via the given fetch:
6
+ * API rules apply on SSR (as the current user), hydration replays the SSR
7
+ * responses in the browser (one single query), nothing global is mutated
8
+ * (safe when `+layout.ts` and `+page.ts` run in parallel), and the ambient
9
+ * remult (user, context) stays in charge. Inherits `remult.apiClient` config.
10
+ *
11
+ * Name the result so the intent shows at every query:
12
+ * ```ts
13
+ * const repoClient = repoFetch(event.fetch)
14
+ * await repoClient(Task).find()
15
+ * ```
6
16
  *
7
17
  * TODO(remult): framework-agnostic (only needs a fetch) - candidate to live in
8
18
  * remult directly.
9
19
  */
10
- export declare function withRemultFetch<T>(fetch: typeof globalThis.fetch, body: () => Promise<T>): Promise<T>;
20
+ export declare function repoFetch(fetch: typeof globalThis.fetch, options?: {
21
+ url?: string;
22
+ }): Repo;
11
23
  /**
12
- * Wrap a SvelteKit UNIVERSAL load (`+page.ts` / `+layout.ts`) so plain global
13
- * `repo()` / `ff()` reads through the API (so `allowApiRead` / `apiPrefilter`
14
- * apply), on both SSR and CSR.
24
+ * Wrap a load: the callback receives a repo bound to `event.fetch`, then the event.
25
+ * Works for universal (`+page.ts`) AND server (`+page.server.ts`) loads.
26
+ *
27
+ * ```ts
28
+ * export const load = loadRepo(async (repoClient, { params }) => ({
29
+ * task: await repoClient(Task).findFirst({ id: params.id }),
30
+ * })) satisfies PageLoad // or PageServerLoad
31
+ * ```
15
32
  *
16
- * - SSR: runs the body in a scoped `withRemult` bound to `event.fetch` - gated,
17
- * and a concurrent `+page.server.ts` keeps its own provider. SvelteKit inlines
18
- * the response.
19
- * - CSR / hydration: points the client `remult` at this page's `event.fetch` so the
20
- * inlined SSR response is reused, then runs the body.
33
+ * For server-only event members (`cookies`, `locals`, ...) annotate the event:
34
+ * `loadRepo(async (repoClient, event: ServerLoadEvent) => ...)`.
21
35
  *
22
- * (A universal load runs on the client too, so it must use `event.fetch`. For a
23
- * server-only load see `remultApiServerLoad` from `firstly/svelte/server`.)
36
+ * TODO(remult): candidate to live in remult - only the event default is
37
+ * SvelteKit-specific, the constraint itself is structural (`{ fetch }`).
24
38
  */
25
- export declare function remultApiUniversalLoad<T>(body: (event: LoadEvent) => Promise<T>): (event: LoadEvent) => Promise<T>;
39
+ export declare function loadRepo<R, Ev extends {
40
+ fetch: typeof globalThis.fetch;
41
+ } = LoadEvent | ServerLoadEvent>(loadFn: (repoClient: Repo, event: Ev) => R): (event: Ev) => R;
@@ -1,39 +1,44 @@
1
- import { isBackend, remult, RestDataProvider, withRemult } from 'remult';
1
+ import { repo as globalRepo, remult, RestDataProvider } from 'remult';
2
2
  /**
3
- * Run `body` in a scoped `withRemult` whose reads go through the API via `fetch`
4
- * (`allowApiRead` / `apiPrefilter` apply); a concurrent load keeps its own
5
- * provider. `remult.user` is carried in so the body can read it.
3
+ * A `repo` whose data access goes through the API via the given fetch:
4
+ * API rules apply on SSR (as the current user), hydration replays the SSR
5
+ * responses in the browser (one single query), nothing global is mutated
6
+ * (safe when `+layout.ts` and `+page.ts` run in parallel), and the ambient
7
+ * remult (user, context) stays in charge. Inherits `remult.apiClient` config.
8
+ *
9
+ * Name the result so the intent shows at every query:
10
+ * ```ts
11
+ * const repoClient = repoFetch(event.fetch)
12
+ * await repoClient(Task).find()
13
+ * ```
6
14
  *
7
15
  * TODO(remult): framework-agnostic (only needs a fetch) - candidate to live in
8
16
  * remult directly.
9
17
  */
10
- export function withRemultFetch(fetch, body) {
11
- const user = remult.user;
12
- return withRemult(async () => {
13
- remult.user = user;
14
- return body();
15
- }, { dataProvider: new RestDataProvider(() => ({ httpClient: fetch })) });
18
+ export function repoFetch(fetch, options) {
19
+ const dp = new RestDataProvider(() => ({
20
+ ...remult.apiClient,
21
+ httpClient: fetch,
22
+ ...(options?.url ? { url: options.url } : {}),
23
+ }));
24
+ return (entity, dataProvider) => globalRepo(entity, dataProvider ?? dp);
16
25
  }
17
26
  /**
18
- * Wrap a SvelteKit UNIVERSAL load (`+page.ts` / `+layout.ts`) so plain global
19
- * `repo()` / `ff()` reads through the API (so `allowApiRead` / `apiPrefilter`
20
- * apply), on both SSR and CSR.
27
+ * Wrap a load: the callback receives a repo bound to `event.fetch`, then the event.
28
+ * Works for universal (`+page.ts`) AND server (`+page.server.ts`) loads.
29
+ *
30
+ * ```ts
31
+ * export const load = loadRepo(async (repoClient, { params }) => ({
32
+ * task: await repoClient(Task).findFirst({ id: params.id }),
33
+ * })) satisfies PageLoad // or PageServerLoad
34
+ * ```
21
35
  *
22
- * - SSR: runs the body in a scoped `withRemult` bound to `event.fetch` - gated,
23
- * and a concurrent `+page.server.ts` keeps its own provider. SvelteKit inlines
24
- * the response.
25
- * - CSR / hydration: points the client `remult` at this page's `event.fetch` so the
26
- * inlined SSR response is reused, then runs the body.
36
+ * For server-only event members (`cookies`, `locals`, ...) annotate the event:
37
+ * `loadRepo(async (repoClient, event: ServerLoadEvent) => ...)`.
27
38
  *
28
- * (A universal load runs on the client too, so it must use `event.fetch`. For a
29
- * server-only load see `remultApiServerLoad` from `firstly/svelte/server`.)
39
+ * TODO(remult): candidate to live in remult - only the event default is
40
+ * SvelteKit-specific, the constraint itself is structural (`{ fetch }`).
30
41
  */
31
- export function remultApiUniversalLoad(body) {
32
- return (event) => {
33
- if (!isBackend()) {
34
- remult.apiClient.httpClient = event.fetch;
35
- return body(event);
36
- }
37
- return withRemultFetch(event.fetch, () => body(event));
38
- };
42
+ export function loadRepo(loadFn) {
43
+ return (event) => loadFn(repoFetch(event.fetch), event);
39
44
  }
@@ -1,14 +1 @@
1
- import type { ServerLoadEvent } from '@sveltejs/kit';
2
1
  export { handleCaching } from './handleCaching.js';
3
- /**
4
- * Wrap a SvelteKit SERVER load (`+page.server.ts`) so plain global `repo()` reads
5
- * apply the API gate (as the current user) instead of the privileged DB provider.
6
- *
7
- * Reads go through `event.fetch`: SvelteKit dispatches same-origin server fetch
8
- * in-process (no network) and forwards cookies, so the app's real auth/hooks run
9
- * for each read. Use it only when a server load should see exactly what the API
10
- * exposes; otherwise a server load keeps the DB provider (gate rows with
11
- * `backendPrefilter`). Gates direct `repo()` reads; BackendMethods keep their own
12
- * `allowed` gate.
13
- */
14
- export declare function remultApiServerLoad<T>(body: (event: ServerLoadEvent) => Promise<T>): (event: ServerLoadEvent) => Promise<T>;
@@ -1,16 +1 @@
1
- import { withRemultFetch } from '../remultApiLoad.js';
2
1
  export { handleCaching } from './handleCaching.js';
3
- /**
4
- * Wrap a SvelteKit SERVER load (`+page.server.ts`) so plain global `repo()` reads
5
- * apply the API gate (as the current user) instead of the privileged DB provider.
6
- *
7
- * Reads go through `event.fetch`: SvelteKit dispatches same-origin server fetch
8
- * in-process (no network) and forwards cookies, so the app's real auth/hooks run
9
- * for each read. Use it only when a server load should see exactly what the API
10
- * exposes; otherwise a server load keeps the DB provider (gate rows with
11
- * `backendPrefilter`). Gates direct `repo()` reads; BackendMethods keep their own
12
- * `allowed` gate.
13
- */
14
- export function remultApiServerLoad(body) {
15
- return (event) => withRemultFetch(event.fetch, () => body(event));
16
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",