@svadmin/core 0.34.1 → 0.35.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 { appendTenantCacheKey, queryKeyMatchesTenant } from './provider-bundle';
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: appendTenantCacheKey([
462
+ options.dataProviderName, resource, 'one', currentId, queryMeta,
463
+ ], adminContext.tenantCacheKey),
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,25 @@ 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
+ res,
533
+ invalidateScopes,
534
+ ['list', 'many'],
535
+ undefined,
536
+ options.dataProviderName,
537
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
538
+ );
522
539
  if (!isManualSubmitIdentityCurrent()) return;
523
- if (successNotification !== false) notify({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.createSuccess') });
540
+ if (successNotification !== false) sendNotification({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.createSuccess') });
524
541
  const pk = adminContext.getResource(res).primaryKey ?? 'id';
525
542
  const newId = (data.data as Record<string, unknown>)[pk];
526
- audit({ action: 'create', resource: res, recordId: String(newId) });
543
+ recordAudit({
544
+ action: 'create',
545
+ resource: res,
546
+ recordId: String(newId),
547
+ meta: adminContext.getProviderMeta(res),
548
+ });
527
549
  try {
528
550
  const lp = adminContext.liveProvider;
529
551
  if (lp?.publish) lp.publish({ type: 'INSERT', resource: res, payload: { ids: newId != null ? [newId as string | number] : [] } });
@@ -563,7 +585,8 @@ export function useForm<
563
585
  const targetId = currentId;
564
586
  const targetResource = resource;
565
587
  const dpN = options.dataProviderName;
566
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
588
+ const dp = (q: { queryKey: readonly unknown[] }) =>
589
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpN;
567
590
  await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resource });
568
591
  const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resource });
569
592
  if (optimisticUpdateMap?.list !== false) {
@@ -584,8 +607,13 @@ export function useForm<
584
607
  const targetId = ctx?.targetId ?? currentId;
585
608
  const res = ctx?.targetResource ?? resource;
586
609
  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) });
610
+ if (successNotification !== false) sendNotification({ type: 'success', message: typeof successNotification === 'string' ? successNotification : i18n.t('common.updateSuccess') });
611
+ recordAudit({
612
+ action: 'update',
613
+ resource: res,
614
+ recordId: String(targetId),
615
+ meta: adminContext.getProviderMeta(res),
616
+ });
589
617
  try {
590
618
  const lp = adminContext.liveProvider;
591
619
  if (lp?.publish) lp.publish({ type: 'UPDATE', resource: res, payload: { ids: targetId != null ? [targetId] : [] } });
@@ -598,7 +626,15 @@ export function useForm<
598
626
  const ctx = context as { targetId?: string | number; targetResource?: string } | undefined;
599
627
  const targetId = ctx?.targetId ?? currentId;
600
628
  const res = ctx?.targetResource ?? resource;
601
- invalidateByScopes(queryClient, res, invalidateScopes, ['list', 'many', 'detail'], targetId ?? undefined, options.dataProviderName);
629
+ invalidateByScopes(
630
+ queryClient,
631
+ res,
632
+ invalidateScopes,
633
+ ['list', 'many', 'detail'],
634
+ targetId ?? undefined,
635
+ options.dataProviderName,
636
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
637
+ );
602
638
  },
603
639
  onError: (error: unknown, _vars: unknown, context: unknown) => {
604
640
  const ctx = context as { previousQueries?: [unknown, unknown][] } | undefined;
@@ -649,7 +685,7 @@ export function useForm<
649
685
 
650
686
  if (!isRecordIdentityCurrent(submitIdentity)) return;
651
687
 
652
- if (!runValidation()) { notify({ type: 'warning', message: i18n.t('validation.required') }); return; }
688
+ if (!runValidation()) { sendNotification({ type: 'warning', message: i18n.t('validation.required') }); return; }
653
689
 
654
690
  const submittedValues = createPersistenceSnapshot(values);
655
691
  if (action === 'create' || action === 'clone') await createMut.mutateAsync(submittedValues);
@@ -740,7 +776,15 @@ export function useForm<
740
776
  await provider.update<TData, TVariables>({ resource: safeResource, id: safeId as string | number, variables: submittedValues, meta: safeMeta });
741
777
  if (formDestroyed) return;
742
778
  const scopes = autoSaveOpts.invalidates ?? ['resourceAll'];
743
- invalidateByScopes(queryClient, safeResource, scopes, ['resourceAll'], safeId ?? undefined, options.dataProviderName);
779
+ invalidateByScopes(
780
+ queryClient,
781
+ safeResource,
782
+ scopes,
783
+ ['resourceAll'],
784
+ safeId ?? undefined,
785
+ options.dataProviderName,
786
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
787
+ );
744
788
  if (!isRecordIdentityCurrent(scheduledIdentity)) return;
745
789
  autoSaveStatus = 'saved';
746
790
  lastAutoSaveData = submittedValues;
@@ -769,7 +813,11 @@ 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) => queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
818
+ && q.queryKey[0] === options.dataProviderName
819
+ && q.queryKey[1] === resource,
820
+ });
773
821
  });
