@svadmin/core 0.34.2 → 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.
- package/package.json +1 -1
- package/src/audit.ts +11 -3
- package/src/auth-hooks.svelte.ts +83 -13
- package/src/context.svelte.ts +180 -23
- package/src/form-hooks.svelte.ts +63 -15
- package/src/hook-utils.svelte.ts +31 -19
- package/src/hooks.svelte.ts +175 -28
- package/src/index.ts +33 -3
- package/src/live.svelte.ts +5 -1
- package/src/mutation-hooks.svelte.ts +116 -19
- package/src/notification.svelte.ts +14 -4
- package/src/provider-bundle.ts +200 -0
- package/src/query-hooks.svelte.ts +54 -9
- package/src/task-hooks.svelte.ts +66 -19
- package/src/types.ts +25 -0
- package/src/useCan.ts +18 -4
- package/src/utility-hooks.svelte.ts +31 -2
|
@@ -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 {
|
|
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
|
-
? () =>
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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[] }) =>
|
|
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(
|
|
279
|
-
|
|
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(
|
|
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(
|
|
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[] }) =>
|
|
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({
|
|
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(
|
|
421
|
-
|
|
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(
|
|
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(
|
|
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
|
|
20
|
+
export interface NotificationParams {
|
|
21
21
|
type: 'success' | 'error' | 'warning' | 'info';
|
|
22
22
|
message: string;
|
|
23
23
|
description?: string;
|
|
24
24
|
key?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
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: [
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|