@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.
package/src/index.ts CHANGED
@@ -8,9 +8,45 @@ export {
8
8
  setLiveProvider, getLiveProvider,
9
9
  setTaskProvider, getTaskProvider,
10
10
  createAdminContext, provideAdminContext, getAdminContext, captureAdminContext,
11
+ provideTenantContext, getTenantContext, getProviderBundle,
11
12
  resetContext,
12
13
  } from './context.svelte';
13
14
  export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor } from './context.svelte';
15
+ export {
16
+ createProviderBundle,
17
+ createTenantCacheKey,
18
+ defaultTenantAdapter,
19
+ isTenantCacheKey,
20
+ resolveTenantProviderMeta,
21
+ withProviderMeta,
22
+ withTenantDataProvider,
23
+ } from './provider-bundle';
24
+ export {
25
+ dataQueryMatches,
26
+ isQueryKey,
27
+ keys,
28
+ parseQueryKey,
29
+ queryKeyMatches,
30
+ queryKeys,
31
+ } from './query-keys';
32
+ export type {
33
+ DataQueryMatcher,
34
+ QueryDescriptor,
35
+ QueryKey,
36
+ QueryKeysBuilder,
37
+ QueryKeysContext,
38
+ QueryMatcher,
39
+ } from './query-keys';
40
+ export type {
41
+ ProviderBundle,
42
+ ProviderMetaInput,
43
+ ProviderMetaResolver,
44
+ TenantAdapter,
45
+ TenantCacheIdentity,
46
+ TenantCacheKey,
47
+ TenantContext,
48
+ TenantId,
49
+ } from './provider-bundle';
14
50
  export {
15
51
  useList, useInfiniteList,
16
52
  useOne, useShow,
@@ -36,10 +72,17 @@ export { readURLState, writeURLState } from './url-sync';
36
72
  export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync, createFeatureGate } from './permissions.svelte';
37
73
  export { useLive, useSubscription, usePublish } from './live.svelte';
38
74
  export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
39
- export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
75
+ export {
76
+ notify,
77
+ notifyWithProvider,
78
+ closeNotification,
79
+ setNotificationProvider,
80
+ getNotificationProvider,
81
+ } from './notification.svelte';
82
+ export type { NotificationParams } from './notification.svelte';
40
83
  export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
41
84
  export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