774
822
  }
775
823
 
@@ -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 { queryKeyMatchesTenant } from './provider-bundle';
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();
@@ -106,7 +109,8 @@ export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams):
106
109
  params.onLiveEvent?.(event);
107
110
  if (liveMode === 'auto') {
108
111
  const dpN = params.dataProviderName;
109
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
112
+ const dpMatch = (q: { queryKey: readonly unknown[] }) =>
113
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpN;
110
114
  queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === params.resource });
111
115
  }
112
116
  },
@@ -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
  }
@@ -24,13 +24,14 @@ export type { OvertimeResult, OvertimeOptions, NotificationConfig } from './hook
24
24
  import { createQuery, createInfiniteQuery, createMutation, useQueryClient } from '@tanstack/svelte-query';
25
25
  import { getAdminOptions } from './options.svelte';
26
26
  import { captureAdminContext } from './context.svelte';
27
+ import { appendTenantCacheKey, queryKeyMatchesTenant } from './provider-bundle';
27
28
  import { useParsed } from './useParsed.svelte';
28
29
  import { createOvertimeTracker, createLiveSubscription, fireSuccessNotification, fireErrorNotification, checkError } from './hook-utils.svelte';
29
30
  import { invalidateByScopes, publishLiveEvent } from './mutation-hooks.svelte';
30
31
  import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
31
32
  import type { BaseRecord, HttpError, Pagination, Sort, Filter, DataProvider, KnownResources } from './types';
32
33
  import type { LiveMode, LiveEvent } from './live.svelte';
33
- import { audit } from './audit';
34
+ import { auditWithProvider } from './audit';
34
35
 
35
36
  // ─── useInfiniteList ────────────────────────────────────────────────
36
37
 
@@ -58,7 +59,15 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
58
59
  const resource = options.resource ?? parsed.resource ?? '';
59
60
  const provider = adminContext.getDataProviderForResource(resource, options.dataProviderName);
60
61
  return {
61
- queryKey: [options.dataProviderName, resource, 'infiniteList', options.pagination?.pageSize, options.sorters, options.filters, options.meta],
62
+ queryKey: appendTenantCacheKey([
63
+ options.dataProviderName,
64
+ resource,
65
+ 'infiniteList',
66
+ options.pagination?.pageSize,
67
+ options.sorters,
68
+ options.filters,
69
+ options.meta,
70
+ ], adminContext.tenantCacheKey),
62
71
  queryFn: async ({ pageParam = 1 }) => {
63
72
  const result = await provider.getList<TData>({
64
73
  resource,
@@ -99,11 +108,23 @@ export function useInfiniteList<TData extends BaseRecord = BaseRecord, TError =
99
108
  $effect(() => {
100
109
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
101
110
  lastSuccessAt = query.dataUpdatedAt;
102
- fireSuccessNotification(options.successNotification, '', query.data, undefined, options.resource ?? parsed.resource ?? '');
111
+ fireSuccessNotification({
112
+ config: options.successNotification,
113
+ defaultMessage: '',
114
+ data: query.data,
115
+ resource: options.resource ?? parsed.resource ?? '',
116
+ provider: adminContext.notificationProvider,
117
+ });
103
118
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
104
119
  lastErrorAt = query.errorUpdatedAt;
105
120
  checkError(query.error, adminContext);
106
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, options.resource ?? parsed.resource ?? '');
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
+ });
107
128
  }
108
129
  });
109
130
 
