@svadmin/core 0.34.2 → 0.36.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.
@@ -2,10 +2,13 @@ import { useQueryClient } from '@tanstack/svelte-query';
2
2
  import { useParsed } from './useParsed.svelte';
3
3
  import { getAdminOptions } from './options.svelte';
4
4
  import { captureAdminContext } from './context.svelte';
5
+ import { queryKeyMatches } from './query-keys';
5
6
  import { createQuery, createMutation } from '@tanstack/svelte-query';
6
- import { notify } from './notification.svelte';
7
+ import { notifyWithProvider } from './notification.svelte';
8
+ import type { NotificationParams } from './notification.svelte';
7
9
  import { useTranslation } from './i18n.svelte';
8
- import { audit } from './audit';
10
+ import { auditWithProvider } from './audit';
11
+ import type { AuditEntry } from './audit';
9
12
  import { HttpError, UndoError } from './types';
10
13
  import type { BaseRecord, MutationMode, KnownResources } from './types';
11
14
  import { checkError } from './hook-utils.svelte';
@@ -153,6 +156,10 @@ export function useForm<
153
156
  const queryClient = useQueryClient();
154
157
  const parsed = useParsed();
155
158
  const adminOptions = getAdminOptions();
159
+ const sendNotification = (params: NotificationParams) =>
160
+ notifyWithProvider(params, adminContext.notificationProvider);
161
+ const recordAudit = (entry: Omit<AuditEntry, 'timestamp'>) =>
162
+ auditWithProvider(entry, adminContext.auditLogProvider);
156
163
 
157
164
  const resource = $derived(options.resource ?? parsed.resource ?? '');
158
165
  let action = $state<'create' | 'edit' | 'clone' | 'show'>(options.action ?? (parsed.action === 'list' ? 'create' : parsed.action as 'create' | 'edit' | 'clone' | 'show') ?? 'create');
@@ -441,9 +448,9 @@ export function useForm<
441
448
  const msg = Array.isArray(messages) ? messages[0] : messages;
442
449
  setFieldError(field, msg);
443
450
  }
444
- if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (error.message || i18n.t('common.operationFailed')) });
451
+ if (errorNotification !== false) sendNotification({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (error.message || i18n.t('common.operationFailed')) });
445
452
  } else {
446
- if (errorNotification !== false) notify({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (i18n.t('common.operationFailed') + ': ' + error.message) });
453
+ if (errorNotification !== false) sendNotification({ type: 'error', message: typeof errorNotification === 'string' ? errorNotification : (i18n.t('common.operationFailed') + ': ' + error.message) });
447
454
  }
448
455
  }
449
456
 
@@ -451,7 +458,9 @@ export function useForm<
451
458
  // Always create the query — use `enabled` to conditionally activate.
452
459
  // This ensures setAction('edit') at runtime will trigger a fetch.
