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