@@ -152,7 +173,9 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
152
173
  const query = createQuery<{ data: TData[]; total: number }>(() => {
153
174
  const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
154
175
  return {
155
- queryKey: [dataProviderName, resource, 'select', allFilters, sorters, pagination, meta],
176
+ queryKey: appendTenantCacheKey([
177
+ dataProviderName, resource, 'select', allFilters, sorters, pagination, meta,
178
+ ], adminContext.tenantCacheKey),
156
179
  queryFn: () => provider.getList<TData>({ resource, sorters, filters: allFilters, pagination: { current: 1, pageSize: effectivePageSize }, meta }),
157
180
  enabled: options.queryOptions?.enabled ?? true,
158
181
  staleTime: options.queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
@@ -165,7 +188,9 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
165
188
  ? createQuery<{ data: TData[] }>(() => {
166
189
  const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
167
190
  return {
168
- queryKey: [dataProviderName, resource, 'select-defaults', defaultValueIds],
191
+ queryKey: appendTenantCacheKey([
192
+ dataProviderName, resource, 'select-defaults', defaultValueIds,
193
+ ], adminContext.tenantCacheKey),
169
194
  queryFn: async () => {
170
195
  if (provider.getMany) return provider.getMany<TData>({ resource, ids: defaultValueIds, meta });
171
196
  const results = await Promise.all(defaultValueIds.map(id => provider.getOne<TData>({ resource, id, meta })));
@@ -203,11 +228,23 @@ export function useSelect<TData extends BaseRecord = BaseRecord, TOption = { lab
203
228
  $effect(() => {
204
229
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
205
230
  lastSuccessAt = query.dataUpdatedAt;
206
- fireSuccessNotification(options.successNotification, '', query.data, undefined, resource);
231
+ fireSuccessNotification({
232
+ config: options.successNotification,
233
+ defaultMessage: '',
234
+ data: query.data,
235
+ resource,
236
+ provider: adminContext.notificationProvider,
237
+ });
207
238
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
208
239
  lastErrorAt = query.errorUpdatedAt;
209
240
  checkError(query.error, adminContext);
210
- fireErrorNotification(options.errorNotification, 'Fetch failed', query.error, resource);
241
+ fireErrorNotification({
242
+ config: options.errorNotification,
243
+ defaultMessage: 'Fetch failed',
244
+ error: query.error,
245
+ resource,
246
+ provider: adminContext.notificationProvider,
247
+ });
211
248
  }
212
249
  });
213
250
 
@@ -250,7 +287,9 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
250
287
  const query = createQuery<{ data: TData }, TError>(() => {
251
288
  const provider = adminContext.getDataProvider(options.dataProviderName);
252
289
  return {
253
- queryKey: [options.dataProviderName, 'custom', options.url, options.method, options.config, options.meta],
290
+ queryKey: appendTenantCacheKey([
291
+ options.dataProviderName, 'custom', options.url, options.method, options.config, options.meta,
292
+ ], adminContext.tenantCacheKey),
254
293
  queryFn: async () => {
255
294
  if (!provider.custom) throw new Error('DataProvider does not support custom method');
256
295
  return provider.custom<TData>({
@@ -276,11 +315,22 @@ export function useCustom<TData = unknown, TError = HttpError>(options: UseCusto
276
315
  $effect(() => {
277
316
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt && options.successNotification) {
278
317
  lastSuccessAt = query.dataUpdatedAt;
279
- fireSuccessNotification(options.successNotification, '', query.data);
318
+ fireSuccessNotification({
319
+ config: options.successNotification,
320
+ defaultMessage: '',
321
+ data: query.data,
322
+ provider: adminContext.notificationProvider,
323
+ });
280
324
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
281
325
  lastErrorAt = query.errorUpdatedAt;
282
326
  checkError(query.error, adminContext);
283
- fireErrorNotification(options.errorNotification, 'Custom request failed', query.error, '');
327
+ fireErrorNotification({
328
+ config: options.errorNotification,
329
+ defaultMessage: 'Custom request failed',
330
+ error: query.error,
331
+ resource: '',
332
+ provider: adminContext.notificationProvider,
333
+ });
284
334
  }
285
335
  });
286
336
 
@@ -301,7 +351,15 @@ export function useCustomMutation<TData = unknown, TError = HttpError, TVariable
301
351
  },
302
352
  onSuccess: (data, params) => {
303
353
  if (params.resource && params.invalidates !== false) {
304
- invalidateByScopes(queryClient, params.resource, params.invalidates, ['list', 'many'], undefined, dataProviderName);
354
+ invalidateByScopes(
355
+ queryClient,
356
+ params.resource,
357
+ params.invalidates,
358
+ ['list', 'many'],
359
+ undefined,
360
+ dataProviderName,
361
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
362
+ );
305
363
  }
306
364
  },
307
365
  onError: (error) => {
@@ -331,17 +389,41 @@ 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
+ resName,
421
+ undefined,
422
+ ['list', 'many'],
423
+ undefined,
424
+ params.dataProviderName,
425
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
426
+ );
345
427
  },
346
428
  }));
347
429
 
@@ -365,19 +447,49 @@ export function useUpdateMany<TData extends BaseRecord = BaseRecord, TError = Ht
365
447
  },
366
448
  onSuccess: (data, params) => {
367
449
  const resName = params.resource ?? resource;
368
- fireSuccessNotification(undefined, 'Updated successfully', data.data, params.variables, resName);
369
- audit({ action: 'update', resource: resName });
450
+ fireSuccessNotification({
451
+ config: undefined,
452
+ defaultMessage: 'Updated successfully',
453
+ data: data.data,
454
+ values: params.variables,
455
+ resource: resName,
456
+ provider: adminContext.notificationProvider,
457
+ });
458
+ auditWithProvider(
459
+ { action: 'update', resource: resName, meta: adminContext.getProviderMeta(resName) },
460
+ adminContext.auditLogProvider,
461
+ );
370
462
  publishLiveEvent(resName, 'updated', params.ids, adminContext);
371
463
  },
372
464
  onError: (error, params) => {
373
465
  checkError(error, adminContext);
374
- fireErrorNotification(undefined, 'Update many failed', error, params.resource ?? resource);
466
+ fireErrorNotification({
467
+ config: undefined,
468
+ defaultMessage: 'Update many failed',
469
+ error,
470
+ resource: params.resource ?? resource,
471
+ provider: adminContext.notificationProvider,
472
+ });
375
473
  },
376
474
  onSettled: (_d, _e, params) => {
377
475
  const resName = params.resource ?? resource;
378
- invalidateByScopes(queryClient, resName, undefined, ['list', 'many', 'detail'], undefined, params.dataProviderName);
476
+ invalidateByScopes(
477
+ queryClient,
478
+ resName,
479
+ undefined,
480
+ ['list', 'many', 'detail'],
481
+ undefined,
482
+ params.dataProviderName,
483
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
484
+ );
379
485
  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 });
486
+ queryClient.invalidateQueries({
487
+ predicate: (q) => queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
488
+ && q.queryKey[0] === params.dataProviderName
489
+ && q.queryKey[1] === resName
490
+ && q.queryKey[2] === 'one'
491
+ && q.queryKey[3] === id,
492
+ });
381
493
  }
382
494
  },
383
495
  }));
@@ -402,19 +514,48 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
402
514
  },