42
- export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
85
+ export { audit, auditWithProvider, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
43
86
  export type { AuditLogProvider } from './audit';
44
87
  export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
45
88
  export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
@@ -51,7 +94,6 @@ export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.s
51
94
  export { checkError } from './hook-utils.svelte';
52
95
  export type { NotificationConfig, OvertimeOptions, OvertimeResult, LiveSubscriptionParams } from './hook-utils.svelte';
53
96
 
54
- export * from './query-keys';
55
97
  export { HttpError, UndoError } from './types';
56
98
  export type {
57
99
  DataProvider, AuthProvider, NotificationProvider, MutationMode,
@@ -67,7 +109,8 @@ export type {
67
109
  DeleteParams, DeleteResult,
68
110
  DeleteManyParams, DeleteManyResult,
69
111
  Pagination, Sort, Filter, Identity,
70
- ResourceDefinition, FieldDefinition, MenuItem,
112
+ ResourceDefinition, ResourceProviderConfig, ResourceTransportConfig, ResourceAdapterConfig,
113
+ FieldDefinition, MenuItem,
71
114
  AuthActionResult, CheckResult,
72
115
  ResourceTypeMap, KnownResources, InferData,
73
116
  BaseRecord, Role, AuditLog
@@ -1,6 +1,8 @@
1
1
  // LiveProvider — Real-time subscription interface + hooks
2
2
 
3
3
  import { useQueryClient } from '@tanstack/svelte-query';
4
+ import { captureAdminContext } from './context.svelte';
5
+ import { dataQueryMatches } from './query-keys';
4
6
 
5
7
  // ─── Types ──────────────────────────────────────────────────────
6
8
 
@@ -34,6 +36,7 @@ export function useLive(
34
36
  options?: { liveMode?: LiveMode | (() => LiveMode); onLiveEvent?: (event: LiveEvent) => void; liveParams?: Record<string, unknown> | (() => Record<string, unknown>); dataProviderName?: string }
35
37
  ): void {
36
38
  const queryClient = useQueryClient();
39
+ const adminContext = captureAdminContext();
37
40
 
38
41
  $effect(() => {
39
42
  const lp = typeof liveProvider === 'function' ? liveProvider() : liveProvider;
@@ -50,9 +53,10 @@ export function useLive(
50
53
  callback: (event) => {
51
54
  options?.onLiveEvent?.(event);
52
55
  if (liveMode === 'auto') {
53
- const dpName = options?.dataProviderName;
54
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpName;
55
- queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res });
56
+ const matcher = adminContext.queryKeyMatcher(res, options?.dataProviderName);
57
+ queryClient.invalidateQueries({
58
+ predicate: (q) => dataQueryMatches(q.queryKey, { ...matcher, resource: res }),
59
+ });
56
60
  }
57
61
  },
58
62
  });
@@ -1,10 +1,11 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import { createMutation, useQueryClient } from '@tanstack/svelte-query';
3
2
  import { getAdminOptions } from './options.svelte';
4
3
  import { captureAdminContext } from './context.svelte';
4
+ import { dataQueryMatches } from './query-keys';
5
+ import type { QueryMatcher } from './query-keys';
5
6
  import type { AdminContextAccessor } from './context.svelte';
6
7
  import { useParsed } from './useParsed.svelte';
7
- import { audit } from './audit';
8
+ import { auditWithProvider } from './audit';
8
9
  import { UndoError } from './types';
9
10
  import type { BaseRecord, HttpError, KnownResources } from './types';
10
11
  import { checkError, createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
@@ -38,38 +39,45 @@ export function publishLiveEvent(
38
39
  * - update: ['list', 'many', 'detail']
39
40
  * - delete: ['list', 'many']
40
41
  */
41
- export function invalidateByScopes(
42
- queryClient: ReturnType<typeof useQueryClient>,
43
- resource: string,
44
- scopes: string[] | false | undefined,
45
- defaults: string[],
46
- id?: string | number,
47
- dataProviderName?: string
48
- ): void {
42
+ export interface InvalidateByScopesRequest {
43
+ queryClient: ReturnType<typeof useQueryClient>;
44
+ resource: string;
45
+ scopes?: string[] | false;
46
+ defaults: string[];
47
+ id?: string | number;
48
+ matcher: Pick<QueryMatcher, 'provider' | 'tenant'>;
49
+ }
50
+
51
+ export function invalidateByScopes(request: InvalidateByScopesRequest): void {
52
+ const { queryClient, resource, scopes, defaults, id, matcher } = request;
49
53
  if (scopes === false) return;
50
54
  const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
51
- const dpMatch = dataProviderName === undefined
52
- ? () => true
53
- : (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dataProviderName;
55
+ const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
56
+ ...matcher,
57
+ resource,
58
+ ...fields,
59
+ });
54
60
  for (const scope of effectiveScopes) {
55
- if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select' || q.queryKey[2] === 'select-defaults') });
56
- else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'many' });
57
- else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'one' && q.queryKey[3] === id });
58
- else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'one' });
59
- else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource });
61
+ if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => ['list', 'infiniteList', 'select', 'selectDefaults'].some((action) => dataMatch(q.queryKey, { action })) });
62
+ else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) });
63
+ else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id }) });
64
+ else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one' }) });
65
+ else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey) });
60
66
  }
61
67
  }
62
68
 