453
460
  const query = createQuery(() => ({
454
- queryKey: [options.dataProviderName, resource, 'one', currentId, queryMeta],
461
+ queryKey: adminContext.queryKeys(resource, options.dataProviderName).data.one(resource, currentId ?? '', {
462
+ meta: queryMeta,
463
+ }),
455
464
  queryFn: async () => {
456
465
  const result = await provider.getOne<BaseRecord>({ resource, id: currentId as string, meta: queryMeta });
457
466
  return result;
@@ -518,12 +527,23 @@ export function useForm<
518
527
  onSuccess: (data: { data: TData }, _vars: unknown, context: unknown) => {
519
528
  const ctx = context as { targetResource?: string } | undefined;
520
529
  const res = ctx?.targetResource ?? resource;
521
- invalidateByScopes(queryClient, res, invalidateScopes, ['list', 'many'], undefined, options.dataProviderName);
530
+ invalidateByScopes({
531
+ queryClient,
532
+ resource: res,
533
+ scopes: invalidateScopes,
534
+ defaults: ['list', 'many'],
535
+ matcher: adminContext.queryKeyMatcher(res, options.dataProviderName),
536
+ });
522
537
  if (!isManualSubmitIdentityCurrent()) return;
523
- if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.createSuccess') });
538
+ if (successNotification !== false) sendNotification({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.createSuccess') });
524
539
  const pk = adminContext.getResource(res).primaryKey ?? 'id';
525
540
  const newId = (data.data as Record<string, unknown>)[pk];
526
- audit({ action: 'create', resource: res, recordId: String(newId) });
541
+ recordAudit({
542
+ action: 'create',
543
+ resource: res,
544
+ recordId: String(newId),
545
+ meta: adminContext.getProviderMeta(res),
546
+ });
527
547
  try {
528
548
  const lp = adminContext.liveProvider;
529
549
  if (lp?.publish) lp.publish({ type: 'INSERT', resource: res, payload: { ids: newId != null ? [newId as string | number] : [] } });
@@ -562,12 +582,17 @@ export function useForm<
562
582
  if (mutationMode === 'pessimistic') return { targetId: currentId, targetResource: resource };
563
583
  const targetId = currentId;
564
584
  const targetResource = resource;
565
- const dpN = options.dataProviderName;
566
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
567
- await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resource });
568
- const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resource });
585
+ const matcher = adminContext.queryKeyMatcher(resource, options.dataProviderName);
586
+ const dataMatch = (queryKey: readonly unknown[], fields: { action?: string; id?: string | number } = {}) => queryKeyMatches(queryKey, {
587
+ ...matcher,
588
+ kind: 'data',
589
+ resource,
590
+ ...fields,
591
+ });
592
+ await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
593
+ const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
569
594
  if (optimisticUpdateMap?.list !== false) {
570
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resource && q.queryKey[2] === 'list' }, (old: unknown) => {
595
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
571
596
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
572
597
  const o = old as { data: Record<string, unknown>[] };
573
598
  const pk = adminContext.getResource(resource).primaryKey ?? 'id';
@@ -575,7 +600,7 @@ export function useForm<
575
600
  });
576
601
  }
577
602
  if (optimisticUpdateMap?.detail !== false && targetId != null) {
578
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resource && q.queryKey[2] === 'one' && q.queryKey[3] === targetId }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, variables) } : old);
603
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, variables) } : old);
579
604
  }
580
605
  return { previousQueries, targetId, targetResource };
581
606
  },
@@ -584,8 +609,13 @@ export function useForm<
584
609
  const targetId = ctx?.targetId ?? currentId;
585
610
  const res = ctx?.targetResource ?? resource;
586
611
  if (!isManualSubmitIdentityCurrent()) return;