403
515
  onSuccess: (data, params) => {
404
516
  const resName = params.resource ?? resource;
405
- fireSuccessNotification(undefined, 'Deleted successfully', data.data, undefined, resName);
406
- audit({ action: 'delete', resource: resName });
517
+ fireSuccessNotification({
518
+ config: undefined,
519
+ defaultMessage: 'Deleted successfully',
520
+ data: data.data,
521
+ resource: resName,
522
+ provider: adminContext.notificationProvider,
523
+ });
524
+ auditWithProvider(
525
+ { action: 'delete', resource: resName, meta: adminContext.getProviderMeta(resName) },
526
+ adminContext.auditLogProvider,
527
+ );
407
528
  publishLiveEvent(resName, 'deleted', params.ids, adminContext);
408
529
  },
409
530
  onError: (error, params) => {
410
531
  checkError(error, adminContext);
411
- fireErrorNotification(undefined, 'Delete many failed', error, params.resource ?? resource);
532
+ fireErrorNotification({
533
+ config: undefined,
534
+ defaultMessage: 'Delete many failed',
535
+ error,
536
+ resource: params.resource ?? resource,
537
+ provider: adminContext.notificationProvider,
538
+ });
412
539
  },
413
540
  onSettled: (_d, _e, params) => {
414
541
  const resName = params.resource ?? resource;
415
- invalidateByScopes(queryClient, resName, undefined, ['list', 'many'], undefined, params.dataProviderName);
542
+ invalidateByScopes(
543
+ queryClient,
544
+ resName,
545
+ undefined,
546
+ ['list', 'many'],
547
+ undefined,
548
+ params.dataProviderName,
549
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
550
+ );
416
551
  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 });
