firstly 0.10.8 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) 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/index.d.ts +1 -0
  17. package/esm/sqlAdmin/index.js +1 -0
  18. package/esm/sqlAdmin/presetQueries.d.ts +8 -0
  19. package/esm/sqlAdmin/presetQueries.js +26 -0
  20. package/esm/sqlAdmin/server/index.d.ts +2 -2
  21. package/esm/sqlAdmin/server/index.js +2 -2
  22. package/esm/sqlAdmin/ui/SqlAdmin.svelte +12 -41
  23. package/esm/sqlAdmin/ui/SqlAdmin.svelte.d.ts +5 -1
  24. package/esm/sqlAdmin/ui/SqlDrift.svelte +41 -41
  25. package/esm/svelte/FF_DialogManager.svelte +4 -4
  26. package/esm/svelte/FF_PromptDefault.svelte +6 -6
  27. package/esm/svelte/class/SP.svelte.d.ts +1 -0
  28. package/esm/svelte/class/SP.svelte.js +14 -15
  29. package/esm/svelte/dialog.svelte.d.ts +1 -1
  30. package/esm/svelte/grid/FF_Grid.svelte +11 -8
  31. package/esm/svelte/grid/buildCells.js +6 -0
  32. package/esm/svelte/grid/cellTypes.d.ts +4 -0
  33. package/esm/svelte/handleError.d.ts +8 -2
  34. package/esm/svelte/handleError.js +24 -2
  35. package/esm/svelte/index.d.ts +1 -1
  36. package/esm/svelte/index.js +1 -1
  37. package/esm/svelte/query/index.svelte.d.ts +40 -0
  38. package/esm/svelte/query/index.svelte.js +112 -0
  39. package/esm/svelte/query/read.remote.d.ts +7 -0
  40. package/esm/svelte/query/read.remote.js +27 -0
  41. package/esm/svelte/remultApiLoad.d.ts +21 -9
  42. package/esm/svelte/remultApiLoad.js +21 -9
  43. package/esm/svelte/server/handleCaching.d.ts +2 -2
  44. package/esm/svelte/server/handleCaching.js +1 -1
  45. package/esm/svelte/ui/Icon.svelte +0 -1
  46. package/esm/vite/index.js +11 -1
  47. package/package.json +8 -3
  48. package/bin/args.d.ts +0 -13
@@ -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,6 +25,11 @@ 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
34
  ...(options?.kitRoutes ?? {}),
30
35
  ...{
@@ -50,7 +55,12 @@ export function firstly(options) {
50
55
  },
51
56
  ],
52
57
  debug: options?.stripper?.debug ?? false,
53
- nullify: options?.stripper?.nullify ?? ['$env/static/private', '$env/dynamic/private'],
58
+ nullify: options?.stripper?.nullify ?? [
59
+ '$app/env/private',
60
+ // deprecated by Kit 3, but still resolvable
61
+ '$env/static/private',
62
+ '$env/dynamic/private',
63
+ ],
54
64
  }));
55
65
  return p;
56
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.10.8",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "bin": {
@@ -15,10 +15,10 @@
15
15
  },
16
16
  "license": "MIT",
17
17
  "engines": {
18
- "node": ">=18"
18
+ "node": ">=22.17"
19
19
  },
20
20
  "peerDependencies": {
21
- "@sveltejs/kit": ">=1.0.0 <4.0.0 || ^3.0.0-next.0",
21
+ "@sveltejs/kit": "^3.0.0",
22
22
  "remult": ">=3.3.7",
23
23
  "svelte": ">=5"
24
24
  },
@@ -59,6 +59,11 @@
59
59
  "svelte": "./esm/svelte/index.js",
60
60
  "default": "./esm/svelte/index.js"
61
61
  },
62
+ "./svelte/query": {
63
+ "types": "./esm/svelte/query/index.svelte.d.ts",
64
+ "svelte": "./esm/svelte/query/index.svelte.js",
65
+ "default": "./esm/svelte/query/index.svelte.js"
66
+ },
62
67
  "./svelte/server": {
63
68
  "types": "./esm/svelte/server/index.d.ts",
64
69
  "default": "./esm/svelte/server/index.js"
package/bin/args.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @param {string[]} argv
3
- * @returns {{ help: boolean, json: boolean, raw: boolean, origin?: string, apiPath?: string, sql: string, error?: string }}
4
- */
5
- export function parseArgs(argv: string[]): {
6
- help: boolean;
7
- json: boolean;
8
- raw: boolean;
9
- origin?: string;
10
- apiPath?: string;
11
- sql: string;
12
- error?: string;
13
- };