firstly 0.11.0 → 0.12.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 (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/esm/changeLog/index.d.ts +1 -0
  3. package/esm/changeLog/server/index.d.ts +2 -2
  4. package/esm/changeLog/server/index.js +2 -2
  5. package/esm/core/FF_Allow.d.ts +3 -3
  6. package/esm/core/FF_Allow.js +3 -3
  7. package/esm/core/FF_Filter.d.ts +3 -3
  8. package/esm/core/FF_Filter.js +3 -3
  9. package/esm/core/FF_Validators.d.ts +2 -2
  10. package/esm/core/FF_Validators.js +2 -2
  11. package/esm/core/index.d.ts +1 -0
  12. package/esm/core/index.js +1 -1
  13. package/esm/cron/server/index.d.ts +1 -1
  14. package/esm/cron/server/index.js +1 -1
  15. package/esm/mail/ui/WriteMail.svelte +1 -1
  16. package/esm/sqlAdmin/server/index.d.ts +2 -2
  17. package/esm/sqlAdmin/server/index.js +2 -2
  18. package/esm/sqlAdmin/ui/SqlAdmin.svelte +1 -2
  19. package/esm/sqlAdmin/ui/SqlDrift.svelte +41 -41
  20. package/esm/svelte/FF_DialogManager.svelte +4 -4
  21. package/esm/svelte/FF_PromptDefault.svelte +6 -6
  22. package/esm/svelte/class/SP.svelte.d.ts +1 -0
  23. package/esm/svelte/class/SP.svelte.js +14 -15
  24. package/esm/svelte/dialog.svelte.d.ts +1 -1
  25. package/esm/svelte/grid/FF_Grid.svelte +11 -8
  26. package/esm/svelte/grid/buildCells.js +6 -0
  27. package/esm/svelte/grid/cellTypes.d.ts +4 -0
  28. package/esm/svelte/handleError.d.ts +8 -2
  29. package/esm/svelte/handleError.js +24 -2
  30. package/esm/svelte/index.d.ts +1 -1
  31. package/esm/svelte/index.js +1 -1
  32. package/esm/svelte/query/index.svelte.d.ts +40 -0
  33. package/esm/svelte/query/index.svelte.js +112 -0
  34. package/esm/svelte/query/read.remote.d.ts +7 -0
  35. package/esm/svelte/query/read.remote.js +27 -0
  36. package/esm/svelte/remultApiLoad.d.ts +21 -9
  37. package/esm/svelte/remultApiLoad.js +21 -9
  38. package/esm/svelte/server/handleCaching.d.ts +2 -2
  39. package/esm/svelte/server/handleCaching.js +1 -1
  40. package/esm/svelte/ui/Icon.svelte +0 -1
  41. package/esm/vite/index.js +17 -9
  42. package/package.json +8 -3
  43. package/bin/args.d.ts +0 -13
@@ -1,9 +1,4 @@
1
- // FIXME
2
- // Why packages/ui pnpm check fails with goto and page?
3
- // - OK it's not a sveltekit project... But this is never used! So why is it checking it ?!
4
- // @ts-ignore
5
1
  import { goto } from '$app/navigation';
6
- // @ts-ignore
7
2
  import { page } from '$app/state';
8
3
  import { debounce } from '../helpers/debounce.js';
9
4
  const CONFIG_DELIMITER = ';';
@@ -23,6 +18,7 @@ export class SP {
23
18
  computing = $state(false);
24
19
  // Store debounced toURL functions for each parameter
25
20
  debouncedToURL = {};
21
+ lastNavigation;
26
22
  // Shared debounce mechanism for all parameters
27
23
  sharedDebouncePending = $state(false);
28
24
  activeDebounces = $state({});
@@ -37,9 +33,8 @@ export class SP {
37
33
  for (const key of Object.keys(this.paramValues)) {
38
34
  this.debouncedValues[key] = this.paramValues[key];
39
35
  }
40
- // Update URL and reset flags
36
+ // `computing` stays on until toURL's navigation lands
41
37
  this.sharedDebouncePending = false;
42
- this.computing = false;
43
38
  this.longestDebounceTime = 0;
44
39
  this.toURL();
45
40
  };
@@ -393,16 +388,21 @@ export class SP {
393
388
  }
394
389
  const strSearch = params.toString() ? '?' + params.toString() : '';
395
390
  // Don't do the goto if the search params haven't changed!
396
- if (strSearch === window.location.search)
391
+ if (strSearch === window.location.search) {
392
+ this.computing = false;
397
393
  return;
398
- goto(`${window.location.pathname}${strSearch}`, {
399
- keepFocus: true,
400
- replaceState: true,
401
- noScroll: true,
394
+ }
395
+ const navigation = goto(`${window.location.pathname}${strSearch}`, {
396
+ replace: true,
397
+ reset: false,
402
398
  ...this.options.gotoOpts,
403
399
  });
404
- // Reset computing flag after URL update is complete
405
- this.computing = false;
400
+ this.lastNavigation = navigation;
401
+ // A newer navigation or a pending debounce keeps `computing` on
402
+ void navigation.finally(() => {
403
+ if (this.lastNavigation === navigation && !this.sharedDebouncePending)
404
+ this.computing = false;
405
+ });
406
406
  }
407
407
  /**
408
408
  * Reset all parameters to their default values
@@ -418,7 +418,6 @@ export class SP {
418
418
  this.sharedDebouncePending = false;
419
419
  this.longestDebounceTime = 0;
420
420
  // Update URL immediately without debounce
421
- this.computing = false;
422
421
  this.toURL();
423
422
  }
424
423
  }
@@ -15,7 +15,7 @@ export { resolveMessage } from '../core/FF_Validators.js';
15
15
  * cancelled/dismissed - no per-method `boolean` / `null` special-casing.
16
16
  *
17
17
  * ```svelte
18
- * import { dialog } from './'
18
+ * import { dialog } from 'firstly/svelte'
19
19
  *
20
20
  * {#snippet body(close)}
21
21
  * <input bind:value={name} />
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" generics="T extends object">
2
- // PUBLISHED batteries-included grid — `import { FF_Grid } from '..'` and go, zero setup:
2
+ // PUBLISHED batteries-included grid — `import { FF_Grid } from 'firstly/svelte'` and go, zero setup:
3
3
  // it bundles a default input + a neutral default skin. Reads the entity `hub` (every prop overrides).
4
4
  // For full control over markup + look, copy the boutique <App_Grid> instead (degit src/boutique/grid).
5
5
  import { untrack } from 'svelte'
@@ -9,10 +9,10 @@
9
9
 
10
10
  import { errorMessage } from '../../core/helper.js'
11
11
  import type { DialogClose } from '../dialog.svelte.js'
12
- import FF_Config from '../FF_Config.svelte'
13
- import { ffConfig } from '../FF_Config.svelte.js'
14
12
  import { ff } from '../ff.svelte.js'
15
13
  import type { FF_Many, ManyStrategy } from '../ff.svelte.js'
14
+ import FF_Config from '../FF_Config.svelte'
15
+ import { ffConfig } from '../FF_Config.svelte.js'
16
16
  import Icon from '../ui/Icon.svelte'
17
17
  import { LibIcon_Add, LibIcon_Edit } from '../ui/LibIcon.js'
18
18
  import { buildCells } from './buildCells.js'
@@ -61,7 +61,10 @@
61
61
  const strategy = untrack(() => strategyProp ?? hub.strategy ?? 'paginate')
62
62
  const pageSize = untrack(() => pageSizeProp ?? hub.pageSize ?? 25)
63
63
 
64
- let sort = $state<EntityOrderBy<T> | undefined>(untrack(() => orderBy ?? hub.orderBy))
64
+ // fall back to the entity default so the header arrow shows what remult applies on landing
65
+ let sort = $state<EntityOrderBy<T> | undefined>(
66
+ untrack(() => orderBy ?? hub.orderBy ?? repo(entity).metadata.options.defaultOrderBy),
67
+ )
65
68
 
66
69
  const m = untrack(() =>
67
70
  ff(entity).many(
@@ -101,9 +104,9 @@
101
104
  const newIcon = $derived(insertCfg !== false ? (insertCfg.icon ?? LibIcon_Add) : LibIcon_Add)
102
105
  const editIcon = $derived(updateCfg !== false ? (updateCfg.icon ?? LibIcon_Edit) : LibIcon_Edit)
103
106
 
104
- function toggleSort(key: string) {
107
+ function toggleSort(key: string, first: 'asc' | 'desc') {
105
108
  const cur = (sort as Record<string, 'asc' | 'desc'> | undefined)?.[key]
106
- sort = { [key]: cur === 'asc' ? 'desc' : 'asc' } as EntityOrderBy<T>
109
+ sort = { [key]: cur ? (cur === 'asc' ? 'desc' : 'asc') : first } as EntityOrderBy<T>
107
110
  }
108
111
  const sortDir = (key: string) => (sort as Record<string, string> | undefined)?.[key]
109
112
 
@@ -185,9 +188,9 @@
185
188
  data-sortable={cell.sortable || undefined}
186
189
  style:text-align={cell.align}
187
190
  class={cell.class}
188
- onclick={() => cell.sortable && cell.col && toggleSort(cell.col)}
191
+ onclick={() => cell.sortable && cell.col && toggleSort(cell.col, cell.firstSort)}
189
192
  >
190
- {cell.caption}{#if cell.col && sortDir(cell.col)}<span data-sort
193
+ {cell.caption}{#if cell.sortable && cell.col && sortDir(cell.col)}<span data-sort
191
194
  >{sortDir(cell.col) === 'asc' ? ' ▲' : ' ▼'}</span
192
195
  >{/if}
193
196
  </th>
@@ -15,6 +15,11 @@ function alignFor(field, ui) {
15
15
  return 'right';
16
16
  return 'left';
17
17
  }
18
+ // newest / biggest first is the useful first view for dates + numbers
19
+ function firstSortFor(field) {
20
+ const t = field?.valueType;
21
+ return t === Date || t === Number ? 'desc' : 'asc';
22
+ }
18
23
  /** Resolve the render kind: explicit > href(field_link) > metaKind(relation/enum) > field. */
19
24
  function resolveKind(field, explicit) {
20
25
  if (explicit)
@@ -58,6 +63,7 @@ export function buildCells(meta, cells, opts) {
58
63
  inputType: field ? getInputType(field, ui) : 'text',
59
64
  align: alignFor(field, ui),
60
65
  sortable,
66
+ firstSort: (isObj && item.firstSort) || firstSortFor(field),
61
67
  class: isObj ? item.class : undefined,
62
68
  cellSnippet: isObj ? item.cellSnippet : undefined,
63
69
  component: isObj ? item.component : undefined,
@@ -41,6 +41,8 @@ export interface Cell<E = any> {
41
41
  align: 'left' | 'center' | 'right';
42
42
  /** Whether this column is sortable by header click (resolved; opt in via `sortable: true`). */
43
43
  sortable: boolean;
44
+ /** Direction of the first header click on an unsorted column. */
45
+ firstSort: 'asc' | 'desc';
44
46
  /** Tailwind/CSS passthrough (e.g. 'col-span-2'). */
45
47
  class?: string;
46
48
  cellSnippet?: Snippet<[{
@@ -62,6 +64,8 @@ export type CellInput<E> = (keyof E & string) | {
62
64
  class?: string;
63
65
  /** Set true to make this column sortable by header click (default: not sortable). */
64
66
  sortable?: boolean;
67
+ /** First header click direction. Default: `desc` for Date/Number fields, else `asc`. */
68
+ firstSort?: 'asc' | 'desc';
65
69
  cellSnippet?: Snippet<[{
66
70
  row: E;
67
71
  cell: Cell<E>;
@@ -1,4 +1,4 @@
1
- import type { HandleClientError } from '@sveltejs/kit';
1
+ import type { HandleClientError } from '@sveltejs/kit/hooks';
2
2
  export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleClientError;
3
3
  /**
4
4
  * Compose `handleError` middlewares into a single `hooks.client.js` handler.
@@ -8,7 +8,7 @@ export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleCli
8
8
  *
9
9
  * ```js
10
10
  * // src/hooks.client.js
11
- * import { stackHandleClientError, withStaleDeployReload } from './'
11
+ * import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
12
12
  *
13
13
  * export const handleError = stackHandleClientError(
14
14
  * withStaleDeployReload(),
@@ -29,3 +29,9 @@ export declare function stackHandleClientError(...middlewares: HandleClientError
29
29
  * falls through to `next`.
30
30
  */
31
31
  export declare function withStaleDeployReload(): HandleClientErrorMiddleware;
32
+ /**
33
+ * Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
34
+ * only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
35
+ * at the top of `hooks.client.js`.
36
+ */
37
+ export declare function reloadOnStaleChunk(): void;
@@ -1,3 +1,4 @@
1
+ import { navigating, updated } from '$app/state';
1
2
  /**
2
3
  * Compose `handleError` middlewares into a single `hooks.client.js` handler.
3
4
  * Middlewares run outermost-first (the first argument wraps the rest). A middleware
@@ -6,7 +7,7 @@
6
7
  *
7
8
  * ```js
8
9
  * // src/hooks.client.js
9
- * import { stackHandleClientError, withStaleDeployReload } from './'
10
+ * import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
10
11
  *
11
12
  * export const handleError = stackHandleClientError(
12
13
  * withStaleDeployReload(),
@@ -22,6 +23,8 @@ export function stackHandleClientError(...middlewares) {
22
23
  }
23
24
  // Errors SvelteKit/Vite throw when a lazy chunk 404s after a deploy.
24
25
  const CHUNK_LOAD_RE = /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/;
26
+ // Kit recovers route chunks itself, and one failing on hover must not reload the page.
27
+ const ROUTE_CHUNK_RE = /\/immutable\/nodes\//;
25
28
  const STORAGE_KEY = 'ff-reload';
26
29
  // A URL that failed again within this window is a genuine break, not a stale deploy.
27
30
  const RETRY_WINDOW_MS = 10_000;
@@ -35,12 +38,19 @@ const RETRY_WINDOW_MS = 10_000;
35
38
  */
36
39
  export function withStaleDeployReload() {
37
40
  return (next) => (input) => {
38
- if (CHUNK_LOAD_RE.test(String(input.message)) && reloadOnce(input.event.url.href)) {
41
+ if (input.kind === 'unknown' &&
42
+ CHUNK_LOAD_RE.test(String(input.error)) &&
43
+ isVisited(input.event.url) &&
44
+ reloadOnce(input.event.url.href)) {
39
45
  return; // page is unloading; no error surfaces
40
46
  }
41
47
  return next(input);
42
48
  };
43
49
  }
50
+ // A hovered link preloads through handleError too, and must not navigate on its own.
51
+ function isVisited(url) {
52
+ return url.href === location.href || url.href === navigating.to?.url.href;
53
+ }
44
54
  /** Hard-reload `href` unless it already failed within the guard window. Returns true if reloading. */
45
55
  function reloadOnce(href) {
46
56
  try {
@@ -60,3 +70,15 @@ function reloadOnce(href) {
60
70
  return false; // no sessionStorage (private mode) -> don't risk a loop
61
71
  }
62
72
  }
73
+ /**
74
+ * Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
75
+ * only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
76
+ * at the top of `hooks.client.js`.
77
+ */
78
+ export function reloadOnStaleChunk() {
79
+ addEventListener('vite:preloadError', (event) => {
80
+ if (ROUTE_CHUNK_RE.test(String(event.payload)))
81
+ return;
82
+ void updated.check().then((stale) => stale && location.reload());
83
+ });
84
+ }
@@ -3,7 +3,7 @@ export { ff } from './ff.svelte.js';
3
3
  export type { FF_Many, FF_One, FF_Builder, FF_RepoOptions, FF_OneOptions, FF_RepoLoading, FF_Issue, ManyStrategy, AggregateOptions, QueryOptionsHelper, } from './ff.svelte.js';
4
4
  export { infiniteScroll } from './infiniteScroll.js';
5
5
  export type { InfiniteScrollOptions } from './infiniteScroll.js';
6
- export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
6
+ export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
7
7
  export type { HandleClientErrorMiddleware } from './handleError.js';
8
8
  export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
9
9
  export type { DialogResult, DialogClose, DialogOptions, DialogItem, DialogRender, ConfirmItem, PromptItem, DialogShellArgs, DialogConfirmArgs, DialogPromptArgs, } from './dialog.svelte.js';
@@ -1,7 +1,7 @@
1
1
  export { errorMessage, isError } from '../core/helper.js';
2
2
  export { ff } from './ff.svelte.js';
3
3
  export { infiniteScroll } from './infiniteScroll.js';
4
- export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
4
+ export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
5
5
  export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
6
6
  export { default as FF_DialogManager } from './FF_DialogManager.svelte';
7
7
  export { default as FF_Config } from './FF_Config.svelte';
@@ -0,0 +1,40 @@
1
+ import { type ClassType, type EntityFilter, type FindOptions } from 'remult';
2
+ import type { RemoteQuery, RemoteResource } from '$app/server';
3
+ /** No `set` or `withOverride`: they would hold rows, not entities. */
4
+ export type RepoQuery<T> = RemoteResource<T> & Pick<RemoteQuery<T>, 'refresh'>;
5
+ /**
6
+ * `repo(E)` for reactive reads: SSR goes through the API (rules apply, as the
7
+ * current user) and hydration sends no query.
8
+ *
9
+ * ```svelte
10
+ * <script lang="ts">
11
+ * import { repo } from 'remult'
12
+ *
13
+ * import { queryRepo } from 'firstly/svelte/query'
14
+ *
15
+ * let { params } = $props()
16
+ *
17
+ * let tasks = $derived(await queryRepo(Task).find({ where: { slug: params.slug } }))
18
+ * </script>
19
+ *
20
+ * {#each tasks as task (task.id)}
21
+ * <button onclick={() => repo(Task).update(task.id, { done: true })}>{task.title}</button>
22
+ * {/each}
23
+ * ```
24
+ *
25
+ * Plain `repo(E)` writes in the browser refresh the queries of `E` on screen.
26
+ * To do so, importing this wraps `remult.dataProvider` in the browser. Call
27
+ * `.refresh()` for what it can't see: BackendMethods, `include`d entities and
28
+ * other users' writes.
29
+ *
30
+ * A failed refresh reaches the error page, like any `.refresh()` in Kit.
31
+ * Long filters can exceed the URL limit: arguments travel in a GET.
32
+ *
33
+ * @experimental Needs `experimental.remoteFunctions` and
34
+ * `compilerOptions.experimental.async`.
35
+ */
36
+ export declare const queryRepo: <T>(entity: ClassType<T>) => {
37
+ find: (options?: FindOptions<T>) => RepoQuery<T[]>;
38
+ findFirst: (where?: EntityFilter<T>, options?: FindOptions<T>) => RepoQuery<T | undefined>;
39
+ count: (where?: EntityFilter<T>) => RepoQuery<number>;
40
+ };
@@ -0,0 +1,112 @@
1
+ import { BROWSER } from 'esm-env';
2
+ import { Filter, remult, repo, } from 'remult';
3
+ import { read } from './read.remote.js';
4
+ const WRITES = new Set([
5
+ 'insert',
6
+ 'update',
7
+ 'delete',
8
+ 'deleteMany',
9
+ 'updateMany',
10
+ 'upsertMany',
11
+ ]);
12
+ /** Entity key -> args of the queries on screen. */
13
+ const live = new Map();
14
+ const track = (arg) => {
15
+ const id = JSON.stringify(arg);
16
+ try {
17
+ $effect.pre(() => {
18
+ const args = live.get(arg.key) ?? live.set(arg.key, new Map()).get(arg.key);
19
+ const entry = args.get(id) ?? args.set(id, { arg, count: 0 }).get(id);
20
+ entry.count++;
21
+ return () => {
22
+ if (--entry.count === 0)
23
+ args.delete(id);
24
+ };
25
+ });
26
+ }
27
+ catch {
28
+ // not in a reactive context: nothing on screen to refresh
29
+ }
30
+ };
31
+ // allSettled: a failed refresh must not fail the write that triggered it
32
+ const refreshLive = (key) => Promise.allSettled([...(live.get(key)?.values() ?? [])].map(({ arg }) => read(arg).refresh()));
33
+ const withRefresh = (dp) => new Proxy(dp, {
34
+ get(t, p) {
35
+ if (p !== 'getEntityDataProvider')
36
+ return Reflect.get(t, p);
37
+ return (entity) => new Proxy(t.getEntityDataProvider(entity), {
38
+ get(edp, m) {
39
+ const v = Reflect.get(edp, m);
40
+ if (typeof v !== 'function')
41
+ return v;
42
+ if (!WRITES.has(m))
43
+ return v.bind(edp);
44
+ return async (...args) => {
45
+ const result = await v.apply(edp, args);
46
+ await refreshLive(entity.key);
47
+ return result;
48
+ };
49
+ },
50
+ });
51
+ },
52
+ });
53
+ if (BROWSER)
54
+ remult.dataProvider = withRefresh(remult.dataProvider);
55
+ const mapped = (inner, arg, map) => new Proxy(inner, {
56
+ get(t, p) {
57
+ if (p === 'then') {
58
+ track(arg);
59
+ // read now: Kit pins the query in the reactive context of this getter
60
+ const then = t.then;
61
+ return (ok, err) => then.call(t, map).then(ok, err);
62
+ }
63
+ if (p === 'current')
64
+ return t.current === undefined ? undefined : map(t.current);
65
+ const v = Reflect.get(t, p);
66
+ return typeof v === 'function' ? v.bind(t) : v;
67
+ },
68
+ });
69
+ /**
70
+ * `repo(E)` for reactive reads: SSR goes through the API (rules apply, as the
71
+ * current user) and hydration sends no query.
72
+ *
73
+ * ```svelte
74
+ * <script lang="ts">
75
+ * import { repo } from 'remult'
76
+ *
77
+ * import { queryRepo } from 'firstly/svelte/query'
78
+ *
79
+ * let { params } = $props()
80
+ *
81
+ * let tasks = $derived(await queryRepo(Task).find({ where: { slug: params.slug } }))
82
+ * </script>
83
+ *
84
+ * {#each tasks as task (task.id)}
85
+ * <button onclick={() => repo(Task).update(task.id, { done: true })}>{task.title}</button>
86
+ * {/each}
87
+ * ```
88
+ *
89
+ * Plain `repo(E)` writes in the browser refresh the queries of `E` on screen.
90
+ * To do so, importing this wraps `remult.dataProvider` in the browser. Call
91
+ * `.refresh()` for what it can't see: BackendMethods, `include`d entities and
92
+ * other users' writes.
93
+ *
94
+ * A failed refresh reaches the error page, like any `.refresh()` in Kit.
95
+ * Long filters can exceed the URL limit: arguments travel in a GET.
96
+ *
97
+ * @experimental Needs `experimental.remoteFunctions` and
98
+ * `compilerOptions.experimental.async`.
99
+ */
100
+ export const queryRepo = (entity) => {
101
+ const r = repo(entity);
102
+ const run = (method, options, map) => {
103
+ const where = options?.where && Filter.entityFilterToJson(r.metadata, options.where);
104
+ const arg = { key: r.metadata.key, method, options: { ...options, where } };
105
+ return mapped(read(arg), arg, map);
106
+ };
107
+ return {
108
+ find: (options) => run('find', options, (rows) => r.fromJson(rows)),
109
+ findFirst: (where, options) => run('find', { ...options, where, limit: 1 }, (rows) => r.fromJson(rows)[0]),
110
+ count: (where) => run('count', { where }, (n) => n),
111
+ };
112
+ };
@@ -0,0 +1,7 @@
1
+ import { type FindOptions } from 'remult';
2
+ export type RepoRead = {
3
+ key: string;
4
+ method: 'find' | 'count';
5
+ options?: FindOptions<any>;
6
+ };
7
+ export declare const read: import("$app/server").RemoteQueryFunction<RepoRead, any, RepoRead>;
@@ -0,0 +1,27 @@
1
+ import { error } from '@sveltejs/kit';
2
+ import { Filter, repo } from 'remult';
3
+ import { getRequestEvent, query } from '$app/server';
4
+ import { repoFetch } from '../remultApiLoad.js';
5
+ const READS = new Set(['find', 'count']);
6
+ let byKey;
7
+ // TODO(remult): needs a public way to resolve an entity by key
8
+ const allEntities = () => globalThis[Symbol.for('remult-static1')]?.allEntities ?? [];
9
+ export const read = query('unchecked', async ({ key, method, options }) => {
10
+ byKey ??= new Map(allEntities().map((e) => [repo(e).metadata.key, e]));
11
+ const entity = byKey.get(key);
12
+ // reachable by anyone: only known entities, only reads
13
+ if (!entity || !READS.has(method))
14
+ error(404);
15
+ // never `repo()`: on the server it reads the DB without API rules
16
+ const r = repoFetch(getRequestEvent().fetch)(entity);
17
+ const where = options?.where && Filter.entityFilterFromJson(r.metadata, options.where);
18
+ try {
19
+ if (method === 'count')
20
+ return await r.count(where);
21
+ // toJson keeps included relations as class instances
22
+ return JSON.parse(JSON.stringify(r.toJson(await r.find({ ...options, where }))));
23
+ }
24
+ catch (e) {
25
+ error(e?.status ?? e?.httpStatusCode ?? 500, e?.message ?? 'Error');
26
+ }
27
+ });
@@ -10,8 +10,16 @@ export type Repo = typeof globalRepo;
10
10
  *
11
11
  * Name the result so the intent shows at every query:
12
12
  * ```ts
13
- * const repoClient = repoFetch(event.fetch)
14
- * await repoClient(Task).find()
13
+ * // +page.ts
14
+ * import { repoFetch } from 'firstly/svelte'
15
+ *
16
+ * import type { PageLoad } from './$types'
17
+ *
18
+ * export const load: PageLoad = async (event) => {
19
+ * const repoClient = repoFetch(event.fetch)
20
+ * const tasks = await repoClient(Task).find()
21
+ * return { tasks }
22
+ * }
15
23
  * ```
16
24
  *
17
25
  * TODO(remult): framework-agnostic (only needs a fetch) - candidate to live in
@@ -22,16 +30,20 @@ export declare function repoFetch(fetch: typeof globalThis.fetch, options?: {
22
30
  }): Repo;
23
31
  /**
24
32
  * 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.
33
+ * Works for universal (`+page.ts`) AND server (`+page.server.ts`) loads:
34
+ * `satisfies` types the event, including `cookies` and `locals` in a server load.
26
35
  *
27
36
  * ```ts
28
- * export const load = loadRepo(async (repoClient, { params }) => ({
29
- * task: await repoClient(Task).findFirst({ id: params.id }),
30
- * })) satisfies PageLoad // or PageServerLoad
31
- * ```
37
+ * // +page.ts
38
+ * import { loadRepo } from 'firstly/svelte'
32
39
  *
33
- * For server-only event members (`cookies`, `locals`, ...) annotate the event:
34
- * `loadRepo(async (repoClient, event: ServerLoadEvent) => ...)`.
40
+ * import type { PageLoad } from './$types'
41
+ *
42
+ * export const load = loadRepo(async (repoClient, { params }) => {
43
+ * const task = await repoClient(Task).findFirst({ id: params.id })
44
+ * return { task }
45
+ * }) satisfies PageLoad // or PageServerLoad
46
+ * ```
35
47
  *
36
48
  * TODO(remult): candidate to live in remult - only the event default is
37
49
  * SvelteKit-specific, the constraint itself is structural (`{ fetch }`).
@@ -8,8 +8,16 @@ import { repo as globalRepo, remult, RestDataProvider } from 'remult';
8
8
  *
9
9
  * Name the result so the intent shows at every query:
10
10
  * ```ts
11
- * const repoClient = repoFetch(event.fetch)
12
- * await repoClient(Task).find()
11
+ * // +page.ts
12
+ * import { repoFetch } from 'firstly/svelte'
13
+ *
14
+ * import type { PageLoad } from './$types'
15
+ *
16
+ * export const load: PageLoad = async (event) => {
17
+ * const repoClient = repoFetch(event.fetch)
18
+ * const tasks = await repoClient(Task).find()
19
+ * return { tasks }
20
+ * }
13
21
  * ```
14
22
  *
15
23
  * TODO(remult): framework-agnostic (only needs a fetch) - candidate to live in
@@ -25,16 +33,20 @@ export function repoFetch(fetch, options) {
25
33
  }
26
34
  /**
27
35
  * 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.
36
+ * Works for universal (`+page.ts`) AND server (`+page.server.ts`) loads:
37
+ * `satisfies` types the event, including `cookies` and `locals` in a server load.
29
38
  *
30
39
  * ```ts
31
- * export const load = loadRepo(async (repoClient, { params }) => ({
32
- * task: await repoClient(Task).findFirst({ id: params.id }),
33
- * })) satisfies PageLoad // or PageServerLoad
34
- * ```
40
+ * // +page.ts
41
+ * import { loadRepo } from 'firstly/svelte'
35
42
  *
36
- * For server-only event members (`cookies`, `locals`, ...) annotate the event:
37
- * `loadRepo(async (repoClient, event: ServerLoadEvent) => ...)`.
43
+ * import type { PageLoad } from './$types'
44
+ *
45
+ * export const load = loadRepo(async (repoClient, { params }) => {
46
+ * const task = await repoClient(Task).findFirst({ id: params.id })
47
+ * return { task }
48
+ * }) satisfies PageLoad // or PageServerLoad
49
+ * ```
38
50
  *
39
51
  * TODO(remult): candidate to live in remult - only the event default is
40
52
  * SvelteKit-specific, the constraint itself is structural (`{ fetch }`).
@@ -1,4 +1,4 @@
1
- import type { Handle } from '@sveltejs/kit';
1
+ import type { Handle } from '@sveltejs/kit/hooks';
2
2
  /**
3
3
  * `hooks.server.js` handle that sets `Cache-Control` so browsers and CDNs cache
4
4
  * correctly across deploys - the server-side twin of `withStaleDeployReload`.
@@ -16,7 +16,7 @@ import type { Handle } from '@sveltejs/kit';
16
16
  * ```js
17
17
  * // src/hooks.server.js
18
18
  * import { sequence } from '@sveltejs/kit/hooks'
19
- * import { handleCaching } from './'
19
+ * import { handleCaching } from 'firstly/svelte/server'
20
20
  *
21
21
  * export const handle = sequence(handleCaching, ...rest)
22
22
  * ```
@@ -17,7 +17,7 @@ const STATIC_ASSET_RE = /\.(js|css|ico|png|jpg|jpeg|gif|svg|webp|avif|woff2?|ttf
17
17
  * ```js
18
18
  * // src/hooks.server.js
19
19
  * import { sequence } from '@sveltejs/kit/hooks'
20
- * import { handleCaching } from './'
20
+ * import { handleCaching } from 'firstly/svelte/server'
21
21
  *
22
22
  * export const handle = sequence(handleCaching, ...rest)
23
23
  * ```
@@ -2,7 +2,6 @@
2
2
  import { BROWSER } from 'esm-env'
3
3
 
4
4
  import './LibIcon'
5
-
6
5
  import { LibIcon_Empty } from './LibIcon'
7
6
 
8
7
  /**
package/esm/vite/index.js CHANGED
@@ -25,15 +25,18 @@ export function firstly(options) {
25
25
  // })
26
26
  // },
27
27
  // })
28
+ p.push({
29
+ name: 'vite-plugin-firstly',
30
+ // Oxc leaves remult's decorators raw unless it finds experimentalDecorators in a tsconfig
31
+ config: () => ({ oxc: { decorator: { legacy: true } } }),
32
+ });
28
33
  p.push(kitRoutes({
29
- ...(options?.kitRoutes ?? {}),
30
- ...{
31
- format_page_route_id: true,
32
- logs: {
33
- post_update_run: false,
34
- update: false,
35
- ...options?.kitRoutes?.logs,
36
- },
34
+ format_page_route_id: true,
35
+ ...options?.kitRoutes,
36
+ logs: {
37
+ post_update_run: false,
38
+ update: false,
39
+ ...options?.kitRoutes?.logs,
37
40
  },
38
41
  }));
39
42
  p.push(stripper({
@@ -50,7 +53,12 @@ export function firstly(options) {
50
53
  },
51
54
  ],
52
55
  debug: options?.stripper?.debug ?? false,
53
- nullify: options?.stripper?.nullify ?? ['$env/static/private', '$env/dynamic/private'],
56
+ nullify: options?.stripper?.nullify ?? [
57
+ '$app/env/private',
58
+ // deprecated by Kit 3, but still resolvable
59
+ '$env/static/private',
60
+ '$env/dynamic/private',
61
+ ],
54
62
  }));
55
63
  return p;
56
64
  }