587
- if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.updateSuccess') });
588
- audit({ action: 'update', resource: res, recordId: String(targetId) });
612
+ if (successNotification !== false) sendNotification({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.updateSuccess') });
613
+ recordAudit({
614
+ action: 'update',
615
+ resource: res,
616
+ recordId: String(targetId),
617
+ meta: adminContext.getProviderMeta(res),
618
+ });
589
619
  try {
590
620
  const lp = adminContext.liveProvider;
591
621
  if (lp?.publish) lp.publish({ type: 'UPDATE', resource: res, payload: { ids: targetId != null ? [targetId] : [] } });
@@ -598,7 +628,14 @@ export function useForm<
598
628
  const ctx = context as { targetId?: string | number; targetResource?: string } | undefined;
599
629
  const targetId = ctx?.targetId ?? currentId;
600
630
  const res = ctx?.targetResource ?? resource;
601
- invalidateByScopes(queryClient, res, invalidateScopes, ['list', 'many', 'detail'], targetId ?? undefined, options.dataProviderName);
631
+ invalidateByScopes({
632
+ queryClient,
633
+ resource: res,
634
+ scopes: invalidateScopes,
635
+ defaults: ['list', 'many', 'detail'],
636
+ id: targetId ?? undefined,
637
+ matcher: adminContext.queryKeyMatcher(res, options.dataProviderName),
638
+ });
602
639
  },
603
640
  onError: (error: unknown, _vars: unknown, context: unknown) => {
604
641
  const ctx = context as { previousQueries?: [unknown, unknown][] } | undefined;
@@ -649,7 +686,7 @@ export function useForm<
649
686
 
650
687
  if (!isRecordIdentityCurrent(submitIdentity)) return;
651
688
 
652
- if (!runValidation()) { notify({ type: 'warning', message: i18n.t('validation.required') }); return; }
689
+ if (!runValidation()) { sendNotification({ type: 'warning', message: i18n.t('validation.required') }); return; }
653
690
 
654
691
  const submittedValues = createPersistenceSnapshot(values);
655
692
  if (action === 'create' || action === 'clone') await createMut.mutateAsync(submittedValues);
@@ -740,7 +777,14 @@ export function useForm<
740
777
  await provider.update<TData, TVariables>({ resource: safeResource, id: safeId as string | number, variables: submittedValues, meta: safeMeta });
741
778
  if (formDestroyed) return;
742
779
  const scopes = autoSaveOpts.invalidates ?? ['resourceAll'];
743
- invalidateByScopes(queryClient, safeResource, scopes, ['resourceAll'], safeId ?? undefined, options.dataProviderName);
780
+ invalidateByScopes({
781
+ queryClient,
782
+ resource: safeResource,
783
+ scopes,
784
+ defaults: ['resourceAll'],
785
+ id: safeId ?? undefined,
786
+ matcher: adminContext.queryKeyMatcher(safeResource, options.dataProviderName),
787
+ });
744
788
  if (!isRecordIdentityCurrent(scheduledIdentity)) return;
745
789
  autoSaveStatus = 'saved';
746
790
  lastAutoSaveData = submittedValues;
@@ -769,7 +813,13 @@ export function useForm<
769
813
 
770
814
  if (autoSaveOpts?.invalidateOnUnmount) {
771
815
  $effect(() => {
772
- return () => queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === options.dataProviderName && q.queryKey[1] === resource });
816
+ return () => queryClient.invalidateQueries({
817
+ predicate: (q) => queryKeyMatches(q.queryKey, {
818
+ ...adminContext.queryKeyMatcher(resource, options.dataProviderName),
819
+ kind: 'data',
820
+ resource,
821
+ }),
822
+ });
773
823
  });
774
824
  }
775
825
 
@@ -5,7 +5,9 @@ import { useQueryClient } from '@tanstack/svelte-query';
5
5
  import type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
6
6
  import { captureAdminContext } from './context.svelte';
7
7
  import type { AdminContextAccessor } from './context.svelte';
8
- import { notify } from './notification.svelte';
8
+ import { dataQueryMatches } from './query-keys';
9
+ import { notifyWithProvider } from './notification.svelte';
10
+ import type { NotificationProvider } from './types';
9
11
 
10
12
  // ─── Auth Error Delegate ────────────────────────────────────────
11
13
  // Delegate auth errors to authProvider.onError() — refine pattern
@@ -88,6 +90,7 @@ export interface LiveSubscriptionParams {
88
90
 
89
91
  export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams): void {
90
92
  const queryClient = useQueryClient();
93
+ const adminContext = captureAdminContext();
91
94
 
92
95
  $effect(() => {
93
96
  const params = paramsFn();
@@ -105,9 +108,10 @@ export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams):
105
108
  callback: (event: LiveEvent) => {
106
109
  params.onLiveEvent?.(event);
107
110
  if (liveMode === 'auto') {
108
- const dpN = params.dataProviderName;
109
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
110
- queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === params.resource });
111
+ const matcher = adminContext.queryKeyMatcher(params.resource, params.dataProviderName);
112
+ queryClient.invalidateQueries({
113
+ predicate: (q) => dataQueryMatches(q.queryKey, { ...matcher, resource: params.resource }),
114
+ });
111
115
  }
112
116
  },
113
117
  });
@@ -127,36 +131,44 @@ export type NotificationConfig =
127
131
  | ((data?: unknown, values?: unknown, resource?: string) => { message: string; description?: string; type?: 'success' | 'error' })
128
132
  | undefined;
129
133
 
