@svadmin/core 0.50.0 → 0.52.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 (97) hide show
  1. package/package.json +21 -4
  2. package/src/access-control-contract.ts +250 -0
  3. package/src/adapters/casbin.ts +20 -14
  4. package/src/adapters/casl.ts +22 -19
  5. package/src/admin-tool.ts +7 -7
  6. package/src/audit-contract.ts +105 -0
  7. package/src/audit.ts +88 -128
  8. package/src/auth-hooks.svelte.ts +593 -261
  9. package/src/auth-mutation-contract.ts +143 -0
  10. package/src/auth-query-contract.ts +97 -0
  11. package/src/captured-mutation.svelte.ts +45 -0
  12. package/src/chatProvider.svelte.ts +1 -1
  13. package/src/command-contract.ts +195 -0
  14. package/src/context.svelte.ts +217 -205
  15. package/src/contract-context.svelte.ts +37 -0
  16. package/src/contract-form.svelte.ts +566 -0
  17. package/src/create-contract.ts +56 -0
  18. package/src/create-hooks.svelte.ts +347 -0
  19. package/src/create-many-contract.ts +90 -0
  20. package/src/create-many-hooks.svelte.ts +396 -0
  21. package/src/data-transfer.svelte.ts +1 -215
  22. package/src/defined-options.ts +50 -0
  23. package/src/delete-contract.ts +50 -0
  24. package/src/delete-hooks.svelte.ts +403 -0
  25. package/src/delete-many-contract.ts +60 -0
  26. package/src/delete-many-hooks.svelte.ts +402 -0
  27. package/src/enterprise.ts +4 -4
  28. package/src/export-format.ts +16 -14
  29. package/src/export-hooks.svelte.ts +191 -0
  30. package/src/feature-gate-contract.ts +53 -0
  31. package/src/hook-utils.svelte.ts +53 -105
  32. package/src/hooks.svelte.ts +19 -776
  33. package/src/http.ts +88 -87
  34. package/src/i18n.svelte.ts +56 -2
  35. package/src/import-contract.ts +106 -0
  36. package/src/import-hooks.svelte.ts +310 -0
  37. package/src/index.ts +77 -30
  38. package/src/inferencer-graphql.ts +7 -6
  39. package/src/infinite-query.svelte.ts +228 -0
  40. package/src/invalidation-contract.ts +26 -0
  41. package/src/live-sse.ts +91 -127
  42. package/src/live-subscription.svelte.ts +130 -0
  43. package/src/live-transport.ts +127 -0
  44. package/src/live-websocket.ts +85 -118
  45. package/src/live.svelte.ts +66 -68
  46. package/src/notification.svelte.ts +17 -15
  47. package/src/permission-catalog.ts +121 -0
  48. package/src/permission-hints-contract.ts +48 -0
  49. package/src/permissions.svelte.ts +112 -59
  50. package/src/plain-data.ts +61 -0
  51. package/src/provider-bundle.ts +85 -79
  52. package/src/provider-response.ts +35 -0
  53. package/src/query-hooks.svelte.ts +241 -288
  54. package/src/query-invalidation.ts +172 -0
  55. package/src/query-keys.ts +221 -110
  56. package/src/query-session.svelte.ts +216 -0
  57. package/src/query-snapshot.ts +144 -0
  58. package/src/reactive-projection.ts +53 -0
  59. package/src/record-decoder.ts +63 -0
  60. package/src/resource-binding.svelte.ts +19 -0
  61. package/src/resource-contract.ts +595 -0
  62. package/src/resource-mutation-types.ts +30 -0
  63. package/src/resource-schemas.ts +371 -0
  64. package/src/resource-transport-config.ts +156 -0
  65. package/src/route-parsing.ts +11 -11
  66. package/src/router-provider.ts +64 -59
  67. package/src/router.ts +8 -3
  68. package/src/routing-hooks.svelte.ts +90 -62
  69. package/src/schema-validation.ts +92 -0
  70. package/src/schema.ts +13 -0
  71. package/src/select-options.ts +95 -0
  72. package/src/select-query.svelte.ts +191 -0
  73. package/src/session-query.svelte.ts +88 -0
  74. package/src/strict-command-hooks.svelte.ts +333 -0
  75. package/src/strict-hooks.svelte.ts +198 -0
  76. package/src/table-hooks.svelte.ts +180 -174
  77. package/src/table-state.svelte.ts +117 -117
  78. package/src/task-contract.ts +87 -0
  79. package/src/task-hooks.svelte.ts +193 -149
  80. package/src/task-provider.ts +115 -0
  81. package/src/task-subscription.ts +132 -0
  82. package/src/theme.svelte.ts +155 -154
  83. package/src/toast.svelte.ts +38 -37
  84. package/src/types.ts +55 -108
  85. package/src/unsafe.ts +7 -0
  86. package/src/update-contract.ts +65 -0
  87. package/src/update-hooks.svelte.ts +347 -0
  88. package/src/update-many-contract.ts +79 -0
  89. package/src/update-many-hooks.svelte.ts +373 -0
  90. package/src/url-sync.ts +11 -10
  91. package/src/useCan.ts +96 -31
  92. package/src/useParsed.svelte.ts +18 -14
  93. package/src/useStepsForm.svelte.ts +123 -63
  94. package/src/utility-hooks.svelte.ts +101 -158
  95. package/src/form-hooks.svelte.ts +0 -889
  96. package/src/mutation-hooks.svelte.ts +0 -561
  97. package/src/schema-form.ts +0 -31