552
+ queryClient.removeQueries({
553
+ predicate: (q) => queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
554
+ && q.queryKey[0] === params.dataProviderName
555
+ && q.queryKey[1] === resName
556
+ && q.queryKey[2] === 'one'
557
+ && q.queryKey[3] === id,
558
+ });
418
559
  }
419
560
  },
420
561
  }));
@@ -426,15 +567,21 @@ export function useDeleteMany<TData extends BaseRecord = BaseRecord, TError = Ht
426
567
 
427
568
  export function useInvalidate() {
428
569
  const queryClient = useQueryClient();
570
+ const adminContext = captureAdminContext();
429
571
  return (params: { resource?: string; invalidates?: string[] | 'all' | false; id?: string | number; dataProviderName?: string }) => {
430
572
  if (params.invalidates === false) return;
431
573
  if (params.invalidates === 'all' || !params.resource) {
432
- queryClient.invalidateQueries({ predicate: (q) => q.queryKey[0] === params.dataProviderName });
574
+ queryClient.invalidateQueries({
575
+ predicate: (q) => queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
576
+ && q.queryKey[0] === params.dataProviderName,
577
+ });
433
578
  return;
434
579
  }
435
580
  const scopes = params.invalidates || ['resourceAll'];
436
581
  const res = params.resource;
437
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === params.dataProviderName;
582
+ const dpMatch = (q: { queryKey: readonly unknown[] }) =>
583
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
584
+ && q.queryKey[0] === params.dataProviderName;
438
585
  for (const scope of scopes) {
439
586
  if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res });
440
587
  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 });
package/src/index.ts CHANGED
@@ -8,9 +8,31 @@ 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
+ appendTenantCacheKey,
17
+ createProviderBundle,
18
+ createTenantCacheKey,
19
+ defaultTenantAdapter,
20
+ isTenantCacheKey,
21
+ queryKeyMatchesTenant,
22
+ resolveTenantProviderMeta,
23
+ withProviderMeta,
24
+ withTenantDataProvider,
25
+ } from './provider-bundle';
26
+ export type {
27
+ ProviderBundle,
28
+ ProviderMetaInput,
29
+ ProviderMetaResolver,
30
+ TenantAdapter,
31
+ TenantCacheIdentity,
32
+ TenantCacheKey,
33
+ TenantContext,
34
+ TenantId,
35
+ } from './provider-bundle';
14
36
  export {
15
37
  useList, useInfiniteList,
16
38
  useOne, useShow,
@@ -36,10 +58,17 @@ export { readURLState, writeURLState } from './url-sync';
36
58
  export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync, createFeatureGate } from './permissions.svelte';
37
59
  export { useLive, useSubscription, usePublish } from './live.svelte';
38
60
  export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
39
- export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
61
+ export {
62
+ notify,
63
+ notifyWithProvider,
64
+ closeNotification,
65
+ setNotificationProvider,
66
+ getNotificationProvider,
67
+ } from './notification.svelte';
68
+ export type { NotificationParams } from './notification.svelte';
40
69
  export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
41
70
  export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
42
- export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
71
+ export { audit, auditWithProvider, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
43
72
  export type { AuditLogProvider } from './audit';
44
73
  export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
45
74
  export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
@@ -67,7 +96,8 @@ export type {
67
96
  DeleteParams, DeleteResult,
68
97
  DeleteManyParams, DeleteManyResult,
69
98
  Pagination, Sort, Filter, Identity,
70
- ResourceDefinition, FieldDefinition, MenuItem,
99
+ ResourceDefinition, ResourceProviderConfig, ResourceTransportConfig, ResourceAdapterConfig,
100
+ FieldDefinition, MenuItem,
71
101
  AuthActionResult, CheckResult,
72
102
  ResourceTypeMap, KnownResources, InferData,
73
103
  BaseRecord, Role, AuditLog