130
- export function fireSuccessNotification(
131
- config: NotificationConfig,
132
- defaultMessage: string,
133
- data?: unknown,
134
- values?: unknown,
135
- resource?: string,
136
- ): void {
134
+ export interface SuccessNotificationRequest {
135
+ config: NotificationConfig;
136
+ defaultMessage: string;
137
+ data?: unknown;
138
+ values?: unknown;
139
+ resource?: string;
140
+ provider?: NotificationProvider | null;
141
+ }
142
+
143
+ export interface ErrorNotificationRequest {
144
+ config: NotificationConfig;
145
+ defaultMessage: string;
146
+ error?: unknown;
147
+ resource?: string;
148
+ provider?: NotificationProvider | null;
149
+ }
150
+
151
+ export function fireSuccessNotification(request: SuccessNotificationRequest): void {
152
+ const { config, defaultMessage, data, values, resource, provider } = request;
137
153
  if (config === false) return;
138
154
  if (!config && !defaultMessage) return;
139
155
  if (typeof config === 'function') {
140
156
  const result = config(data, values, resource);
141
- notify({ type: 'success', message: result.message, description: result.description });
157
+ notifyWithProvider({ type: 'success', message: result.message, description: result.description }, provider);
142
158
  return;
143
159
  }
144
- notify({ type: 'success', message: config || defaultMessage });
160
+ notifyWithProvider({ type: 'success', message: config || defaultMessage }, provider);
145
161
  }
146
162
 
147
- export function fireErrorNotification(
148
- config: NotificationConfig,
149
- defaultMessage: string,
150
- error?: unknown,
151
- resource?: string,
152
- ): void {
163
+ export function fireErrorNotification(request: ErrorNotificationRequest): void {
164
+ const { config, defaultMessage, error, resource, provider } = request;
153
165
  if (config === false) return;
154
166
  if (!config && !defaultMessage) return;
155
167
  if (typeof config === 'function') {
156
168
  const result = config(error, undefined, resource);
157
- notify({ type: 'error', message: result.message, description: result.description });
169
+ notifyWithProvider({ type: 'error', message: result.message, description: result.description }, provider);
158
170
  return;
159
171
  }
160
172
  const errMsg = error instanceof Error ? error.message : String(error ?? '');
161
- notify({ type: 'error', message: config || `${defaultMessage}${errMsg ? ': ' + errMsg : ''}` });
173
+ notifyWithProvider({ type: 'error', message: config || `${defaultMessage}${errMsg ? ': ' + errMsg : ''}` }, provider);
162
174
  }
@@ -17,20 +17,19 @@ export type { InvalidateScope, OvertimeConfig } from './options.svelte';
17
17
  export { createOvertimeTracker, createLiveSubscription } from './hook-utils.svelte';
18
18
  export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook-utils.svelte';
19
19
 
20
- // ─── Missing hooks stubs ───────────────────────────────────────────
21
- // These hooks were in the original monolithic file. They are re-created
22
- // here as thin wrappers or stubs until they get their own modular files.
20
+ // ─── Additional core hooks ─────────────────────────────────────────
23
21
 
24
22
  import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
25
23
  import { getAdminOptions } from './options.svelte';
26
24
  import { captureAdminContext } from './context.svelte';
25
+ import { queryKeyMatches } from './query-keys';
27
26
  import { useParsed } from './useParsed.svelte';
28
27
  import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification, checkError } from './hook-utils.svelte';
29
28
  import { invalidateByScopes, publishLiveEvent } from './mutation-hooks.svelte';
30
29
  import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
31
30
  import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
32
31
  import type { LiveMode, LiveEvent } from './live.svelte';
33
- import { audit } from './audit';
32
+ import { auditWithProvider } from './audit';
34
33
 
35
34
  // ─── useInfiniteList ────────────────────────────────────────────────
36
35
 
@@ -58,7 +57,12 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
58
57
  const resource = options.resource ?? parsed.resource ?? '';
59
58
  const provider = adminContext.getDataProviderForResource(resource, options.dataProviderName);
60
59
  return {
61
- queryKey: [options.dataProviderName, resource, 'infiniteList', options.pagination?.pageSize, options.sorters, options.filters, options.meta],
60
+ queryKey: adminContext.queryKeys(resource, options.dataProviderName).data.infiniteList(resource, {
61
+ pageSize: options.pagination?.pageSize,
62
+ sorters: options.sorters,
63
+ filters: options.filters,
64
+ meta: options.meta,
65
+ }),
62
66
  queryFn: async ({ pageParam = 1 }) => {
63
67
  const result = await provider.getList<TData>({
64
68
  resource,
@@ -99,11 +103,23 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
99
103
  $effect(() => {
100
104
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
101
105
  lastSuccessAt = query.dataUpdatedAt;
102
- fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource ?? parsed.resource ?? '');
106
+ fireSuccessNotification({
107
+ config: options.successNotification,
108
+ defaultMessage: '',
109
+ data: query.data,
110
+ resource: options.resource ?? parsed.resource ?? '',
111
+ provider: adminContext.notificationProvider,
112
+ });
103
113
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
104
114
  lastErrorAt = query.errorUpdatedAt;
105
115
  checkError(query.error, adminContext);
106
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, options.resource ?? parsed.resource ?? '');
116
+ fireErrorNotification({
117
+ config: options.errorNotification,
118
+ defaultMessage: 'Fetch failed',
119
+ error: query.error,
120
+ resource: options.resource ?? parsed.resource ?? '',
121
+ provider: adminContext.notificationProvider,
122
+ });
107
123
  }
108
124
  });
109
125
 
@@ -152,7 +168,12 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
152
168
  const query = createQuery<{ data: TData[]; total: number }>(() => {
153
169
  const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
154
170
  return {
155
- queryKey: [dataProviderName, resource, 'select', allFilters, sorters, pagination, meta],
171
+ queryKey: adminContext.queryKeys(resource, dataProviderName).data.select(resource, {
172
+ filters: allFilters,
173
+ sorters,
174
+ pagination,
175
+ meta,
176
+ }),
156
177
  queryFn: () => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta }),
157
178
  enabled: options.queryOptions?.enabled ?? true,
158
179
  staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
@@ -165,7 +186,10 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
165
186
  ? createQuery<{ data: TData[] }>(() => {
166
187
  const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
167
188
  return {
168
- queryKey: [dataProviderName, resource, 'select-defaults', defaultValueIds],
189
+ queryKey: adminContext.queryKeys(resource, dataProviderName).data.selectDefaults(resource, {
190
+ ids: defaultValueIds,
191
+ meta,
192
+ }),
169
193
  queryFn: async () => {
170
194
  if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta });
171
195
  const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta })));
@@ -203,11 +227,23 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
203
227
  $effect(() => {
204
228
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
205
229
  lastSuccessAt = query.dataUpdatedAt;
206
- fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
230
+ fireSuccessNotification({
231
+ config: options.successNotification,
232
+ defaultMessage: '',
233
+ data: query.data,
234
+ resource,
235
+ provider: adminContext.notificationProvider,
236
+ });
207
237
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
208
238
  lastErrorAt = query.errorUpdatedAt;
209
239
  checkError(query.error, adminContext);
210
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, resource);
240
+ fireErrorNotification({
241
+ config: options.errorNotification,
242
+ defaultMessage: 'Fetch failed',
243
+ error: query.error,
244
+ resource,
245
+ provider: adminContext.notificationProvider,
246
+ });
211
247
  }
212
248
  });