@@ -3,821 +3,64 @@
3
3
  // ─── Re-exports from modular hook files ─────────────────────────────
4
4
 
5
5
  export * from './query-hooks.svelte';
6
- export * from './mutation-hooks.svelte';
7
- export * from './form-hooks.svelte';
8
6
  export * from './table-hooks.svelte';
9
7
  export * from './routing-hooks.svelte';
10
8
  export * from './utility-hooks.svelte';
11
9
 
12
10
  // ─── Re-export types from options ─────────────────────────────────
13
- export type { InvalidateScope, OvertimeConfig } from './options.svelte';
11
+ export type { InvalidateScope,OvertimeConfig } from './options.svelte';
14
12
 
15
13
  // ─── Export shared utilities ────────────────────────────────────────
16
14
 
17
- export { createOvertimeTracker, createLiveSubscription } from './hook-utils.svelte';
18
- export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook-utils.svelte';
15
+ export { createOvertimeTracker,createLiveSubscription } from './hook-utils.svelte';
16
+ export type { OvertimeResult,OvertimeOptions,NotificationConfig } from './hook-utils.svelte';
19
17
 
20
18
  // ─── Additional core hooks ─────────────────────────────────────────
21
19
 
22
- import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
23
- import { getAdminOptions } from './options.svelte';
24
20
  import { captureAdminContext } from './context.svelte';
25
- import { dataQueryMatches, queryKeyMatches } from './query-keys';
26
- import type { QueryMatcher } from './query-keys';
27
- import { useParsed } from './useParsed.svelte';
28
- import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification, checkError } from './hook-utils.svelte';
29
- import { invalidateByScopes, publishLiveEvent } from './mutation-hooks.svelte';
30
- import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
31
- import { DeleteManyPartialError, UndoError } from './types';
32
- import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources, MutationMode } from './types';
33
- import type { LiveMode, LiveEvent } from './live.svelte';
34
- import { auditWithProvider } from './audit';
35
- import { useTranslation } from './i18n.svelte';
36
- import { toast } from './toast.svelte';
37
- import type { MutationCallbacks } from './mutation-hooks.svelte';
21
+ import { createOvertimeTracker } from './hook-utils.svelte';
22
+ import type { OvertimeOptions } from './hook-utils.svelte';
23
+ import type { DataProvider } from './types';
38
24
 
39
25
  // ─── useInfiniteList ────────────────────────────────────────────────
40
26
 
41
- export interface UseInfiniteListOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
42
- resource?: KnownResources;
43
- pagination?: Pagination;
44
- sorters?: Sort[];
45
- filters?: Filter[];
46
- meta?: Record<string, unknown>;
47
- dataProviderName?: string;
48
- queryOptions?: { staleTime?: number; enabled?: boolean };
49
- successNotification?: NotificationConfig;
50
- errorNotification?: NotificationConfig;
51
- liveMode?: LiveMode;
52
- onLiveEvent?: (event: LiveEvent) => void;
53
- overtimeOptions?: OvertimeOptions;
54
- }
55
-
56
- export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseInfiniteListOptions<TData, TError> = {}) {
57
- const adminContext = captureAdminContext();
58
- const parsed = useParsed();
59
- const adminOptions = getAdminOptions();
60
-
61
- const query = createInfiniteQuery<{ data: TData[]; total: number }, TError>(() => {
62
- const resource = options.resource ?? parsed.resource ?? '';
63
- const provider = adminContext.getDataProviderForResource(resource, options.dataProviderName);
64
- return {
65
- queryKey: adminContext.queryKeys(resource, options.dataProviderName).data.infiniteList(resource, {
66
- pageSize: options.pagination?.pageSize,
67
- sorters: options.sorters,
68
- filters: options.filters,
69
- meta: options.meta,
70
- }),
71
- queryFn: async ({ pageParam = 1, signal }) => {
72
- const result = await provider.getList<TData>({
73
- signal,
74
- resource,
75
- pagination: { current: pageParam as number, pageSize: options.pagination?.pageSize ?? 10 },
76
- sorters: options.sorters,
77
- filters: options.filters,
78
- meta: options.meta,
79
- });
80
- return result;
81
- },
82
- initialPageParam: 1,
83
- getNextPageParam: (lastPage: { data: TData[]; total: number }, allPages: { data: TData[]; total: number }[]) => {
84
- const totalFetched = allPages.reduce((acc: number, p) => acc + (p.data?.length ?? 0), 0);
85
- if (totalFetched >= (lastPage.total ?? 0)) return undefined;
86
- return allPages.length + 1;
87
- },
88
- enabled: options.queryOptions?.enabled ?? true,
89
- staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
90
- };
91
- });
92
-
93
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
94
-
95
- createLiveSubscription((): LiveSubscriptionParams => ({
96
- resource: options.resource ?? parsed.resource ?? '',
97
- liveProvider: adminContext.liveProvider,
98
- liveMode: options.liveMode ?? adminOptions.liveMode,
99
- onLiveEvent: (e: LiveEvent) => {
100
- options.onLiveEvent?.(e);
101
- adminOptions.onLiveEvent?.(e);
102
- },
103
- enabled: options.queryOptions?.enabled ?? true,
104
- dataProviderName: options.dataProviderName,
105
- }));
106
-
107
- let lastSuccessAt = 0;
108
- let lastErrorAt = 0;
109
- $effect(() => {
110
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
111
- lastSuccessAt = query.dataUpdatedAt;
112
- fireSuccessNotification({
113
- config: options.successNotification,
114
- defaultMessage: '',
115
- data: query.data,
116
- resource: options.resource ?? parsed.resource ?? '',
117
- provider: adminContext.notificationProvider,
118
- });
119
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
120
- lastErrorAt = query.errorUpdatedAt;
121
- checkError(query.error, adminContext);
122
- fireErrorNotification({
123
- config: options.errorNotification,
124
- defaultMessage: 'Fetch failed',
125
- error: query.error,
126
- resource: options.resource ?? parsed.resource ?? '',
127
- provider: adminContext.notificationProvider,
128
- });
129
- }
130
- });
131
-
132
- return { query, get overtime() { return overtime; } };
133
- }
27
+ export { useInfiniteList, createInfiniteListQuery } from './infinite-query.svelte';
28
+ export type { UseInfiniteListOptions } from './infinite-query.svelte';
134
29
 
