@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.
@@ -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 { queryKeyMatchesTenant } from './provider-bundle';
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;
@@ -51,7 +54,8 @@ export function useLive(
51
54
  options?.onLiveEvent?.(event);
52
55
  if (liveMode === 'auto') {
53
56
  const dpName = options?.dataProviderName;
54
- const dpMatch = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpName;
57
+ const dpMatch = (q: { queryKey: readonly unknown[] }) =>
58
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpName;
55
59
  queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === res });
56
60
  }
57
61
  },
@@ -2,9 +2,10 @@
2
2
  import { createMutation, useQueryClient } from '@tanstack/svelte-query';
3
3
  import { getAdminOptions } from './options.svelte';
4
4
  import { captureAdminContext } from './context.svelte';
5
+ import { queryKeyMatchesTenant } from './provider-bundle';
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';
@@ -44,13 +45,14 @@ export function invalidateByScopes(
44
45
  scopes: string[] | false | undefined,
45
46
  defaults: string[],
46
47
  id?: string | number,
47
- dataProviderName?: string
48
+ dataProviderName?: string,
49
+ queryMatches: (queryKey: readonly unknown[]) => boolean = () => true,
48
50
  ): void {
49
51
  if (scopes === false) return;
50
52
  const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
51
53
  const dpMatch = dataProviderName === undefined
52
- ? () => true
53
- : (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dataProviderName;
54
+ ? (q: { queryKey: readonly unknown[] }) => queryMatches(q.queryKey)
55
+ : (q: { queryKey: readonly unknown[] }) => queryMatches(q.queryKey) && q.queryKey[0] === dataProviderName;
54
56
  for (const scope of effectiveScopes) {
55
57
  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
58
  else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'many' });
@@ -133,21 +135,50 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
133
135
  onMutate: options.mutationOptions?.onMutate,
134
136
  onSuccess: (data, params, context) => {
135
137
  const resName = params.resource ?? defaultResource;
136
- fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
138
+ fireSuccessNotification({
139
+ config: params.successNotification,
140
+ defaultMessage: 'Created successfully',
141
+ data: data.data,
142
+ values: params.variables,
143
+ resource: resName,
144
+ provider: adminContext.notificationProvider,
145
+ });
137
146
  const res = adminContext.getResource(resName);
138
147
  const pk = res.primaryKey ?? 'id';
139
148
  const newId = (data.data as Record<string, unknown>)[pk];
140
- audit({ action: 'create', resource: resName, recordId: String(newId) });
149
+ auditWithProvider(
150
+ {
151
+ action: 'create',
152
+ resource: resName,
153
+ recordId: String(newId),
154
+ meta: adminContext.getProviderMeta(resName),
155
+ },
156
+ adminContext.auditLogProvider,
157
+ );
141
158
  publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
142
159
  // 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);
160
+ invalidateByScopes(
161
+ queryClient,
162
+ resName,
163
+ params.invalidates,
164
+ ['list', 'many'],
165
+ undefined,
166
+ params.dataProviderName,
167
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
168
+ );
144
169
  if (typeof options.mutationOptions?.onSuccess === 'function') {
145
170
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, context);
146
171
  }
147
172
  },
148
173
  onError: (error, params, context) => {
149
174
  checkError(error, adminContext);
150
- fireErrorNotification(params.errorNotification, 'Create failed', error, params.resource ?? defaultResource);
175
+ fireErrorNotification({
176
+ config: params.errorNotification,
177
+ defaultMessage: 'Create failed',
178
+ error,
179
+ resource: params.resource ?? defaultResource,
180
+ provider: adminContext.notificationProvider,
181
+ });
151
182
  if (typeof options.mutationOptions?.onError === 'function') {
152
183
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
153
184
  }
@@ -233,7 +264,8 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
233
264
  const resName = params.resource ?? defaultResource;
234
265
  const targetId = params.id ?? defaultId;
235
266
  const dpN = params.dataProviderName;
236
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
267
+ const dp = (q: { queryKey: readonly unknown[] }) =>
268
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpN;
237
269
 
238
270
  await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
239
271
 
@@ -275,8 +307,23 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
275
307
  const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
276
308
  const resName = params.resource ?? defaultResource;
277
309
  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) });