213
249
 
@@ -250,7 +286,12 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
250
286
  const query = createQuery<{ data: TData }, TError>(() => {
251
287
  const provider = adminContext.getDataProvider(options.dataProviderName);
252
288
  return {
253
- queryKey: [options.dataProviderName, 'custom', options.url, options.method, options.config, options.meta],
289
+ queryKey: adminContext.queryKeys(undefined, options.dataProviderName).custom.call(
290
+ options.url,
291
+ options.url,
292
+ options.method,
293
+ { config: options.config, meta: options.meta },
294
+ ),
254
295
  queryFn: async () => {
255
296
  if (!provider.custom) throw new Error('DataProvider does not support custom method');
256
297
  return provider.custom<TData>({
@@ -276,11 +317,22 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
276
317
  $effect(() => {
277
318
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
278
319
  lastSuccessAt = query.dataUpdatedAt;
279
- fireSuccessNotification(options.successNotification, '', query.data);
320
+ fireSuccessNotification({
321
+ config: options.successNotification,
322
+ defaultMessage: '',
323
+ data: query.data,
324
+ provider: adminContext.notificationProvider,
325
+ });
280
326
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
281
327
  lastErrorAt = query.errorUpdatedAt;
282
328
  checkError(query.error, adminContext);
283
- fireErrorNotification(options.errorNotification, 'Custom request failed', query.error, '');
329
+ fireErrorNotification({
330
+ config: options.errorNotification,
331
+ defaultMessage: 'Custom request failed',
332
+ error: query.error,
333
+ resource: '',
334
+ provider: adminContext.notificationProvider,
335
+ });
284
336
  }
285
337
  });
286
338
 
@@ -301,7 +353,13 @@ export function useCustomMutation<TData = unknown, TError = HttpError, TVariable
301
353
  },
302
354
  onSuccess: (data, params) => {
303
355
  if (params.resource && params.invalidates !== false) {
304
- invalidateByScopes(queryClient, params.resource, params.invalidates, ['list', 'many'], undefined, dataProviderName);
356
+ invalidateByScopes({
357
+ queryClient,
358
+ resource: params.resource,
359
+ scopes: params.invalidates,
360
+ defaults: ['list', 'many'],
361
+ matcher: adminContext.queryKeyMatcher(params.resource, dataProviderName),
362
+ });
305
363
  }
306
364
  },
307
365
  onError: (error) => {
@@ -331,17 +389,38 @@ export function useCreateMany<TData extends BaseRecord = BaseRecord, TError = Ht
331
389
  },
332
390
  onSuccess: (data, params) => {
333
391
  const resName = params.resource ?? resource;
334
- fireSuccessNotification(undefined, 'Created successfully', data.data, params.variables, resName);
335
- audit({ action: 'create', resource: resName });
392
+ fireSuccessNotification({
393
+ config: undefined,
394
+ defaultMessage: 'Created successfully',
395
+ data: data.data,
396
+ values: params.variables,
397
+ resource: resName,
398
+ provider: adminContext.notificationProvider,
399
+ });
400
+ auditWithProvider(
401
+ { action: 'create', resource: resName, meta: adminContext.getProviderMeta(resName) },
402
+ adminContext.auditLogProvider,
403
+ );
336
404
  publishLiveEvent(resName, 'created', undefined, adminContext);
337
405
  },
338
406
  onError: (error, params) => {
339
407
  checkError(error, adminContext);
340
- fireErrorNotification(undefined, 'Create many failed', error, params.resource ?? resource);
408
+ fireErrorNotification({
409
+ config: undefined,
410
+ defaultMessage: 'Create many failed',
411
+ error,
412
+ resource: params.resource ?? resource,
413
+ provider: adminContext.notificationProvider,
414
+ });
341
415
  },
342
416
  onSettled: (_d, _e, params) => {
343
417
  const resName = params.resource ?? resource;
344
- invalidateByScopes(queryClient, resName, undefined, ['list', 'many'], undefined, params.dataProviderName);
418
+ invalidateByScopes({
419
+ queryClient,
420
+ resource: resName,
421
+ defaults: ['list', 'many'],
422
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
423
+ });
345
424
  },
346
425
  }));
347
426
 
@@ -365,19 +444,48 @@ export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = Ht
365
444
  },
