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.
- package/CHANGELOG.md +22 -0
- package/esm/changeLog/index.d.ts +1 -0
- package/esm/changeLog/server/index.d.ts +2 -2
- package/esm/changeLog/server/index.js +2 -2
- package/esm/core/FF_Allow.d.ts +3 -3
- package/esm/core/FF_Allow.js +3 -3
- package/esm/core/FF_Filter.d.ts +3 -3
- package/esm/core/FF_Filter.js +3 -3
- package/esm/core/FF_Validators.d.ts +2 -2
- package/esm/core/FF_Validators.js +2 -2
- package/esm/core/index.d.ts +1 -0
- package/esm/core/index.js +1 -1
- package/esm/cron/server/index.d.ts +1 -1
- package/esm/cron/server/index.js +1 -1
- package/esm/mail/ui/WriteMail.svelte +1 -1
- package/esm/sqlAdmin/index.d.ts +1 -0
- package/esm/sqlAdmin/index.js +1 -0
- package/esm/sqlAdmin/presetQueries.d.ts +8 -0
- package/esm/sqlAdmin/presetQueries.js +26 -0
- package/esm/sqlAdmin/server/index.d.ts +2 -2
- package/esm/sqlAdmin/server/index.js +2 -2
- package/esm/sqlAdmin/ui/SqlAdmin.svelte +12 -41
- package/esm/sqlAdmin/ui/SqlAdmin.svelte.d.ts +5 -1
- package/esm/sqlAdmin/ui/SqlDrift.svelte +41 -41
- package/esm/svelte/FF_DialogManager.svelte +4 -4
- package/esm/svelte/FF_PromptDefault.svelte +6 -6
- package/esm/svelte/class/SP.svelte.d.ts +1 -0
- package/esm/svelte/class/SP.svelte.js +14 -15
- package/esm/svelte/dialog.svelte.d.ts +1 -1
- package/esm/svelte/grid/FF_Grid.svelte +11 -8
- package/esm/svelte/grid/buildCells.js +6 -0
- package/esm/svelte/grid/cellTypes.d.ts +4 -0
- package/esm/svelte/handleError.d.ts +8 -2
- package/esm/svelte/handleError.js +24 -2
- package/esm/svelte/index.d.ts +1 -1
- package/esm/svelte/index.js +1 -1
- package/esm/svelte/query/index.svelte.d.ts +40 -0
- package/esm/svelte/query/index.svelte.js +112 -0
- package/esm/svelte/query/read.remote.d.ts +7 -0
- package/esm/svelte/query/read.remote.js +27 -0
- package/esm/svelte/remultApiLoad.d.ts +21 -9
- package/esm/svelte/remultApiLoad.js +21 -9
- package/esm/svelte/server/handleCaching.d.ts +2 -2
- package/esm/svelte/server/handleCaching.js +1 -1
- package/esm/svelte/ui/Icon.svelte +0 -1
- package/esm/vite/index.js +11 -1
- package/package.json +8 -3
- 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,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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* })) satisfies PageLoad // or PageServerLoad
|
|
31
|
-
* ```
|
|
37
|
+
* // +page.ts
|
|
38
|
+
* import { loadRepo } from 'firstly/svelte'
|
|
32
39
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* })) satisfies PageLoad // or PageServerLoad
|
|
34
|
-
* ```
|
|
40
|
+
* // +page.ts
|
|
41
|
+
* import { loadRepo } from 'firstly/svelte'
|
|
35
42
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
* ```
|
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 ?? [
|
|
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.
|
|
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
|
+
"node": ">=22.17"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@sveltejs/kit": "
|
|
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
|
-
};
|