63
- export function deepMerge(target: any, source: any): any {
69
+ export function deepMerge(target: unknown, source: unknown): unknown {
64
70
  if (!target || typeof target !== 'object') return source;
65
71
  if (!source || typeof source !== 'object') return target;
66
72
  if (Array.isArray(source) || source instanceof Date) return source;
67
- const result = { ...target };
68
- for (const key in source) {
69
- if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && !(source[key] instanceof Date)) {
70
- result[key] = deepMerge(result[key] || {}, source[key]);
73
+ const targetRecord = target as Record<string, unknown>;
74
+ const sourceRecord = source as Record<string, unknown>;
75
+ const result: Record<string, unknown> = { ...targetRecord };
76
+ for (const [key, value] of Object.entries(sourceRecord)) {
77
+ if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
78
+ result[key] = deepMerge(targetRecord[key] ?? {}, value);
71
79
  } else {
72
- result[key] = source[key];
80
+ result[key] = value;
73
81
  }
74
82
  }
75
83
  return result;
@@ -133,21 +141,48 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
133
141
  onMutate: options.mutationOptions?.onMutate,
134
142
  onSuccess: (data, params, context) => {
135
143
  const resName = params.resource ?? defaultResource;
136
- fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
144
+ fireSuccessNotification({
145
+ config: params.successNotification,
146
+ defaultMessage: 'Created successfully',
147
+ data: data.data,
148
+ values: params.variables,
149
+ resource: resName,
150
+ provider: adminContext.notificationProvider,
151
+ });
137
152
  const res = adminContext.getResource(resName);
138
153
  const pk = res.primaryKey ?? 'id';
139
154
  const newId = (data.data as Record<string, unknown>)[pk];
140
- audit({ action: 'create', resource: resName, recordId: String(newId) });
155
+ auditWithProvider(
156
+ {
157
+ action: 'create',
158
+ resource: resName,
159
+ recordId: String(newId),
160
+ meta: adminContext.getProviderMeta(resName),
161
+ },
162
+ adminContext.auditLogProvider,
163
+ );
141
164
  publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
142
165
  // Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
143
- invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many'], undefined, params.dataProviderName);
166
+ invalidateByScopes({
167
+ queryClient,
168
+ resource: resName,
169
+ scopes: params.invalidates,
170
+ defaults: ['list', 'many'],
171
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
172
+ });
144
173
  if (typeof options.mutationOptions?.onSuccess === 'function') {
145
174
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, context);
146
175
  }
147
176
  },
148
177
  onError: (error, params, context) => {
149
178
  checkError(error, adminContext);
150
- fireErrorNotification(params.errorNotification, 'Create failed', error, params.resource ?? defaultResource);
179
+ fireErrorNotification({
180
+ config: params.errorNotification,
181
+ defaultMessage: 'Create failed',
182
+ error,
183
+ resource: params.resource ?? defaultResource,
184
+ provider: adminContext.notificationProvider,
185
+ });
151
186
  if (typeof options.mutationOptions?.onError === 'function') {
152
187
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
153
188
  }
@@ -232,12 +267,16 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
232
267
  if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
233
268
  const resName = params.resource ?? defaultResource;
234
269
  const targetId = params.id ?? defaultId;
235
- const dpN = params.dataProviderName;
236
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
270
+ const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
271
+ const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
272
+ ...matcher,
273
+ resource: resName,
274
+ ...fields,
275
+ });
237
276
 
238
- await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
277
+ await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
239
278
 
240
- const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
279
+ const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
241
280
 
242
281
  const pk = adminContext.getResource(resName).primaryKey ?? 'id';
243
282
  const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
@@ -245,14 +284,17 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
245
284
  if (updater.detail !== false) {
246
285
  const detailFn = updater.detail;
247
286
  if (typeof detailFn === 'function') {
248
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId }, (old: unknown) => detailFn(old, params.variables, targetId));
287
+ queryClient.setQueriesData(
288
+ { predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) },
289
+ (old: unknown) => detailFn(old, params.variables, targetId),
290
+ );
249
291
  } else {
250
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, params.variables) } : old);
292
+ 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 || {}, params.variables) } : old);
251
293
  }
252
294
  }
253
295
 