135
30
  // ─── useSelect ──────────────────────────────────────────────────────
136
31
 
137
- export interface UseSelectOptions<TData extends BaseRecord = BaseRecord, _TOption = { label: string; value: string | number }> {
138
- resource: KnownResources;
139
- optionLabel?: string | ((item: TData) => string);
140
- optionValue?: string | ((item: TData) => string | number);
141
- sorters?: Sort[];
142
- filters?: Filter[];
143
- defaultValue?: (string | number)[];
144
- fetchSize?: number;
145
- pagination?: Pagination;
146
- debounce?: number;
147
- queryOptions?: { staleTime?: number; enabled?: boolean };
148
- defaultValueQueryOptions?: { staleTime?: number; enabled?: boolean };
149
- meta?: Record<string, unknown>;
150
- dataProviderName?: string;
151
- onSearch?: (value: string) => Filter[];
152
- successNotification?: NotificationConfig;
153
- errorNotification?: NotificationConfig;
154
- overtimeOptions?: OvertimeOptions;
155
- }
156
-
157
- export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { label: string; value: string | number }>(options: UseSelectOptions<TData, TOption>) {
158
- const adminContext = captureAdminContext();
159
- const { resource, optionLabel = 'title', optionValue = 'id', sorters, filters, pagination, meta, dataProviderName, onSearch, debounce: debounceMs = 300 } = options;
160
- const adminOptions = getAdminOptions();
161
-
162
- let searchText = $state('');
163
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
164
-
165
- const searchFilters = $derived.by<Filter[]>(() => {
166
- if (!onSearch || !searchText) return [];
167
- return onSearch(searchText);
168
- });
169
-
170
- const allFilters = $derived([...(filters ?? []), ...searchFilters]);
171
-
172
- const effectivePageSize = options.fetchSize ?? pagination?.pageSize ?? 999;
173
-
174
- const query = createQuery<{ data: TData[]; total: number }>(() => {
175
- const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
176
- return {
177
- queryKey: adminContext.queryKeys(resource, dataProviderName).data.select(resource, {
178
- filters: allFilters,
179
- sorters,
180
- pagination,
181
- meta,
182
- }),
183
- queryFn: ({ signal }) => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta, signal }),
184
- enabled: options.queryOptions?.enabled ?? true,
185
- staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
186
- };
187
- });
188
-
189
- // Parallel query to ensure default values are always available in options
190
- const defaultValueIds = options.defaultValue ?? [];
191
- const defaultValueQuery = defaultValueIds.length > 0
192
- ? createQuery<{ data: TData[] }>(() => {
193
- const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
194
- return {
195
- queryKey: adminContext.queryKeys(resource, dataProviderName).data.selectDefaults(resource, {
196
- ids: defaultValueIds,
197
- meta,
198
- }),
199
- queryFn: async ({ signal }) => {
200
- if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta, signal });
201
- const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta, signal })));
202
- return { data: results.map(r => r.data) };
203
- },
204
- enabled: (options.defaultValueQueryOptions?.enabled ?? true) && defaultValueIds.length > 0,
205
- staleTime: options.defaultValueQueryOptions?.staleTime ?? Infinity,
206
- };
207
- })
208
- : null;
209
-
210
- const selectOptions = $derived.by(() => {
211
- const queryResult = query.data as { data: TData[]; total: number } | undefined;
212
- const data: TData[] = queryResult?.data ?? [];
213
- // Merge default value items if not already in data
214
- const defaultQueryResult = defaultValueQuery as { data: { data: TData[] } | undefined } | null;
215
- const defaultData: TData[] = defaultQueryResult?.data?.data ?? [];
216
- const allData: TData[] = [...data];
217
- const resolveValue = (item: TData) => typeof optionValue === 'function' ? optionValue(item) : (item as Record<string, unknown>)[optionValue];
218
- const existingIds = new Set(data.map((d: TData) => String(resolveValue(d))));
219
- for (const item of defaultData) {
220
- if (!existingIds.has(String(resolveValue(item)))) allData.push(item);
221
- }
222
- return allData.map((item: TData) => {
223
- const label = typeof optionLabel === 'function' ? optionLabel(item) : String((item as Record<string, unknown>)[optionLabel] ?? '');
224
- const value = resolveValue(item);
225
- return { label, value } as unknown as TOption;
226
- });
227
- });
228
-
229
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
230
-
231
- let lastSuccessAt = 0;
232
- let lastErrorAt = 0;
233
- $effect(() => {
234
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
235
- lastSuccessAt = query.dataUpdatedAt;
236
- fireSuccessNotification({
237
- config: options.successNotification,
238
- defaultMessage: '',
239
- data: query.data,
240
- resource,
241
- provider: adminContext.notificationProvider,
242
- });
243
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
244
- lastErrorAt = query.errorUpdatedAt;
245
- checkError(query.error, adminContext);
246
- fireErrorNotification({
247
- config: options.errorNotification,
248
- defaultMessage: 'Fetch failed',
249
- error: query.error,
250
- resource,
251
- provider: adminContext.notificationProvider,
252
- });
253
- }
254
- });
255
-
256
- function onSearchChange(value: string) {
257
- if (debounceTimer) clearTimeout(debounceTimer);
258
- debounceTimer = setTimeout(() => { searchText = value; }, debounceMs);
259
- }
260
-
261
- // Cleanup debounce timer on unmount
262
- $effect(() => {
263
- return () => { if (debounceTimer) clearTimeout(debounceTimer); };
264
- });
265
-
266
- return {
267
- query,
268
- get options() { return selectOptions; },
269
- get overtime() { return overtime; },
270
- onSearchChange,
271
- };
272
- }
273
-
274
- // ─── useCustom ──────────────────────────────────────────────────────
275
-
276
- export interface UseCustomOptions<_TData = unknown, _TError = HttpError> {
277
- url: string;
278
- method: 'get' | 'post' | 'put' | 'patch' | 'delete';
279
- config?: { payload?: unknown; query?: Record<string, unknown>; headers?: Record<string, string>; sorters?: Sort[]; filters?: Filter[] };
280
- meta?: Record<string, unknown>;
281
- dataProviderName?: string;
282
- queryOptions?: { staleTime?: number; enabled?: boolean };
283
- successNotification?: NotificationConfig;
284
- errorNotification?: NotificationConfig;
285
- overtimeOptions?: OvertimeOptions;
286
- }
287
-
288
- export function useCustom<TData = unknown, TError = HttpError>(options: UseCustomOptions<TData, TError>) {
289
- const adminContext = captureAdminContext();
290
- const adminOptions = getAdminOptions();
291
-
292
- const query = createQuery<{ data: TData }, TError>(() => {
293
- const provider = adminContext.getDataProvider(options.dataProviderName);
294
- return {
295
- queryKey: adminContext.queryKeys(undefined, options.dataProviderName).custom.call(
296
- options.url,
297
- options.url,
298
- options.method,
299
- { config: options.config, meta: options.meta },
300
- ),
301
- queryFn: async ({ signal }) => {
302
- if (!provider.custom) throw new Error('DataProvider does not support custom method');
303
- return provider.custom<TData>({
304
- signal,
305
- url: options.url,
306
- method: options.method,
307
- payload: options.config?.payload,
308
- query: options.config?.query,
309
- headers: options.config?.headers,
310
- sorters: options.config?.sorters,
311
- filters: options.config?.filters,
312
- meta: options.meta,
313
- });
314
- },
315
- enabled: options.queryOptions?.enabled ?? true,
316
- staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
317
- };
318
- });
319
-
320
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
321
-
322
- let lastSuccessAt = 0;
323
- let lastErrorAt = 0;
324
- $effect(() => {
325
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
326
- lastSuccessAt = query.dataUpdatedAt;
327
- fireSuccessNotification({
328
- config: options.successNotification,
329
- defaultMessage: '',
330
- data: query.data,
331
- provider: adminContext.notificationProvider,
332
- });
333
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
334
- lastErrorAt = query.errorUpdatedAt;
335
- checkError(query.error, adminContext);
336
- fireErrorNotification({
337
- config: options.errorNotification,
338
- defaultMessage: 'Custom request failed',
339
- error: query.error,
340
- resource: '',
341
- provider: adminContext.notificationProvider,
342
- });
343
- }
344
- });
345
-
346
- return { query, get overtime() { return overtime; } };
347
- }
348
-
349
- // ─── useCustomMutation ──────────────────────────────────────────────
350
-
351
- export function useCustomMutation<TData = unknown, TError = HttpError, TVariables = unknown>(dataProviderName?: string) {
352
- const adminContext = captureAdminContext();
353
- const queryClient = useQueryClient();
354
-
355
- const mutation = createMutation<{ data: TData }, TError, { url: string; method: 'get' | 'post' | 'put' | 'patch' | 'delete'; values?: TVariables; query?: Record<string, unknown>; headers?: Record<string, string>; sorters?: Sort[]; filters?: Filter[]; meta?: Record<string, unknown>; invalidates?: string[] | false; resource?: string }>(() => ({
356
- mutationFn: async (params) => {
357
- const provider = adminContext.getDataProvider(dataProviderName);
358
- if (!provider.custom) throw new Error('DataProvider does not support custom method');
359
- return provider.custom<TData>({ url: params.url, method: params.method, payload: params.values, query: params.query, headers: params.headers, sorters: params.sorters, filters: params.filters, meta: params.meta });
360
- },
361
- onSuccess: (data, params) => {
362
- if (params.resource && params.invalidates !== false) {
363
- invalidateByScopes({
364
- queryClient,
365
- resource: params.resource,
366
- scopes: params.invalidates,
367
- defaults: ['list', 'many'],
368
- matcher: adminContext.queryKeyMatcher(params.resource, dataProviderName),
369
- });
370
- }
371
- },
372
- onError: (error) => {
373
- checkError(error, adminContext);
374
- },
375
- }));
376
-
377
- return { mutation };
378
- }
379
-
380
- // ─── useCreateMany / useUpdateMany / useDeleteMany ──────────────────
381
-
382
- export function useCreateMany<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
383
- const adminContext = captureAdminContext();
384
- const parsed = useParsed();
385
- const resource = options.resource ?? parsed.resource ?? '';
386
- const adminOptions = getAdminOptions();
387
- const queryClient = useQueryClient();
388
-
389
- const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; variables: TVariables[]; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
390
- mutationFn: async (params) => {
391
- const resName = params.resource ?? resource;
392
- const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
393
- if (provider.createMany) return await provider.createMany<TData, TVariables>({ resource: resName, variables: params.variables, meta: params.meta });
394
- const results = await Promise.all(params.variables.map(v => provider.create<TData, TVariables>({ resource: resName, variables: v, meta: params.meta })));
395
- return { data: results.map(r => r.data) };
396
- },
397
- onSuccess: (data, params) => {
398
- const resName = params.resource ?? resource;
399
- fireSuccessNotification({
400
- config: undefined,
401
- defaultMessage: 'Created successfully',
402
- data: data.data,
403
- values: params.variables,
404
- resource: resName,
405
- provider: adminContext.notificationProvider,
406
- });
407
- auditWithProvider(
408
- { action: 'create', resource: resName, meta: adminContext.getProviderMeta(resName) },
409
- adminContext.auditLogProvider,
410
- );
411
- publishLiveEvent(resName, 'created', undefined, adminContext);
412
- },
413
- onError: (error, params) => {
414
- checkError(error, adminContext);
415
- fireErrorNotification({
416
- config: undefined,
417
- defaultMessage: 'Create many failed',
418
- error,
419
- resource: params.resource ?? resource,
420
- provider: adminContext.notificationProvider,
421
- });
422
- },
423
- onSettled: (_d, _e, params) => {
424
- const resName = params.resource ?? resource;
425
- invalidateByScopes({
426
- queryClient,
427
- resource: resName,
428
- defaults: ['list', 'many'],
429
- matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
430
- });
431
- },
432
- }));
433
-
434
- return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
435
- }
436
-
437
- export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: { resource?: KnownResources; overtimeOptions?: OvertimeOptions } = {}) {
438
- const adminContext = captureAdminContext();
439
- const parsed = useParsed();
440
- const resource = options.resource ?? parsed.resource ?? '';
441
- const adminOptions = getAdminOptions();
442
- const queryClient = useQueryClient();
443
-
444
- const mutation = createMutation<{ data: TData[] }, TError, { resource?: KnownResources; ids: (string | number)[]; variables: TVariables; meta?: Record<string, unknown>; dataProviderName?: string }>(() => ({
445
- mutationFn: async (params) => {
446
- const resName = params.resource ?? resource;
447
- const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
448
- if (provider.updateMany) return await provider.updateMany<TData, TVariables>({ resource: resName, ids: params.ids, variables: params.variables, meta: params.meta });
449
- const results = await Promise.all(params.ids.map(id => provider.update<TData, TVariables>({ resource: resName, id, variables: params.variables, meta: params.meta })));
450
- return { data: results.map(r => r.data) };
451
- },
452
- onSuccess: (data, params) => {
453
- const resName = params.resource ?? resource;
454
- fireSuccessNotification({
455
- config: undefined,
456
- defaultMessage: 'Updated successfully',
457
- data: data.data,
458
- values: params.variables,
459
- resource: resName,
460
- provider: adminContext.notificationProvider,
461
- });
462
- auditWithProvider(
463
- { action: 'update', resource: resName, meta: adminContext.getProviderMeta(resName) },
464
- adminContext.auditLogProvider,
465
- );
466
- publishLiveEvent(resName, 'updated', params.ids, adminContext);
467
- },
468
- onError: (error, params) => {
469
- checkError(error, adminContext);
470
- fireErrorNotification({
471
- config: undefined,
472
- defaultMessage: 'Update many failed',
473
- error,
474
- resource: params.resource ?? resource,
475
- provider: adminContext.notificationProvider,
476
- });
477
- },
478
- onSettled: (_d, _e, params) => {
479
- const resName = params.resource ?? resource;
480
- invalidateByScopes({
481
- queryClient,
482
- resource: resName,
483
- defaults: ['list', 'many', 'detail'],
484
- matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
485
- });
486
- for (const id of params.ids) {
487
- queryClient.invalidateQueries({
488
- predicate: (q) => queryKeyMatches(q.queryKey, {
489
- ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
490
- kind: 'data',
491
- resource: resName,
492
- action: 'one',
493
- id,
494
- }),
495
- });
496
- }
497
- },
498
- }));
499
-
500
- return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
501
- }
502
-
503
- export interface UseDeleteManyOptions {
504
- resource?: KnownResources;
505
- mutationMode?: MutationMode;
506
- undoableTimeout?: number;
507
- mutationOptions?: MutationCallbacks;
508
- overtimeOptions?: OvertimeOptions;
509
- successNotification?: NotificationConfig;
510
- errorNotification?: NotificationConfig;
511
- }
512
-
513
- export interface UseDeleteManyMutateParams {
514
- resource?: KnownResources;
515
- ids: (string | number)[];
516
- mutationMode?: MutationMode;
517
- undoableTimeout?: number;
518
- meta?: Record<string, unknown>;
519
- dataProviderName?: string;
520
- invalidates?: string[] | false;
521
- onCancel?: () => void;
522
- successNotification?: NotificationConfig;
523
- errorNotification?: NotificationConfig;
524
- }
525
-
526
- interface DeleteManyMutationContext {
527
- _svadmin_ctx?: boolean;
528
- userContext?: unknown;
529
- previousQueries?: [readonly unknown[], unknown][];
530
- }
531
-
532
- export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(options: UseDeleteManyOptions = {}) {
533
- const adminContext = captureAdminContext();
534
- const parsed = useParsed();
535
- const resource = options.resource ?? parsed.resource ?? '';
536
- const adminOptions = getAdminOptions();
537
- const queryClient = useQueryClient();
538
- const i18n = useTranslation();
539
- const mutationMode = options.mutationMode ?? adminOptions.mutationMode ?? 'pessimistic';
540
- const undoableTimeout = options.undoableTimeout ?? adminOptions.undoableTimeout ?? 5000;
541
-
542
- const removeIdsFromCache = (
543
- resName: string,
544
- ids: (string | number)[],
545
- dataProviderName?: string,
546
- ) => {
547
- const matcher = adminContext.queryKeyMatcher(resName, dataProviderName);
548
- const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
549
- ...matcher,
550
- resource: resName,
551
- ...fields,
552
- });
553
- const primaryKey = adminContext.getResource(resName).primaryKey ?? 'id';
554
- const deletedIds = new Set(ids.map(String));
555
- const removeFromCollection = (old: unknown): unknown => {
556
- if (!old || typeof old !== 'object' || !('data' in old)) return old;
557
- const current = old as { data: Record<string, unknown>[]; total?: number };
558
- if (!Array.isArray(current.data)) return old;
559
- const filtered = current.data.filter((item) => !deletedIds.has(String(item[primaryKey])));
560
- const removedCount = current.data.length - filtered.length;
561
- return {
562
- ...current,
563
- data: filtered,
564
- total: current.total == null ? current.total : Math.max(0, current.total - removedCount),
565
- };
566
- };
567
- const removeFromInfiniteCollection = (old: unknown): unknown => {
568
- if (!old || typeof old !== 'object' || !('pages' in old)) return old;
569
- const current = old as {
570
- pages: { data: Record<string, unknown>[]; total?: number }[];
571
- pageParams?: unknown[];
572
- };
573
- if (!Array.isArray(current.pages)) return old;
574
-
575
- const removedIds = new Set<string>();
576
- for (const page of current.pages) {
577
- if (!Array.isArray(page.data)) continue;
578
- for (const item of page.data) {
579
- const itemId = String(item[primaryKey]);
580
- if (deletedIds.has(itemId)) removedIds.add(itemId);
581
- }
582
- }
583
-
584
- return {
585
- ...current,
586
- pages: current.pages.map((page) => ({
587
- ...page,
588
- data: Array.isArray(page.data)
589
- ? page.data.filter((item) => !deletedIds.has(String(item[primaryKey])))
590
- : page.data,
591
- total: page.total == null ? page.total : Math.max(0, page.total - removedIds.size),
592
- })),
593
- };
594
- };
595
-
596
- queryClient.setQueriesData({
597
- predicate: (query) => ['list', 'select', 'selectDefaults'].some((action) => dataMatch(query.queryKey, { action })),
598
- }, removeFromCollection);
599
- queryClient.setQueriesData({
600
- predicate: (query) => dataMatch(query.queryKey, { action: 'infiniteList' }),
601
- }, removeFromInfiniteCollection);
602
- queryClient.setQueriesData({
603
- predicate: (query) => dataMatch(query.queryKey, { action: 'many' }),
604
- }, removeFromCollection);
605
- };
606
-
607
- const mutation = createMutation<{ data: TData[] }, TError | DeleteManyPartialError, UseDeleteManyMutateParams, DeleteManyMutationContext>(() => ({
608
- mutationFn: async (params) => {
609
- const resName = params.resource ?? resource;
610
- const effectiveMutationMode = params.mutationMode ?? mutationMode;
611
- const effectiveUndoableTimeout = params.undoableTimeout ?? undoableTimeout;
612
- if (effectiveMutationMode === 'undoable') {
613
- await new Promise<void>((resolve, reject) => {
614
- toast.undoable(i18n.t('common.actionCanBeUndone'), effectiveUndoableTimeout, () => {
615
- params.onCancel?.();
616
- reject(new UndoError());
617
- }, resolve);
618
- });
619
- }
620
- const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
621
- if (provider.deleteMany) return await provider.deleteMany<TData>({ resource: resName, ids: params.ids, meta: params.meta });
622
- const settled = await Promise.allSettled(
623
- params.ids.map(id => provider.deleteOne<TData>({ resource: resName, id, meta: params.meta })),
624
- );
625
- const succeededIds: (string | number)[] = [];
626
- const failedIds: (string | number)[] = [];
627
- const causes: unknown[] = [];
628
- const data: TData[] = [];
629
- settled.forEach((result, index) => {
630
- const id = params.ids[index];
631
- if (result.status === 'fulfilled') {
632
- succeededIds.push(id);
633
- data.push(result.value.data);
634
- } else {
635
- failedIds.push(id);
636
- causes.push(result.reason);
637
- }
638
- });
639
- if (failedIds.length > 0) {
640
- if (succeededIds.length > 0) {
641
- throw new DeleteManyPartialError(succeededIds, failedIds, causes);
642
- }
643
- throw causes[0] ?? new Error('Delete many failed');
644
- }
645
- return { data };
646
- },
647
- onMutate: async (params) => {
648
- const userContext = typeof options.mutationOptions?.onMutate === 'function'
649
- ? await options.mutationOptions.onMutate(params)
650
- : undefined;
651
- const effectiveMutationMode = params.mutationMode ?? mutationMode;
652
- if (effectiveMutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
653
- const resName = params.resource ?? resource;
654
- const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
655
- const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
656
- ...matcher,
657
- resource: resName,
658
- ...fields,
659
- });
660
-
661
- await queryClient.cancelQueries({ predicate: (query) => dataMatch(query.queryKey) });
662
- const previousQueries = queryClient.getQueriesData({ predicate: (query) => dataMatch(query.queryKey) });
663
- removeIdsFromCache(resName, params.ids, params.dataProviderName);
664
-
665
- return { _svadmin_ctx: true, userContext, previousQueries };
666
- },
667
- onSuccess: (data, params, context) => {
668
- const resName = params.resource ?? resource;
669
- const extractedContext = context?._svadmin_ctx ? context.userContext : context;
670
- fireSuccessNotification({
671
- config: params.successNotification ?? options.successNotification,
672
- defaultMessage: 'Deleted successfully',
673
- data: data.data,
674
- resource: resName,
675
- provider: adminContext.notificationProvider,
676
- });
677
- auditWithProvider(
678
- { action: 'delete', resource: resName, meta: adminContext.getProviderMeta(resName) },
679
- adminContext.auditLogProvider,
680
- );
681
- publishLiveEvent(resName, 'deleted', params.ids, adminContext);
682
- for (const id of params.ids) {
683
- queryClient.removeQueries({
684
- predicate: (query) => queryKeyMatches(query.queryKey, {
685
- ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
686
- kind: 'data',
687
- resource: resName,
688
- action: 'one',
689
- id,
690
- }),
691
- });
692
- }
693
- if (typeof options.mutationOptions?.onSuccess === 'function') {
694
- options.mutationOptions.onSuccess(data, params, extractedContext);
695
- }
696
- },
697
- onError: (error, params, context) => {
698
- if (context?.previousQueries) {
699
- for (const [queryKey, data] of context.previousQueries) queryClient.setQueryData(queryKey, data);
700
- }
701
- if (error instanceof UndoError) return;
702
- const resName = params.resource ?? resource;
703
- if (error instanceof DeleteManyPartialError) {
704
- removeIdsFromCache(resName, error.succeededIds, params.dataProviderName);
705
- for (const id of error.succeededIds) {
706
- queryClient.removeQueries({
707
- predicate: (query) => queryKeyMatches(query.queryKey, {
708
- ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
709
- kind: 'data',
710
- resource: resName,
711
- action: 'one',
712
- id,
713
- }),
714
- });
715
- }
716
- auditWithProvider(
717
- {
718
- action: 'delete',
719
- resource: resName,
720
- outcome: 'failure',
721
- details: { succeededIds: error.succeededIds, failedIds: error.failedIds },
722
- meta: adminContext.getProviderMeta(resName),
723
- },
724
- adminContext.auditLogProvider,
725
- );
726
- publishLiveEvent(resName, 'deleted', error.succeededIds, adminContext);
727
- }
728
- const authError = error instanceof DeleteManyPartialError
729
- ? (error.causes[0] ?? error)
730
- : error;
731
- checkError(authError, adminContext);
732
- fireErrorNotification({
733
- config: params.errorNotification ?? options.errorNotification,
734
- defaultMessage: 'Delete many failed',
735
- error,
736
- resource: resName,
737
- provider: adminContext.notificationProvider,
738
- });
739
- const extractedContext = context?._svadmin_ctx ? context.userContext : context;
740
- if (typeof options.mutationOptions?.onError === 'function') {
741
- options.mutationOptions.onError(error, params, extractedContext);
742
- }
743
- },
744
- onSettled: (_data, error, params, context) => {
745
- if (error instanceof UndoError) return;
746
- const resName = params.resource ?? resource;
747
- invalidateByScopes({
748
- queryClient,
749
- resource: resName,
750
- scopes: params.invalidates,
751
- defaults: ['list', 'many'],
752
- matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
753
- });
754
- const extractedContext = context?._svadmin_ctx ? context.userContext : context;
755
- if (typeof options.mutationOptions?.onSettled === 'function') {
756
- options.mutationOptions.onSettled(_data, error, params, extractedContext);
757
- }
758
- },
759
- }));
760
-
761
- return { mutation, get overtime() { return createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime); } };
762
- }
763
-
764
- // ─── useInvalidate ──────────────────────────────────────────────────
765
-
766
- export function useInvalidate() {
767
- const queryClient = useQueryClient();
768
- const adminContext = captureAdminContext();
769
- return (params: { resource?: string; invalidates?: string[] | 'all' | false; id?: string | number; dataProviderName?: string }) => {
770
- if (params.invalidates === false) return;
771
- if (params.invalidates === 'all' || !params.resource) {
772
- queryClient.invalidateQueries({
773
- predicate: (q) => queryKeyMatches(q.queryKey, {
774
- ...adminContext.queryKeyMatcher(undefined, params.dataProviderName),
775
- kind: 'data',
776
- }),
777
- });
778
- return;
779
- }
780
- const res = params.resource;
781
- invalidateByScopes({
782
- queryClient,
783
- resource: res,
784
- scopes: params.invalidates || ['resourceAll'],
785
- defaults: ['resourceAll'],
786
- id: params.id,
787
- matcher: adminContext.queryKeyMatcher(res, params.dataProviderName),
788
- });
789
- };
790
- }
32
+ export { createSelectQuery } from './select-query.svelte';
33
+ export type { UseSelectOptions } from './select-query.svelte';
791
34
 