310
+ fireSuccessNotification({
311
+ config: params.successNotification,
312
+ defaultMessage: 'Updated successfully',
313
+ data: data.data,
314
+ values: params.variables,
315
+ resource: resName,
316
+ provider: adminContext.notificationProvider,
317
+ });
318
+ auditWithProvider(
319
+ {
320
+ action: 'update',
321
+ resource: resName,
322
+ recordId: String(targetId),
323
+ meta: adminContext.getProviderMeta(resName),
324
+ },
325
+ adminContext.auditLogProvider,
326
+ );
280
327
  publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
281
328
  if (typeof options.mutationOptions?.onSuccess === 'function') {
282
329
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
@@ -286,7 +333,15 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
286
333
  if (error instanceof UndoError) return;
287
334
  const resName = params.resource ?? defaultResource;
288
335
  const targetId = params.id ?? defaultId;
289
- invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many', 'detail'], targetId != null ? targetId : undefined, params.dataProviderName);
336
+ invalidateByScopes(
337
+ queryClient,
338
+ resName,
339
+ params.invalidates,
340
+ ['list', 'many', 'detail'],
341
+ targetId != null ? targetId : undefined,
342
+ params.dataProviderName,
343
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
344
+ );
290
345
  if (typeof options.mutationOptions?.onSettled === 'function') {
291
346
  (options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
292
347
  }
@@ -301,7 +356,13 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
301
356
  if (error instanceof UndoError) return;
302
357
  checkError(error, adminContext);
303
358
  const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
304
- fireErrorNotification(params.errorNotification, 'Update failed', error, params.resource ?? defaultResource);
359
+ fireErrorNotification({
360
+ config: params.errorNotification,
361
+ defaultMessage: 'Update failed',
362
+ error,
363
+ resource: params.resource ?? defaultResource,
364
+ provider: adminContext.notificationProvider,
365
+ });
305
366
  if (typeof options.mutationOptions?.onError === 'function') {
306
367
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
307
368
  }
@@ -382,7 +443,8 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
382
443
  const resName = params.resource ?? defaultResource;
383
444
  const targetId = params.id ?? defaultId;
384
445
  const dpN = params.dataProviderName;
385
- const dp = (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dpN;
446
+ const dp = (q: { queryKey: readonly unknown[] }) =>
447
+ queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey) && q.queryKey[0] === dpN;
386
448
 
387
449
  await queryClient.cancelQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
388
450
 
@@ -414,11 +476,32 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
414
476
 
415
477
  // Remove detail cache entry (refine pattern — no stale show page)
416
478
  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 });
479
+ queryClient.removeQueries({
480
+ predicate: (q) => queryKeyMatchesTenant(q.queryKey, adminContext.tenantCacheKey)
481
+ && q.queryKey[0] === params.dataProviderName
482
+ && q.queryKey[1] === resName
483
+ && q.queryKey[2] === 'one'
484
+ && q.queryKey[3] === targetId,
485
+ });
418
486
  }
419
487
 
420
- fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
421
- audit({ action: 'delete', resource: resName, recordId: String(targetId) });
488
+ fireSuccessNotification({
489
+ config: params.successNotification,
490
+ defaultMessage: 'Deleted successfully',
491
+ data: data.data,
492
+ values: params.variables,
493
+ resource: resName,
494
+ provider: adminContext.notificationProvider,
495
+ });
496
+ auditWithProvider(
497
+ {
498
+ action: 'delete',
499
+ resource: resName,
500
+ recordId: String(targetId),
501
+ meta: adminContext.getProviderMeta(resName),
502
+ },
503
+ adminContext.auditLogProvider,
504
+ );
422
505
  publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