366
445
  onSuccess: (data, params) => {
367
446
  const resName = params.resource ?? resource;
368
- fireSuccessNotification(undefined, 'Updated successfully', data.data, params.variables, resName);
369
- audit({ action: 'update', resource: resName });
447
+ fireSuccessNotification({
448
+ config: undefined,
449
+ defaultMessage: 'Updated successfully',
450
+ data: data.data,
451
+ values: params.variables,
452
+ resource: resName,
453
+ provider: adminContext.notificationProvider,
454
+ });
455
+ auditWithProvider(
456
+ { action: 'update', resource: resName, meta: adminContext.getProviderMeta(resName) },
457
+ adminContext.auditLogProvider,
458
+ );
370
459
  publishLiveEvent(resName, 'updated', params.ids, adminContext);
371
460
  },
372
461
  onError: (error, params) => {
373
462
  checkError(error, adminContext);
374
- fireErrorNotification(undefined, 'Update many failed', error, params.resource ?? resource);
463
+ fireErrorNotification({
464
+ config: undefined,
465
+ defaultMessage: 'Update many failed',
466
+ error,
467
+ resource: params.resource ?? resource,
468
+ provider: adminContext.notificationProvider,
469
+ });
375
470
  },
376
471
  onSettled: (_d, _e, params) => {
377
472
  const resName = params.resource ?? resource;
378
- invalidateByScopes(queryClient, resName, undefined, ['list', 'many', 'detail'], undefined, params.dataProviderName);
473
+ invalidateByScopes({
474
+ queryClient,
475
+ resource: resName,
476
+ defaults: ['list', 'many', 'detail'],
477
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
478
+ });
379
479
  for (const id of params.ids) {
380
- queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
480
+ queryClient.invalidateQueries({
481
+ predicate: (q) => queryKeyMatches(q.queryKey, {
482
+ ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
483
+ kind: 'data',
484
+ resource: resName,
485
+ action: 'one',
486
+ id,
487
+ }),
488
+ });
381
489
  }
382
490
  },
