firstly 0.4.5 → 0.5.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 +26 -0
- package/esm/core/FF_Filter.d.ts +17 -2
- package/esm/core/FF_Filter.js +18 -2
- package/esm/core/containsWords.d.ts +13 -0
- package/esm/core/containsWords.js +27 -0
- package/esm/core/httpClientStack.d.ts +18 -0
- package/esm/core/httpClientStack.js +26 -0
- package/esm/formats/index.d.ts +1 -1
- package/esm/formats/index.js +1 -1
- package/esm/formats/strings.d.ts +7 -0
- package/esm/formats/strings.js +12 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +1 -0
- package/esm/svelte/DemoGrid.svelte +167 -0
- package/esm/svelte/DemoGrid.svelte.d.ts +40 -0
- package/esm/svelte/FF_Repo.svelte.d.ts +178 -56
- package/esm/svelte/FF_Repo.svelte.js +273 -161
- package/esm/svelte/index.d.ts +5 -1
- package/esm/svelte/index.js +3 -1
- package/esm/svelte/infiniteScroll.d.ts +31 -0
- package/esm/svelte/infiniteScroll.js +40 -0
- package/package.json +3 -3
|
@@ -1,18 +1,83 @@
|
|
|
1
|
-
import { type ClassType, type
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type ClassType, type EntityFilter, type EntityMetadata, type EntityOrderBy, type GroupByOptions, type GroupByResult, type MembersOnly, type MembersToInclude, type NumericKeys, type QueryOptions, type Repository } from 'remult';
|
|
2
|
+
/**
|
|
3
|
+
* `ffRepo` - thin reactive wrapper around a Remult `repo`, exposing its results as
|
|
4
|
+
* Svelte runes. Pick the mode with a verb:
|
|
5
|
+
*
|
|
6
|
+
* ffRepo(E).load(() => ({ where })) // load - one-shot list + refresh()
|
|
7
|
+
* ffRepo(E).listen(() => ({ where })) // live - liveQuery, auto-updates
|
|
8
|
+
* ffRepo(E).paginate(() => ({ where })) // paginate - more() / hasNextPage / aggregates
|
|
9
|
+
* ffRepo(E).one(() => ({ where })) // one - reactive single record in `item`
|
|
10
|
+
*
|
|
11
|
+
* Two surfaces, one rule: anything NOT under `.repo` is reactive (a verb returns a
|
|
12
|
+
* runes handle; that handle's writes sync its own state). Anything under `.repo` is
|
|
13
|
+
* the plain remult repo - imperative, returns Promises, touches no runes state.
|
|
14
|
+
*
|
|
15
|
+
* The options getter is reactive: change `where` (e.g. a search box), `orderBy`,
|
|
16
|
+
* `enabled`, etc. and the query re-runs - `listen` re-subscribes (the old
|
|
17
|
+
* subscription is torn down), `load`/`paginate`/`one` re-fetch and ignore any
|
|
18
|
+
* stale in-flight response. `orderBy` defaults to the entity's `defaultOrderBy`.
|
|
19
|
+
*
|
|
20
|
+
* Getter hygiene: read SvelteKit load `data` through a `$derived`
|
|
21
|
+
* (`const did = $derived(data.targetDid)`), never raw inside the getter. A derived
|
|
22
|
+
* only propagates on value change, so a parent layout load revalidating - e.g. an
|
|
23
|
+
* SP URL write re-running url-dependent loads on every filter - hands you a new
|
|
24
|
+
* `data` object but does NOT re-fetch unless the value actually changed; a real
|
|
25
|
+
* change (route param switch) still does. Reading `data.x` raw re-fetches on every
|
|
26
|
+
* revalidation (a duplicate request per filter).
|
|
27
|
+
*
|
|
28
|
+
* `enabled: false` skips the query entirely (keeps the last result) and runs it
|
|
29
|
+
* the moment it flips true - use it for search-min-length, tab visibility,
|
|
30
|
+
* dependent queries, or a manual button trigger.
|
|
31
|
+
*
|
|
32
|
+
* Writes: only the record handle (`one` / `create()`) writes - argless `save()` / `delete()`
|
|
33
|
+
* act on its `item` and re-sync it. List handles (`load` / `listen` / `paginate`) don't write;
|
|
34
|
+
* go through `.repo` (the plain remult repo: `insert` / `update` / `save` / `delete` /
|
|
35
|
+
* `deleteMany`), then reflect it in a `load` / `paginate` list with the local reconcilers
|
|
36
|
+
* (`addItem` / `updateItem` / `removeItem`) - a `listen` list re-syncs itself via the liveQuery.
|
|
37
|
+
* A failed write fills `error` and re-throws.
|
|
38
|
+
*
|
|
39
|
+
* The factory's return type is mode-specific, so e.g. `.more()` doesn't exist on
|
|
40
|
+
* a `listen()` handle. Methods also throw if reached via a cast in the wrong mode.
|
|
41
|
+
*
|
|
42
|
+
* Reactive vs imperative: the reactive verbs take a *getter* (`() => ({ ... })`) and
|
|
43
|
+
* return a runes handle; they build an `$effect`, so they must be created during
|
|
44
|
+
* component init. For a one-off read/write in a click handler / async fn (no runes
|
|
45
|
+
* context) go through `.repo` (plain remult): `ffRepo(E).repo.findFirst(where)`,
|
|
46
|
+
* `ffRepo(E).repo.findId(id)`, `ffRepo(E).repo.find(...)`.
|
|
47
|
+
*
|
|
48
|
+
* Tip: prefer `.paginate()` whenever you want a total - it returns `aggregates.$count`
|
|
49
|
+
* for free in the same request. `load`/`listen`/`one` don't count; for a one-off count
|
|
50
|
+
* use `ffRepo(E).repo.count(where)`.
|
|
51
|
+
*/
|
|
52
|
+
/** The aggregate request shape - remult's `GroupByOptions` minus the grouping/paging keys. */
|
|
53
|
+
export type AggregateOptions<Entity> = Omit<GroupByOptions<Entity, never, NumericKeys<Entity>[], NumericKeys<Entity>[], (keyof MembersOnly<Entity>)[], (keyof MembersOnly<Entity>)[], (keyof MembersOnly<Entity>)[]>, 'group' | 'orderBy' | 'where' | 'limit' | 'page'>;
|
|
54
|
+
/**
|
|
55
|
+
* Remult `QueryOptions` plus the `aggregate` request shape. Exported for callers
|
|
56
|
+
* that build query options outside `ffRepo` (e.g. a generic table component).
|
|
57
|
+
*/
|
|
58
|
+
export type QueryOptionsHelper<Entity> = QueryOptions<Entity> & {
|
|
59
|
+
aggregate?: AggregateOptions<Entity>;
|
|
4
60
|
};
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
61
|
+
export type FF_RepoOptions<Entity> = {
|
|
62
|
+
where?: EntityFilter<Entity>;
|
|
63
|
+
orderBy?: EntityOrderBy<Entity>;
|
|
64
|
+
/** paginate: rows per page (default 25). */
|
|
65
|
+
pageSize?: number;
|
|
66
|
+
/** find/one/live: cap the rows returned. No default - returns every matching row. */
|
|
67
|
+
limit?: number;
|
|
68
|
+
include?: MembersToInclude<Entity>;
|
|
69
|
+
/** When false, the query is skipped (last result kept) until it flips true. */
|
|
70
|
+
enabled?: boolean;
|
|
71
|
+
/** Aggregations to compute alongside the page (paginate mode only). `$count` is always returned. */
|
|
72
|
+
aggregate?: AggregateOptions<Entity>;
|
|
11
73
|
};
|
|
12
|
-
type
|
|
13
|
-
|
|
74
|
+
type Getter<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = () => O;
|
|
75
|
+
/** `$count` is always present; richer keys appear only for the requested `aggregate`. */
|
|
76
|
+
type EmptyAggregateResult = {
|
|
77
|
+
$count: number;
|
|
14
78
|
};
|
|
15
|
-
|
|
79
|
+
/** The typed aggregate result for a given options object (paginate mode). */
|
|
80
|
+
type ExtractAggregateResult<Entity, O extends FF_RepoOptions<Entity>> = O extends {
|
|
16
81
|
aggregate: infer A;
|
|
17
82
|
} ? GroupByResult<Entity, never, A extends {
|
|
18
83
|
sum?: infer S;
|
|
@@ -20,57 +85,114 @@ type ExtractAggregateResult<Entity, Options extends QueryOptionsHelper<Entity>>
|
|
|
20
85
|
avg?: infer V;
|
|
21
86
|
} ? (V extends NumericKeys<Entity>[] ? V : never) : never, A extends {
|
|
22
87
|
min?: infer M;
|
|
23
|
-
} ? (M extends
|
|
88
|
+
} ? (M extends NumericKeys<Entity>[] ? M : never) : never, A extends {
|
|
24
89
|
max?: infer X;
|
|
25
|
-
} ? (X extends
|
|
90
|
+
} ? (X extends NumericKeys<Entity>[] ? X : never) : never, A extends {
|
|
26
91
|
distinctCount?: infer D;
|
|
27
92
|
} ? D extends (keyof MembersOnly<Entity>)[] ? D : never : never> : EmptyAggregateResult;
|
|
28
|
-
export
|
|
93
|
+
export type FF_RepoLoading = {
|
|
94
|
+
init: boolean;
|
|
95
|
+
fetching: boolean;
|
|
96
|
+
more: boolean;
|
|
97
|
+
saving: boolean;
|
|
98
|
+
deleting: boolean;
|
|
99
|
+
};
|
|
100
|
+
type Mode = 'live' | 'load' | 'paginate' | 'one';
|
|
101
|
+
/**
|
|
102
|
+
* The reactive handle implementation. Not exported directly - consumers use a per-mode
|
|
103
|
+
* alias (`FF_RepoLoad`/`FF_RepoLive`/`FF_RepoPaginate`/`FF_RepoOne`) or the umbrella
|
|
104
|
+
* union `FF_Repo` (any mode). Each verb returns the Omit'd per-mode view of this.
|
|
105
|
+
*/
|
|
106
|
+
declare class FF_RepoHandle<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> {
|
|
29
107
|
#private;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
metadata: Repository<Entity>['metadata'];
|
|
33
|
-
loading: Loading;
|
|
34
|
-
items: Entity[] | undefined;
|
|
35
|
-
aggregates: ExtractAggregateResult<Entity, QueryOptions> | undefined;
|
|
36
|
-
hasNextPage: boolean | undefined;
|
|
108
|
+
items: Entity[];
|
|
109
|
+
/** Single-record slot: the loaded row in `one` mode, or a create/edit draft (see `create`). */
|
|
37
110
|
item: Entity | undefined;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
skipAutoFetch?: boolean;
|
|
49
|
-
};
|
|
50
|
-
}) & {
|
|
51
|
-
item?: Entity;
|
|
52
|
-
});
|
|
53
|
-
find(options: FindOptions<Entity>): Promise<Entity[] | undefined>;
|
|
54
|
-
query(options: Pick<QueryOptionsHelper<Entity>, 'where' | 'orderBy'>): Promise<{
|
|
55
|
-
items: Entity[];
|
|
56
|
-
aggregates: ExtractAggregateResult<Entity, QueryOptions>;
|
|
57
|
-
hasNextPage: boolean;
|
|
58
|
-
} | undefined>;
|
|
59
|
-
queryMore(): Promise<{
|
|
60
|
-
items: Entity[] | undefined;
|
|
61
|
-
aggregates: ExtractAggregateResult<Entity, QueryOptions>;
|
|
62
|
-
hasNextPage: boolean;
|
|
63
|
-
} | undefined>;
|
|
111
|
+
loading: FF_RepoLoading;
|
|
112
|
+
error: string | undefined;
|
|
113
|
+
hasNextPage: boolean;
|
|
114
|
+
/** Aggregations for the whole query (paginate mode). `aggregates.$count` is the total row count. */
|
|
115
|
+
aggregates: ExtractAggregateResult<Entity, O> | undefined;
|
|
116
|
+
constructor(r: Repository<Entity>, opts: Getter<Entity, O>, mode: Mode);
|
|
117
|
+
/** Re-run the current query (load/paginate/one), back to the first page. */
|
|
118
|
+
refresh(): Promise<void>;
|
|
119
|
+
/** Load and append the next page (paginate mode). */
|
|
120
|
+
more(): Promise<void>;
|
|
64
121
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
122
|
+
* Run `fn` once - the first time a row exists (`items[0]`).
|
|
123
|
+
*
|
|
124
|
+
* The point: seed editable UI state from the latest row WITHOUT a live query
|
|
125
|
+
* clobbering in-progress edits. It fires a single time, on the first non-empty
|
|
126
|
+
* result, and never again - later ticks (an edit, a delete, a re-sort) are
|
|
127
|
+
* ignored. Empty snapshots are skipped (a liveQuery often emits one before the
|
|
128
|
+
* data lands; there is nothing to seed from an empty result).
|
|
129
|
+
*
|
|
130
|
+
* For pure derived state prefer `$derived`; reach for `onFirst` only when the
|
|
131
|
+
* seed must become independently editable (a draft the user then mutates).
|
|
132
|
+
*
|
|
133
|
+
* ```svelte
|
|
134
|
+
* const list = ffRepo(Plan).listen(() => ({ where: { ownerDid } }))
|
|
135
|
+
* let draft = $state({ title: '' })
|
|
136
|
+
* list.onFirst((latest) => (draft.title = latest.title)) // seed once, then edit freely
|
|
137
|
+
* ```
|
|
67
138
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
aggregates: ExtractAggregateResult<Entity, QueryOptions>;
|
|
71
|
-
hasNextPage: boolean;
|
|
72
|
-
} | undefined>;
|
|
139
|
+
onFirst(fn: (latest: Entity) => void): void;
|
|
140
|
+
/** Create a new unsaved entity into the `item` slot (for an edit form). */
|
|
73
141
|
create(...args: Parameters<Repository<Entity>['create']>): Entity;
|
|
74
|
-
|
|
142
|
+
/** Save the current `item` (from `one` / `create()`). To save a specific row, use `.repo.save(row)`. */
|
|
143
|
+
save(): Promise<Entity>;
|
|
144
|
+
/** Delete the current `item`. To delete a specific row/id, use `.repo.delete(idOrRow)`. */
|
|
145
|
+
delete(): Promise<void>;
|
|
146
|
+
/** Insert into `items` at `top` (default) / `bottom` / an index (`-1` = last). +1 to `$count`. */
|
|
147
|
+
addItem(item: Entity, options?: {
|
|
148
|
+
at?: 'top' | 'bottom' | number;
|
|
149
|
+
}): void;
|
|
150
|
+
/** Replace the row whose id matches `item`'s id (no `$count` change). */
|
|
151
|
+
updateItem(item: Entity): void;
|
|
152
|
+
/** Drop the matching row (pass an id or the item). -1 to `$count`. */
|
|
153
|
+
removeItem(idOrItem: Parameters<Repository<Entity>['delete']>[0]): void;
|
|
154
|
+
/**
|
|
155
|
+
* The entity's remult metadata - the single escape hatch for everything not on
|
|
156
|
+
* this handle: permissions (`apiInsertAllowed()`, `apiUpdateAllowed(item)`,
|
|
157
|
+
* `apiDeleteAllowed(item)`, `apiReadAllowed`), `fields`, `idMetadata`, `options`,
|
|
158
|
+
* `key`. Reflects the current `remult.user`.
|
|
159
|
+
*/
|
|
160
|
+
get meta(): EntityMetadata<Entity>;
|
|
161
|
+
/** Escape hatch to the underlying repo (count, findId, upsert, projections, ...). */
|
|
162
|
+
get repo(): Repository<Entity>;
|
|
75
163
|
}
|
|
164
|
+
/** load: one-shot list (`refresh()` to re-run) - a read+reconcile view. No paging/aggregates, and no `item`/`save`/`delete`/`create` (edit via `one`, write via `.repo`). */
|
|
165
|
+
export type FF_RepoLoad<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'more' | 'hasNextPage' | 'aggregates' | 'item' | 'save' | 'delete' | 'create'>;
|
|
166
|
+
/** live: reactive subscription, auto-updates - a read view. No refresh/paging/aggregates/reconcilers (the liveQuery does it), and no `item`/`save`/`delete`/`create`. */
|
|
167
|
+
export type FF_RepoLive<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'refresh' | 'more' | 'hasNextPage' | 'aggregates' | 'addItem' | 'updateItem' | 'removeItem' | 'item' | 'save' | 'delete' | 'create'>;
|
|
168
|
+
/** paginate: `more()` / `hasNextPage` / `aggregates` - a read+reconcile view. No `onFirst` (paged ≠ latest), and no `item`/`save`/`delete`/`create`. */
|
|
169
|
+
export type FF_RepoPaginate<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'onFirst' | 'item' | 'save' | 'delete' | 'create'>;
|
|
170
|
+
/** one: a single reactive record in `item`. No paging / aggregates / list reconcilers. */
|
|
171
|
+
export type FF_RepoOne<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'more' | 'hasNextPage' | 'aggregates' | 'addItem' | 'updateItem' | 'removeItem'>;
|
|
172
|
+
/**
|
|
173
|
+
* Umbrella handle type - any mode. Use for a component prop that accepts a
|
|
174
|
+
* `load`/`listen`/`paginate`/`one` handle (`r: FF_Repo<T>`). It exposes the surface
|
|
175
|
+
* common to every mode (`items`/`loading`/`error`/`meta`/`repo`); mode-specific members
|
|
176
|
+
* (`item`/`save`/`delete`/`create` on `one`; `more`/`hasNextPage`/`aggregates`/`refresh`/
|
|
177
|
+
* `onFirst`/`addItem`/`updateItem`/`removeItem`) require the matching per-mode type.
|
|
178
|
+
*/
|
|
179
|
+
export type FF_Repo<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = FF_RepoLoad<Entity, O> | FF_RepoLive<Entity, O> | FF_RepoPaginate<Entity, O> | FF_RepoOne<Entity, O>;
|
|
180
|
+
type StrictGetter<Entity, O extends FF_RepoOptions<Entity>> = () => O & Record<Exclude<keyof O, keyof FF_RepoOptions<Entity>>, never>;
|
|
181
|
+
/**
|
|
182
|
+
* The builder returned by `ffRepo(E)`. Two surfaces only: the reactive verbs
|
|
183
|
+
* (`load`/`listen`/`paginate`/`one`), and `.repo` - the plain remult repo for every
|
|
184
|
+
* imperative read/write (`findFirst`, `findId`, `find`, `insert`, `update`, `save`,
|
|
185
|
+
* `delete`, `count`, `upsert`, ...). `.meta` is a shortcut to `repo.metadata`.
|
|
186
|
+
*/
|
|
187
|
+
export type FF_RepoBuilder<Entity> = {
|
|
188
|
+
load: <O extends FF_RepoOptions<Entity>>(opts: StrictGetter<Entity, O>) => FF_RepoLoad<Entity, O>;
|
|
189
|
+
listen: <O extends FF_RepoOptions<Entity>>(opts: StrictGetter<Entity, O>) => FF_RepoLive<Entity, O>;
|
|
190
|
+
paginate: <O extends FF_RepoOptions<Entity>>(opts: StrictGetter<Entity, O>) => FF_RepoPaginate<Entity, O>;
|
|
191
|
+
one: <O extends FF_RepoOptions<Entity>>(opts: StrictGetter<Entity, O>) => FF_RepoOne<Entity, O>;
|
|
192
|
+
/** The entity's remult metadata (permissions, fields, key). Shortcut to `repo.metadata`. */
|
|
193
|
+
readonly meta: EntityMetadata<Entity>;
|
|
194
|
+
/** The underlying remult repo - every imperative read/write lives here. */
|
|
195
|
+
readonly repo: Repository<Entity>;
|
|
196
|
+
};
|
|
197
|
+
export declare function ffRepo<Entity>(entity: ClassType<Entity>): FF_RepoBuilder<Entity>;
|
|
76
198
|
export {};
|