254
296
  if (updater.list !== false) {
255
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'list' }, (old: unknown) => {
297
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
256
298
  if (typeof updater.list === 'function') return updater.list(old, params.variables, targetId);
257
299
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
258
300
  const o = old as { data: Record<string, unknown>[] };
@@ -261,7 +303,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
261
303
  }
262
304
 
263
305
  if (updater.many !== false) {
264
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'many' }, (old: unknown) => {
306
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
265
307
  if (typeof updater.many === 'function') return updater.many(old, params.variables, targetId);
266
308
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
267
309
  const o = old as { data: Record<string, unknown>[] };
@@ -275,8 +317,23 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
275
317
  const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
276
318
  const resName = params.resource ?? defaultResource;
277
319
  const targetId = params.id ?? defaultId;
278
- fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
279
- audit({ action: 'update', resource: resName, recordId: String(targetId) });
320
+ fireSuccessNotification({
321
+ config: params.successNotification,
322
+ defaultMessage: 'Updated successfully',
323
+ data: data.data,
324
+ values: params.variables,
325
+ resource: resName,
326
+ provider: adminContext.notificationProvider,
327
+ });
328
+ auditWithProvider(
329
+ {
330
+ action: 'update',
331
+ resource: resName,
332
+ recordId: String(targetId),
333
+ meta: adminContext.getProviderMeta(resName),
334
+ },
335
+ adminContext.auditLogProvider,
336
+ );
280
337
  publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
281
338
  if (typeof options.mutationOptions?.onSuccess === 'function') {
282
339
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
@@ -286,7 +343,14 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
286
343
  if (error instanceof UndoError) return;
287
344
  const resName = params.resource ?? defaultResource;
288
345
  const targetId = params.id ?? defaultId;
289
- invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many', 'detail'], targetId != null ? targetId : undefined, params.dataProviderName);
346
+ invalidateByScopes({
347
+ queryClient,
348
+ resource: resName,
349
+ scopes: params.invalidates,
350
+ defaults: ['list', 'many', 'detail'],
351
+ id: targetId != null ? targetId : undefined,
352
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
353
+ });
290
354
  if (typeof options.mutationOptions?.onSettled === 'function') {
291
355
  (options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
292
356
  }
@@ -301,7 +365,13 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
301
365
  if (error instanceof UndoError) return;
302
366
  checkError(error, adminContext);
303
367
  const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
304
- fireErrorNotification(params.errorNotification, 'Update failed', error, params.resource ?? defaultResource);
368
+ fireErrorNotification({
369
+ config: params.errorNotification,
370
+ defaultMessage: 'Update failed',
371
+ error,
372
+ resource: params.resource ?? defaultResource,
373
+ provider: adminContext.notificationProvider,
374
+ });
305
375
  if (typeof options.mutationOptions?.onError === 'function') {
306
376
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
307
377
  }
@@ -381,25 +451,29 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
381
451
  if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
382
452
  const resName = params.resource ?? defaultResource;
383
453
  const targetId = params.id ?? defaultId;
384
- const dpN = params.dataProviderName;
385
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
454
+ const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
455
+ const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
456
+ ...matcher,
457
+ resource: resName,
458
+ ...fields,
459
+ });
386
460
 
387
- await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
461
+ await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
388
462
 
389
- const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
463
+ const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
390
464
 
391
465
  const pk = adminContext.getResource(resName).primaryKey ?? 'id';
392
466
 
393
- if (targetId != null) queryClient.removeQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
467
+ if (targetId != null) queryClient.removeQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) });
394
468
 
395
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'list' }, (old: unknown) => {
469
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
396
470
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
397
471
  const o = old as { data: Record<string, unknown>[]; total?: number };
398
472
  const filtered = o.data.filter((item) => String(item[pk]) !== String(targetId));
399
473
  return { ...o, data: filtered, total: (o.total ?? o.data.length) - (o.data.length - filtered.length) };
400
474
  });
401
475
 