423
506
  if (typeof options.mutationOptions?.onSuccess === 'function') {
424
507
  (options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
@@ -428,7 +511,15 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
428
511
  if (error instanceof UndoError) return;
429
512
  const resName = params.resource ?? defaultResource;
430
513
  const targetId = params.id ?? defaultId;
431
- invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many'], targetId != null ? targetId : undefined, params.dataProviderName);
514
+ invalidateByScopes(
515
+ queryClient,
516
+ resName,
517
+ params.invalidates,
518
+ ['list', 'many'],
519
+ targetId != null ? targetId : undefined,
520
+ params.dataProviderName,
521
+ (queryKey) => queryKeyMatchesTenant(queryKey, adminContext.tenantCacheKey),
522
+ );
432
523
  if (typeof options.mutationOptions?.onSettled === 'function') {
433
524
  (options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
434
525
  }
@@ -443,7 +534,13 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
443
534
  if (error instanceof UndoError) return;
444
535
  checkError(error, adminContext);
445
536
  const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
446
- fireErrorNotification(params.errorNotification, 'Delete failed', error, params.resource ?? defaultResource);
537
+ fireErrorNotification({
538
+ config: params.errorNotification,
539
+ defaultMessage: 'Delete failed',
540
+ error,
541
+ resource: params.resource ?? defaultResource,
542
+ provider: adminContext.notificationProvider,
543
+ });
447
544
  if (typeof options.mutationOptions?.onError === 'function') {
448
545
  (options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
449
546
  }
@@ -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
  */
@@ -0,0 +1,200 @@
1
+ import type {
2
+ AgentProvider,
3
+ ChatProvider,
4
+ } from './chatProvider.svelte';
5
+ import type { AuditLogProvider } from './audit';
6
+ import type { AccessControlProvider } from './permissions.svelte';
7
+ import type { LiveProvider } from './live.svelte';
8
+ import type { RouterProvider } from './router-provider';
9
+ import type {
10
+ AuthProvider,
11
+ BaseRecord,
12
+ CreateManyParams,
13
+ CreateManyResult,
14
+ CreateParams,
15
+ CreateResult,
16
+ CustomParams,
17
+ CustomResult,
18
+ DataProvider,
19
+ DeleteManyParams,
20
+ DeleteManyResult,
21
+ DeleteParams,
22
+ DeleteResult,
23
+ GetListParams,
24
+ GetListResult,
25
+ GetManyParams,
26
+ GetManyResult,
27
+ GetOneParams,
28
+ GetOneResult,
29
+ NotificationProvider,
30
+ TaskProvider,
31
+ UpdateManyParams,
32
+ UpdateManyResult,
33
+ UpdateParams,
34
+ UpdateResult,
35
+ } from './types';
36
+
37
+ export type DataProviderInput = DataProvider | Record<string, DataProvider>;
38
+
39
+ /**
40
+ * 一等 Provider 配置对象。字段名与现有独立 provider API 保持一致,
41
+ * 因此调用方可以渐进地从多个 props 迁移为一个 typed bundle。
42
+ */
43
+ export interface ProviderBundle {
44
+ readonly dataProvider: DataProviderInput;
45
+ readonly authProvider?: AuthProvider | null;
46
+ readonly accessControlProvider?: AccessControlProvider | null;
47
+ readonly liveProvider?: LiveProvider;
48
+ readonly auditLogProvider?: AuditLogProvider | null;
49
+ readonly notificationProvider?: NotificationProvider | null;
50
+ readonly chatProvider?: ChatProvider | null;
51
+ readonly agentProvider?: AgentProvider | null;
52
+ readonly taskProvider?: TaskProvider;
53
+ readonly routerProvider?: RouterProvider;
54
+ readonly tenantAdapter?: TenantAdapter;
55
+ }
56
+
57
+ /** 保留传入对象的精确类型,同时执行 ProviderBundle 的编译期校验。 */
58
+ export function createProviderBundle<const TBundle extends ProviderBundle>(bundle: TBundle): TBundle {
59
+ return bundle;
60
+ }
61
+
62
+ export type TenantId = string | number;
63
+ export type TenantCacheIdentity = string | number;
64
+
65
+ /** 请求树内的租户状态;不得通过模块级变量在 SSR 请求之间共享。 */
66
+ export interface TenantContext {
67
+ readonly tenantId: TenantId;
68
+ readonly meta?: Readonly<Record<string, unknown>>;
69
+ }
70
+
71
+ /**
72
+ * 将标准 TenantContext 投影到具体后端所需的 provider meta 与 cache identity。
73
+ * Core 始终额外写入标准 `tenantId`,adapter 不能覆盖该字段。
74
+ */
75
+ export interface TenantAdapter {
76
+ readonly getProviderMeta?: (tenant: TenantContext) => Readonly<Record<string, unknown>>;
77
+ readonly getCacheIdentity?: (tenant: TenantContext) => TenantCacheIdentity;
78
+ }
79
+
80
+ export interface TenantCacheKey {
81
+ readonly __svadminTenant: TenantCacheIdentity;
82
+ }
83
+
84
+ export const defaultTenantAdapter: Required<TenantAdapter> = {
85
+ getProviderMeta: (tenant) => tenant.meta ?? {},
86
+ getCacheIdentity: (tenant) => tenant.tenantId,
87
+ };
88
+
89
+ export function resolveTenantProviderMeta(
90
+ tenant: TenantContext,
91
+ adapter: TenantAdapter = defaultTenantAdapter,
92
+ ): Record<string, unknown> {
93
+ return {
94
+ ...(tenant.meta ?? {}),
95
+ ...(adapter.getProviderMeta?.(tenant) ?? {}),
96
+ tenantId: tenant.tenantId,
97
+ };
98
+ }
99
+
100
+ export function createTenantCacheKey(
101
+ tenant: TenantContext,
102
+ adapter: TenantAdapter = defaultTenantAdapter,
103
+ ): TenantCacheKey {
104
+ return {
105
+ __svadminTenant: adapter.getCacheIdentity?.(tenant) ?? tenant.tenantId,
106
+ };
107
+ }
108
+
109
+ export function isTenantCacheKey(value: unknown): value is TenantCacheKey {
110
+ return Boolean(
111
+ value
112
+ && typeof value === 'object'
113
+ && Object.keys(value).length === 1
114
+ && '__svadminTenant' in value
115
+ && (typeof value.__svadminTenant === 'string' || typeof value.__svadminTenant === 'number'),
116
+ );
117
+ }
118
+
119
+ export function appendTenantCacheKey(
120
+ queryKey: readonly unknown[],
121
+ tenantCacheKey: TenantCacheKey | undefined,
122
+ ): readonly unknown[] {
123
+ return tenantCacheKey ? [...queryKey, tenantCacheKey] : queryKey;
124
+ }
125
+
126
+ export function queryKeyMatchesTenant(
127
+ queryKey: readonly unknown[],
128
+ tenantCacheKey: TenantCacheKey | undefined,
129
+ ): boolean {
130
+ const candidate = queryKey.at(-1);
131
+ const keyTenant = isTenantCacheKey(candidate) ? candidate : undefined;
132
+ if (!tenantCacheKey) return keyTenant === undefined;
133
+ return keyTenant?.__svadminTenant === tenantCacheKey.__svadminTenant;
134
+ }
135
+
136
+ export interface ProviderMetaInput {
137
+ readonly resource?: string;
138
+ readonly meta?: Record<string, unknown>;
139
+ }
140
+
141
+ export type ProviderMetaResolver = (input: ProviderMetaInput) => Record<string, unknown> | undefined;
142
+
143
+ /**
144
+ * 为 DataProvider 的所有标准操作增量注入 meta;未声明的扩展字段仍由原 provider 保留。
145
+ */
146
+ export function withProviderMeta(provider: DataProvider, resolveMeta: ProviderMetaResolver): DataProvider {
147
+ const adapted: DataProvider = {
148
+ ...provider,
149
+ getList: <TData extends BaseRecord = BaseRecord>(params: GetListParams): Promise<GetListResult<TData>> =>
150
+ provider.getList<TData>({ ...params, meta: resolveMeta(params) }),
151
+ getOne: <TData extends BaseRecord = BaseRecord>(params: GetOneParams): Promise<GetOneResult<TData>> =>
152
+ provider.getOne<TData>({ ...params, meta: resolveMeta(params) }),
153
+ create: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>): Promise<CreateResult<TData>> =>
154
+ provider.create<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
155
+ update: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>): Promise<UpdateResult<TData>> =>
156
+ provider.update<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
157
+ deleteOne: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>): Promise<DeleteResult<TData>> =>
158
+ provider.deleteOne<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
159
+ getApiUrl: () => provider.getApiUrl(),
160
+ };
161
+
162
+ const getMany = provider.getMany?.bind(provider);
163
+ if (getMany) {
164
+ adapted.getMany = <TData extends BaseRecord = BaseRecord>(params: GetManyParams): Promise<GetManyResult<TData>> =>
165
+ getMany<TData>({ ...params, meta: resolveMeta(params) });
166
+ }
167
+ const createMany = provider.createMany?.bind(provider);
168
+ if (createMany) {
169
+ adapted.createMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> =>
170
+ createMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
171
+ }
172
+ const updateMany = provider.updateMany?.bind(provider);
173
+ if (updateMany) {
174
+ adapted.updateMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> =>
175
+ updateMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
176
+ }
177
+ const deleteMany = provider.deleteMany?.bind(provider);
178
+ if (deleteMany) {
179
+ adapted.deleteMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> =>
180
+ deleteMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
181
+ }
182
+ const custom = provider.custom?.bind(provider);
183
+ if (custom) {
184
+ adapted.custom = <TData = unknown, TVariables = unknown>(params: CustomParams<TVariables>): Promise<CustomResult<TData>> =>
185
+ custom<TData, TVariables>({ ...params, meta: resolveMeta(params) });
186
+ }
187
+
188
+ return adapted;
189
+ }
190
+
191
+ export function withTenantDataProvider(
192
+ provider: DataProvider,
193
+ tenant: TenantContext,
194
+ adapter: TenantAdapter = defaultTenantAdapter,
195
+ ): DataProvider {
196
+ return withProviderMeta(provider, ({ meta }) => ({
197
+ ...(meta ?? {}),
198
+ ...resolveTenantProviderMeta(tenant, adapter),
199
+ }));
200
+ }
@@ -1,6 +1,7 @@
1
1
  import { createQuery } from '@tanstack/svelte-query';
2
2
  import { getAdminOptions } from './options.svelte';
3
3
  import { captureAdminContext } from './context.svelte';
4
+ import { appendTenantCacheKey } from './provider-bundle';
4
5
  import { useParsed } from './useParsed.svelte';
5
6
  import {
6
7
  checkError,
@@ -109,7 +110,9 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
109
110
  const filters = [...parentFilters, ...(explicitFilters ?? [])];
110
111
 
111
112
  return {
112
- queryKey: [opts.dataProviderName, resource, 'list', pagination, sorters, filters, meta],
113
+ queryKey: appendTenantCacheKey([
114
+ opts.dataProviderName, resource, 'list', pagination, sorters, filters, meta,
115
+ ], adminContext.tenantCacheKey),
113
116
  queryFn: async () => provider.getList<TData>({
114
117
  resource,
115
118
  pagination,
@@ -150,12 +153,24 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
150
153
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
151
154
  lastSuccessAt = query.dataUpdatedAt;
152
155
  if (opts.successNotification) {
153
- fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
156
+ fireSuccessNotification({
157
+ config: opts.successNotification,
158
+ defaultMessage: '',
159
+ data: query.data,
160
+ resource: getResource(),
161
+ provider: adminContext.notificationProvider,
162
+ });
154
163
  }
155
164
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
156
165
  lastErrorAt = query.errorUpdatedAt;
157
166
  checkError(query.error, adminContext);
158
- fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
167
+ fireErrorNotification({
168
+ config: opts.errorNotification,
169
+ defaultMessage: 'Fetch failed',
170
+ error: query.error,
171
+ resource: getResource(),
172
+ provider: adminContext.notificationProvider,
173
+ });
159
174
  }
160
175
  });
161
176
 
@@ -195,7 +210,10 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
195
210
  const queryOptions = opts.queryOptions;
196
211
 
197
212
  return {
198
- queryKey: [opts.dataProviderName, resource, 'one', id, opts.meta],
213
+ queryKey: appendTenantCacheKey(
214
+ [opts.dataProviderName, resource, 'one', id, opts.meta],
215
+ adminContext.tenantCacheKey,
216
+ ),
199
217
  queryFn: async () => {
200
218
  if (id == null) throw new Error('useOne requires an id');
201
219
  const result = await provider.getOne<TData>({
@@ -238,12 +256,24 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
238
256
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
239
257
  lastSuccessAt = query.dataUpdatedAt;
240
258
  if (opts.successNotification) {
241
- fireSuccessNotification(opts.successNotification, '', query.data, undefined, getResource());
259
+ fireSuccessNotification({
260
+ config: opts.successNotification,
261
+ defaultMessage: '',
262
+ data: query.data,
263
+ resource: getResource(),
264
+ provider: adminContext.notificationProvider,
265
+ });
242
266
  }
243
267
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
244
268
  lastErrorAt = query.errorUpdatedAt;
245
269
  checkError(query.error, adminContext);
246
- fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
270
+ fireErrorNotification({
271
+ config: opts.errorNotification,
272
+ defaultMessage: 'Fetch failed',
273
+ error: query.error,
274
+ resource: getResource(),
275
+ provider: adminContext.notificationProvider,
276
+ });
247
277
  }
248
278
  });
249
279
 
@@ -300,7 +330,10 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
300
330
  const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
301
331
 
302
332
  return {
303
- queryKey: [dataProviderName, resource, 'many', ids, meta],
333
+ queryKey: appendTenantCacheKey(
334
+ [dataProviderName, resource, 'many', ids, meta],
335
+ adminContext.tenantCacheKey,
336
+ ),
304
337
  queryFn: async () => {
305
338
  if (!ids.length) return { data: [] };
306
339
  if (provider.getMany) {
@@ -341,12 +374,24 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
341
374
  if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
342
375
  lastSuccessAt = query.dataUpdatedAt;
343
376
  if (opts.successNotification) {
344
- fireSuccessNotification(opts.successNotification, '', query.data, undefined, opts.resource);
377
+ fireSuccessNotification({
378
+ config: opts.successNotification,
379
+ defaultMessage: '',
380
+ data: query.data,
381
+ resource: opts.resource,
382
+ provider: adminContext.notificationProvider,
383
+ });
345
384
  }
346
385
  } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
347
386
  lastErrorAt = query.errorUpdatedAt;
348
387
  checkError(query.error, adminContext);
349
- fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, opts.resource);
388
+ fireErrorNotification({
389
+ config: opts.errorNotification,
390
+ defaultMessage: 'Fetch failed',
391
+ error: query.error,
392
+ resource: opts.resource,
393
+ provider: adminContext.notificationProvider,
394
+ });
350
395
  }
351
396
  });
352
397