383
491
  }));
@@ -402,19 +510,47 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
402
510
  },
403
511
  onSuccess: (data, params) => {
404
512
  const resName = params.resource ?? resource;
405
- fireSuccessNotification(undefined, 'Deleted successfully', data.data, undefined, resName);
406
- audit({ action: 'delete', resource: resName });
513
+ fireSuccessNotification({
514
+ config: undefined,
515
+ defaultMessage: 'Deleted successfully',
516
+ data: data.data,
517
+ resource: resName,
518
+ provider: adminContext.notificationProvider,
519
+ });
520
+ auditWithProvider(
521
+ { action: 'delete', resource: resName, meta: adminContext.getProviderMeta(resName) },
522
+ adminContext.auditLogProvider,
523
+ );
407
524
  publishLiveEvent(resName, 'deleted', params.ids, adminContext);
408
525
  },
409
526
  onError: (error, params) => {
410
527
  checkError(error, adminContext);
411
- fireErrorNotification(undefined, 'Delete many failed', error, params.resource ?? resource);
528
+ fireErrorNotification({
529
+ config: undefined,
530
+ defaultMessage: 'Delete many failed',
531
+ error,
532
+ resource: params.resource ?? resource,
533
+ provider: adminContext.notificationProvider,
534
+ });
412
535
  },
413
536
  onSettled: (_d, _e, params) => {
414
537
  const resName = params.resource ?? resource;
415
- invalidateByScopes(queryClient, resName, undefined, ['list', 'many'], undefined, params.dataProviderName);
538
+ invalidateByScopes({
539
+ queryClient,
540
+ resource: resName,
541
+ defaults: ['list', 'many'],
542
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
543
+ });
416
544
  for (const id of params.ids) {
417
- queryClient.removeQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === id });
545
+ queryClient.removeQueries({
546
+ predicate: (q) => queryKeyMatches(q.queryKey, {
547
+ ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
548
+ kind: 'data',
549
+ resource: resName,
550
+ action: 'one',
551
+ id,
552
+ }),
553
+ });
418
554
  }
419
555
  },
420
556
  }));
@@ -426,22 +562,27 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
426
562
 
427
563
  export function useInvalidate() {
428
564
  const queryClient = useQueryClient();
565
+ const adminContext = captureAdminContext();
429
566
  return (params: { resource?: string; invalidates?: string[] | 'all' | false; id?: string | number; dataProviderName?: string }) => {
430
567
  if (params.invalidates === false) return;
431
568
  if (params.invalidates === 'all' || !params.resource) {
432
- queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName });
569
+ queryClient.invalidateQueries({
570
+ predicate: (q) => queryKeyMatches(q.queryKey, {
571
+ ...adminContext.queryKeyMatcher(undefined, params.dataProviderName),
572
+ kind: 'data',
573
+ }),
574
+ });
433
575
  return;
434
576
  }
435
- const scopes = params.invalidates || ['resourceAll'];
436
577
  const res = params.resource;
437
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === params.dataProviderName;
438
- for (const scope of scopes) {
439
- if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res });
440
- else if ((scope === 'detail' || scope === 'one') && params.id) queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'one' && q.queryKey[3] === params.id });
441
- else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'one' });
442
- else if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select') });
443
- else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res && q.queryKey[2] === 'many' });
444
- }
578
+ invalidateByScopes({
579
+ queryClient,
580
+ resource: res,
581
+ scopes: params.invalidates || ['resourceAll'],
582
+ defaults: ['resourceAll'],
583
+ id: params.id,
584
+ matcher: adminContext.queryKeyMatcher(res, params.dataProviderName),
585
+ });
445
586
  };
446
587
  }
447
588