792
35
  // ─── useOvertime ────────────────────────────────────────────────────
793
36
 
794
37
  export function useOvertime(options?: OvertimeOptions) {
795
- let isLoading = $state(false);
796
- const overtime = createOvertimeTracker(() => isLoading, options);
38
+ let isLoading=$state(false);
39
+ const overtime=createOvertimeTracker(() => isLoading,options);
797
40
  return {
798
41
  get elapsedTime() { return overtime.elapsedTime; },
799
- start() { isLoading = true; },
800
- stop() { isLoading = false; },
42
+ start() { isLoading=true; },
43
+ stop() { isLoading=false; },
801
44
  };
802
45
  }
803
46
 
804
47
  // ─── useDataProvider ────────────────────────────────────────────────
805
48
 
806
49
  export function useDataProvider(): (dataProviderName?: string) => DataProvider {
807
- const adminContext = captureAdminContext();
50
+ const adminContext=captureAdminContext();
808
51
  return (name?: string) => adminContext.getDataProvider(name);
809
52
  }
810
53
 
811
54
  // ─── useThemedLayoutContext ─────────────────────────────────────────
812
55
 
813
- let _sidebarCollapsed = $state(false);
56
+ let _sidebarCollapsed=$state(false);
814
57
 
815
- export function resetSidebarCollapsed() { _sidebarCollapsed = false; }
58
+ export function resetSidebarCollapsed() { _sidebarCollapsed=false; }
816
59
 
817
60
  export function useThemedLayoutContext() {
818
61
  return {
819
62
  get sidebarCollapsed() { return _sidebarCollapsed; },
820
- setSidebarCollapsed(v: boolean) { _sidebarCollapsed = v; },
821
- toggleSidebar() { _sidebarCollapsed = !_sidebarCollapsed; },
63
+ setSidebarCollapsed(v: boolean) { _sidebarCollapsed=v; },
64
+ toggleSidebar() { _sidebarCollapsed=!_sidebarCollapsed; },
822
65
  };
823
66
  }