402
- queryClient.setQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'many' }, (old: unknown) => {
476
+ queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
403
477
  if (!old || typeof old !== 'object' || !('data' in old)) return old;
404
478
  const o = old as { data: Record<string, unknown>[] };
405
479
  return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
@@ -414,11 +488,33 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
414
488
 
415
489
  // Remove detail cache entry (refine pattern — no stale show page)
416
490
  if (targetId != null) {
417
- queryClient.removeQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
491
+ queryClient.removeQueries({
492
+ predicate: (q) => dataQueryMatches(q.queryKey, {
493
+ ...adminContext.queryKeyMatcher(resName, params.dataProviderName),
494
+ resource: resName,
495
+ action: 'one',
496
+ id: targetId,
497
+ }),
498
+ });
418
499
  }
419
500
 
420
- fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
421
- audit({ action: 'delete', resource: resName, recordId: String(targetId) });
501
+ fireSuccessNotification({
502
+ config: params.successNotification,
503
+ defaultMessage: 'Deleted successfully',
504
+ data: data.data,
505
+ values: params.variables,
506
+ resource: resName,
507
+ provider: adminContext.notificationProvider,
508
+ });
509
+ auditWithProvider(
510
+ {
511
+ action: 'delete',
512
+ resource: resName,
513
+ recordId: String(targetId),
514
+ meta: adminContext.getProviderMeta(resName),
515
+ },
516
+ adminContext.auditLogProvider,
517
+ );
422
518
  publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
423
519
  if (typeof options.mutationOptions?.onSuccess === 'function') {
424
520
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
@@ -428,7 +524,14 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
428
524
  if (error instanceof UndoError) return;
429
525
  const resName = params.resource ?? defaultResource;
430
526
  const targetId = params.id ?? defaultId;
431
- invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many'], targetId != null ? targetId : undefined, params.dataProviderName);
527
+ invalidateByScopes({
528
+ queryClient,
529
+ resource: resName,
530
+ scopes: params.invalidates,
531
+ defaults: ['list', 'many'],
532
+ id: targetId != null ? targetId : undefined,
533
+ matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
534
+ });
432
535
  if (typeof options.mutationOptions?.onSettled === 'function') {
433
536
  (options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
434
537
  }
@@ -443,7 +546,13 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
443
546
  if (error instanceof UndoError) return;
444
547
  checkError(error, adminContext);
445
548
  const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
446
- fireErrorNotification(params.errorNotification, 'Delete failed', error, params.resource ?? defaultResource);
549
+ fireErrorNotification({
550
+ config: params.errorNotification,
551
+ defaultMessage: 'Delete failed',
552
+ error,
553
+ resource: params.resource ?? defaultResource,
554
+ provider: adminContext.notificationProvider,
555
+ });
447
556
  if (typeof options.mutationOptions?.onError === 'function') {
448
557
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
449
558
  }
@@ -17,14 +17,20 @@ export function getNotificationProvider(): NotificationProvider | null {
17
17
  /**
18
18
  * Send a notification through the configured provider, or fall back to toast.
19
19
  */
20
- export function notify(params: {
20
+ export interface NotificationParams {
21
21
  type: 'success' | 'error' | 'warning' | 'info';
22
22
  message: string;
23
23
  description?: string;
24
24
  key?: string;
25
- }): void {
26
- if (notificationProvider) {
27
- notificationProvider.open(params);
25
+ }
26
+
27
+ /** Send through an explicitly scoped provider, with the built-in toast as fallback. */
28
+ export function notifyWithProvider(
29
+ params: NotificationParams,
30
+ provider: NotificationProvider | null | undefined,
31
+ ): void {
32
+ if (provider) {
33
+ provider.open(params);
28
34
  } else {
29
35
  const { type, message, description } = params;
30
36
  const fullMessage = description ? `${message}: ${description}` : message;
@@ -37,6 +43,10 @@ export function notify(params: {
37
43
  }
38
44
  }
39
45
 
46
+ export function notify(params: NotificationParams): void {
47
+ notifyWithProvider(params, notificationProvider);
48
+ }
49
+
40
50
  /**
41
51
  * Close a notification by key (only works with custom provider).
42
52
  */