@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/package.json +1 -1
- package/src/audit.ts +11 -3
- package/src/auth-hooks.svelte.ts +83 -13
- package/src/context.svelte.ts +202 -29
- package/src/form-hooks.svelte.ts +70 -20
- package/src/hook-utils.svelte.ts +33 -21
- package/src/hooks.svelte.ts +180 -39
- package/src/index.ts +47 -4
- package/src/live.svelte.ts +7 -3
- package/src/mutation-hooks.svelte.ts +161 -52
- package/src/notification.svelte.ts +14 -4
- package/src/provider-bundle.ts +183 -0
- package/src/query-hooks.svelte.ts +55 -9
- package/src/query-keys.ts +124 -18
- package/src/task-hooks.svelte.ts +68 -19
- package/src/types.ts +25 -0
- package/src/useCan.ts +19 -4
- package/src/utility-hooks.svelte.ts +34 -6
|
@@ -0,0 +1,183 @@
|
|
|
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 interface ProviderMetaInput {
|
|
120
|
+
readonly resource?: string;
|
|
121
|
+
readonly meta?: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ProviderMetaResolver = (input: ProviderMetaInput) => Record<string, unknown> | undefined;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 为 DataProvider 的所有标准操作增量注入 meta;未声明的扩展字段仍由原 provider 保留。
|
|
128
|
+
*/
|
|
129
|
+
export function withProviderMeta(provider: DataProvider, resolveMeta: ProviderMetaResolver): DataProvider {
|
|
130
|
+
const adapted: DataProvider = {
|
|
131
|
+
...provider,
|
|
132
|
+
getList: <TData extends BaseRecord = BaseRecord>(params: GetListParams): Promise<GetListResult<TData>> =>
|
|
133
|
+
provider.getList<TData>({ ...params, meta: resolveMeta(params) }),
|
|
134
|
+
getOne: <TData extends BaseRecord = BaseRecord>(params: GetOneParams): Promise<GetOneResult<TData>> =>
|
|
135
|
+
provider.getOne<TData>({ ...params, meta: resolveMeta(params) }),
|
|
136
|
+
create: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>): Promise<CreateResult<TData>> =>
|
|
137
|
+
provider.create<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
|
|
138
|
+
update: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>): Promise<UpdateResult<TData>> =>
|
|
139
|
+
provider.update<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
|
|
140
|
+
deleteOne: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>): Promise<DeleteResult<TData>> =>
|
|
141
|
+
provider.deleteOne<TData, TVariables>({ ...params, meta: resolveMeta(params) }),
|
|
142
|
+
getApiUrl: () => provider.getApiUrl(),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getMany = provider.getMany?.bind(provider);
|
|
146
|
+
if (getMany) {
|
|
147
|
+
adapted.getMany = <TData extends BaseRecord = BaseRecord>(params: GetManyParams): Promise<GetManyResult<TData>> =>
|
|
148
|
+
getMany<TData>({ ...params, meta: resolveMeta(params) });
|
|
149
|
+
}
|
|
150
|
+
const createMany = provider.createMany?.bind(provider);
|
|
151
|
+
if (createMany) {
|
|
152
|
+
adapted.createMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> =>
|
|
153
|
+
createMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
|
|
154
|
+
}
|
|
155
|
+
const updateMany = provider.updateMany?.bind(provider);
|
|
156
|
+
if (updateMany) {
|
|
157
|
+
adapted.updateMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> =>
|
|
158
|
+
updateMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
|
|
159
|
+
}
|
|
160
|
+
const deleteMany = provider.deleteMany?.bind(provider);
|
|
161
|
+
if (deleteMany) {
|
|
162
|
+
adapted.deleteMany = <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> =>
|
|
163
|
+
deleteMany<TData, TVariables>({ ...params, meta: resolveMeta(params) });
|
|
164
|
+
}
|
|
165
|
+
const custom = provider.custom?.bind(provider);
|
|
166
|
+
if (custom) {
|
|
167
|
+
adapted.custom = <TData = unknown, TVariables = unknown>(params: CustomParams<TVariables>): Promise<CustomResult<TData>> =>
|
|
168
|
+
custom<TData, TVariables>({ ...params, meta: resolveMeta(params) });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return adapted;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function withTenantDataProvider(
|
|
175
|
+
provider: DataProvider,
|
|
176
|
+
tenant: TenantContext,
|
|
177
|
+
adapter: TenantAdapter = defaultTenantAdapter,
|
|
178
|
+
): DataProvider {
|
|
179
|
+
return withProviderMeta(provider, ({ meta }) => ({
|
|
180
|
+
...(meta ?? {}),
|
|
181
|
+
...resolveTenantProviderMeta(tenant, adapter),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
@@ -109,7 +109,12 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
109
109
|
const filters = [...parentFilters, ...(explicitFilters ?? [])];
|
|
110
110
|
|
|
111
111
|
return {
|
|
112
|
-
queryKey:
|
|
112
|
+
queryKey: adminContext.queryKeys(resource, opts.dataProviderName).data.list(resource, {
|
|
113
|
+
pagination,
|
|
114
|
+
sorters,
|
|
115
|
+
filters,
|
|
116
|
+
meta,
|
|
117
|
+
}),
|
|
113
118
|
queryFn: async () => provider.getList<TData>({
|
|
114
119
|
resource,
|
|
115
120
|
pagination,
|
|
@@ -150,12 +155,24 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
150
155
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
151
156
|
lastSuccessAt = query.dataUpdatedAt;
|
|
152
157
|
if (opts.successNotification) {
|
|
153
|
-
fireSuccessNotification(
|
|
158
|
+
fireSuccessNotification({
|
|
159
|
+
config: opts.successNotification,
|
|
160
|
+
defaultMessage: '',
|
|
161
|
+
data: query.data,
|
|
162
|
+
resource: getResource(),
|
|
163
|
+
provider: adminContext.notificationProvider,
|
|
164
|
+
});
|
|
154
165
|
}
|
|
155
166
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
156
167
|
lastErrorAt = query.errorUpdatedAt;
|
|
157
168
|
checkError(query.error, adminContext);
|
|
158
|
-
fireErrorNotification(
|
|
169
|
+
fireErrorNotification({
|
|
170
|
+
config: opts.errorNotification,
|
|
171
|
+
defaultMessage: 'Fetch failed',
|
|
172
|
+
error: query.error,
|
|
173
|
+
resource: getResource(),
|
|
174
|
+
provider: adminContext.notificationProvider,
|
|
175
|
+
});
|
|
159
176
|
}
|
|
160
177
|
});
|
|
161
178
|
|
|
@@ -195,7 +212,9 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
195
212
|
const queryOptions = opts.queryOptions;
|
|
196
213
|
|
|
197
214
|
return {
|
|
198
|
-
queryKey:
|
|
215
|
+
queryKey: adminContext.queryKeys(resource, opts.dataProviderName).data.one(resource, id ?? '', {
|
|
216
|
+
meta: cloneQueryKeyPart(opts.meta),
|
|
217
|
+
}),
|
|
199
218
|
queryFn: async () => {
|
|
200
219
|
if (id == null) throw new Error('useOne requires an id');
|
|
201
220
|
const result = await provider.getOne<TData>({
|
|
@@ -238,12 +257,24 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
238
257
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
239
258
|
lastSuccessAt = query.dataUpdatedAt;
|
|
240
259
|
if (opts.successNotification) {
|
|
241
|
-
fireSuccessNotification(
|
|
260
|
+
fireSuccessNotification({
|
|
261
|
+
config: opts.successNotification,
|
|
262
|
+
defaultMessage: '',
|
|
263
|
+
data: query.data,
|
|
264
|
+
resource: getResource(),
|
|
265
|
+
provider: adminContext.notificationProvider,
|
|
266
|
+
});
|
|
242
267
|
}
|
|
243
268
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
244
269
|
lastErrorAt = query.errorUpdatedAt;
|
|
245
270
|
checkError(query.error, adminContext);
|
|
246
|
-
fireErrorNotification(
|
|
271
|
+
fireErrorNotification({
|
|
272
|
+
config: opts.errorNotification,
|
|
273
|
+
defaultMessage: 'Fetch failed',
|
|
274
|
+
error: query.error,
|
|
275
|
+
resource: getResource(),
|
|
276
|
+
provider: adminContext.notificationProvider,
|
|
277
|
+
});
|
|
247
278
|
}
|
|
248
279
|
});
|
|
249
280
|
|
|
@@ -300,7 +331,10 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
300
331
|
const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
|
|
301
332
|
|
|
302
333
|
return {
|
|
303
|
-
queryKey:
|
|
334
|
+
queryKey: adminContext.queryKeys(resource, dataProviderName).data.many(resource, {
|
|
335
|
+
ids: cloneQueryKeyPart(ids),
|
|
336
|
+
meta: cloneQueryKeyPart(meta),
|
|
337
|
+
}),
|
|
304
338
|
queryFn: async () => {
|
|
305
339
|
if (!ids.length) return { data: [] };
|
|
306
340
|
if (provider.getMany) {
|
|
@@ -341,12 +375,24 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
341
375
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
342
376
|
lastSuccessAt = query.dataUpdatedAt;
|
|
343
377
|
if (opts.successNotification) {
|
|
344
|
-
fireSuccessNotification(
|
|
378
|
+
fireSuccessNotification({
|
|
379
|
+
config: opts.successNotification,
|
|
380
|
+
defaultMessage: '',
|
|
381
|
+
data: query.data,
|
|
382
|
+
resource: opts.resource,
|
|
383
|
+
provider: adminContext.notificationProvider,
|
|
384
|
+
});
|
|
345
385
|
}
|
|
346
386
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
347
387
|
lastErrorAt = query.errorUpdatedAt;
|
|
348
388
|
checkError(query.error, adminContext);
|
|
349
|
-
fireErrorNotification(
|
|
389
|
+
fireErrorNotification({
|
|
390
|
+
config: opts.errorNotification,
|
|
391
|
+
defaultMessage: 'Fetch failed',
|
|
392
|
+
error: query.error,
|
|
393
|
+
resource: opts.resource,
|
|
394
|
+
provider: adminContext.notificationProvider,
|
|
395
|
+
});
|
|
350
396
|
}
|
|
351
397
|
});
|
|
352
398
|
|
package/src/query-keys.ts
CHANGED
|
@@ -1,21 +1,127 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
export const NAMESPACE = 'svadmin';
|
|
2
|
+
export const VERSION = 2;
|
|
3
|
+
|
|
4
|
+
export type QueryKind = 'data' | 'task' | 'access' | 'custom';
|
|
5
|
+
export type QueryDataAction = 'list' | 'infiniteList' | 'one' | 'many' | 'select' | 'selectDefaults';
|
|
6
|
+
export type QueryTaskAction = 'list' | 'one';
|
|
7
|
+
type TenantId = string | number;
|
|
8
|
+
|
|
9
|
+
type BaseDescriptor = {
|
|
10
|
+
readonly namespace: typeof NAMESPACE;
|
|
11
|
+
readonly version: typeof VERSION;
|
|
12
|
+
readonly provider: string;
|
|
13
|
+
readonly tenant?: TenantId;
|
|
14
|
+
readonly resource?: string;
|
|
15
|
+
readonly id?: string | number;
|
|
16
|
+
readonly params?: unknown;
|
|
17
|
+
readonly method?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type QueryDescriptorData = BaseDescriptor & { readonly kind: 'data'; readonly action: QueryDataAction };
|
|
21
|
+
export type QueryDescriptorTask = BaseDescriptor & { readonly kind: 'task'; readonly action: QueryTaskAction };
|
|
22
|
+
export type QueryDescriptorAccess = BaseDescriptor & { readonly kind: 'access'; readonly action?: 'can' };
|
|
23
|
+
export type QueryDescriptorCustom = BaseDescriptor & { readonly kind: 'custom'; readonly action?: string };
|
|
24
|
+
export type QueryDescriptor = QueryDescriptorData | QueryDescriptorTask | QueryDescriptorAccess | QueryDescriptorCustom;
|
|
25
|
+
export type QueryKey = readonly [descriptor: QueryDescriptor];
|
|
26
|
+
|
|
27
|
+
export type QueryKeysContext = { readonly provider?: string; readonly tenant?: TenantId; readonly resource?: string; readonly action?: string; readonly id?: string | number; readonly params?: unknown; readonly method?: string };
|
|
28
|
+
export type QueryMatcher = { readonly provider?: string; readonly tenant?: TenantId; readonly resource?: string; readonly action?: string; readonly id?: string | number; readonly method?: string; readonly kind?: QueryKind };
|
|
29
|
+
export type DataQueryMatcher = Omit<QueryMatcher, 'kind'> & { readonly resource: string };
|
|
30
|
+
export type QueryKeysBuilder = {
|
|
31
|
+
data: { list(resource: string, params?: unknown): QueryKey; infiniteList(resource: string, params?: unknown): QueryKey; one(resource: string, id: string | number, params?: unknown): QueryKey; many(resource: string, params: unknown): QueryKey; select(resource: string, params?: unknown): QueryKey; selectDefaults(resource: string, params?: unknown): QueryKey; };
|
|
32
|
+
task: { list(params?: unknown): QueryKey; one(id: string | number): QueryKey };
|
|
33
|
+
access: { can(resource?: string, params?: unknown): QueryKey };
|
|
34
|
+
custom: { call(resource: string, id: string | number, method?: string, params?: unknown): QueryKey };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function withDefaultProvider(value: string | undefined): string { return value === undefined ? 'default' : value; }
|
|
38
|
+
function assertProvider(provider: string): void { if (!provider.length) throw new Error('provider must be a non-empty string'); }
|
|
39
|
+
|
|
40
|
+
function makeDescriptor(value: { kind: 'data'; provider?: string; tenant?: TenantId; resource?: string; action: QueryDataAction; id?: string | number; params?: unknown; method?: string; }): QueryDescriptorData;
|
|
41
|
+
function makeDescriptor(value: { kind: 'task'; provider?: string; tenant?: TenantId; resource?: string; action: QueryTaskAction; id?: string | number; params?: unknown; method?: string; }): QueryDescriptorTask;
|
|
42
|
+
function makeDescriptor(value: { kind: 'access'; provider?: string; tenant?: TenantId; resource?: string; action?: 'can'; id?: string | number; params?: unknown; method?: string; }): QueryDescriptorAccess;
|
|
43
|
+
function makeDescriptor(value: { kind: 'custom'; provider?: string; tenant?: TenantId; resource?: string; id?: string | number; action?: string; params?: unknown; method?: string; }): QueryDescriptorCustom;
|
|
44
|
+
function makeDescriptor(value: { kind: QueryKind; provider?: string; tenant?: TenantId; resource?: string; action?: string; id?: string | number; params?: unknown; method?: string; }): QueryDescriptor {
|
|
45
|
+
const provider = withDefaultProvider(value.provider);
|
|
46
|
+
assertProvider(provider);
|
|
47
|
+
|
|
48
|
+
const base = { namespace: NAMESPACE, version: VERSION, provider, tenant: value.tenant, resource: value.resource, id: value.id, params: value.params, method: value.method } as const;
|
|
49
|
+
if (value.kind === 'data') return { ...base, kind: 'data', action: value.action } as QueryDescriptorData;
|
|
50
|
+
if (value.kind === 'task') return { ...base, kind: 'task', action: value.action as QueryTaskAction } as QueryDescriptorTask;
|
|
51
|
+
if (value.kind === 'access') return { ...base, kind: 'access', action: value.action as 'can' | undefined } as QueryDescriptorAccess;
|
|
52
|
+
return { ...base, kind: 'custom', action: value.action } as QueryDescriptorCustom;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function keys(context: QueryKeysContext = {}): QueryKeysBuilder {
|
|
56
|
+
const base = { provider: withDefaultProvider(context.provider), tenant: context.tenant, resource: context.resource, id: context.id, params: context.params, method: context.method };
|
|
57
|
+
return {
|
|
58
|
+
data: {
|
|
59
|
+
list: (resource, params = base.params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'list', id: base.id, params, method: base.method })],
|
|
60
|
+
infiniteList: (resource, params = base.params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'infiniteList', id: base.id, params, method: base.method })],
|
|
61
|
+
one: (resource, id, params = base.params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'one', id, params, method: base.method })],
|
|
62
|
+
many: (resource, params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'many', params, method: base.method })],
|
|
63
|
+
select: (resource, params = base.params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'select', id: base.id, params, method: base.method })],
|
|
64
|
+
selectDefaults: (resource, params = base.params) => [makeDescriptor({ kind: 'data', provider: base.provider, tenant: base.tenant, resource, action: 'selectDefaults', id: base.id, params, method: base.method })],
|
|
65
|
+
},
|
|
66
|
+
task: {
|
|
67
|
+
list: (params = base.params) => [makeDescriptor({ kind: 'task', provider: base.provider, tenant: base.tenant, resource: base.resource, action: 'list', id: base.id, params, method: base.method })],
|
|
68
|
+
one: (id) => [makeDescriptor({ kind: 'task', provider: base.provider, tenant: base.tenant, resource: base.resource, action: 'one', id, method: base.method })],
|
|
69
|
+
},
|
|
70
|
+
access: { can: (resource = base.resource, params = base.params) => [makeDescriptor({ kind: 'access', provider: base.provider, tenant: base.tenant, resource, action: 'can', id: base.id, params, method: base.method })] },
|
|
71
|
+
custom: { call: (resource, id, method, params) => [makeDescriptor({ kind: 'custom', provider: base.provider, tenant: base.tenant, resource, id, method, params, action: 'call' })] },
|
|
18
72
|
};
|
|
73
|
+
}
|
|
19
74
|
|
|
20
|
-
|
|
75
|
+
export const queryKeys: QueryKeysBuilder = {
|
|
76
|
+
data: {
|
|
77
|
+
list: (resource, params) => keys().data.list(resource, params),
|
|
78
|
+
infiniteList: (resource, params) => keys().data.infiniteList(resource, params),
|
|
79
|
+
one: (resource, id, params) => keys().data.one(resource, id, params),
|
|
80
|
+
many: (resource, params) => keys().data.many(resource, params),
|
|
81
|
+
select: (resource, params) => keys().data.select(resource, params),
|
|
82
|
+
selectDefaults: (resource, params) => keys().data.selectDefaults(resource, params),
|
|
83
|
+
},
|
|
84
|
+
task: { list: (params) => keys().task.list(params), one: (id) => keys().task.one(id) },
|
|
85
|
+
access: { can: (resource, params) => keys().access.can(resource, params) },
|
|
86
|
+
custom: { call: (resource, id, method, params) => keys().custom.call(resource, id, method, params) },
|
|
21
87
|
};
|
|
88
|
+
|
|
89
|
+
export function parseQueryKey(value: unknown): QueryDescriptor | undefined {
|
|
90
|
+
if (!Array.isArray(value) || value.length !== 1) return;
|
|
91
|
+
const candidate = value[0];
|
|
92
|
+
if (!candidate || typeof candidate !== 'object') return;
|
|
93
|
+
const descriptor = candidate as QueryDescriptor;
|
|
94
|
+
if (descriptor.namespace !== NAMESPACE || descriptor.version !== VERSION || typeof descriptor.provider !== 'string' || !descriptor.provider.length) return;
|
|
95
|
+
if (descriptor.tenant !== undefined && typeof descriptor.tenant !== 'string' && typeof descriptor.tenant !== 'number') return;
|
|
96
|
+
if (descriptor.resource !== undefined && typeof descriptor.resource !== 'string') return;
|
|
97
|
+
if (descriptor.id !== undefined && typeof descriptor.id !== 'string' && typeof descriptor.id !== 'number') return;
|
|
98
|
+
if (descriptor.method !== undefined && typeof descriptor.method !== 'string') return;
|
|
99
|
+
if (descriptor.kind === 'data' && ['list', 'infiniteList', 'one', 'many', 'select', 'selectDefaults'].includes(descriptor.action)) return descriptor;
|
|
100
|
+
if (descriptor.kind === 'task' && ['list', 'one'].includes(descriptor.action)) return descriptor;
|
|
101
|
+
if (descriptor.kind === 'access' && (descriptor.action === undefined || descriptor.action === 'can')) return descriptor;
|
|
102
|
+
if (descriptor.kind === 'custom' && (descriptor.action === undefined || typeof descriptor.action === 'string')) return descriptor;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isQueryKey(value: unknown): value is QueryKey { return parseQueryKey(value) !== undefined; }
|
|
106
|
+
|
|
107
|
+
function has(value: QueryMatcher, key: keyof QueryMatcher): boolean { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
108
|
+
function matchField(descriptor: QueryDescriptor, matcher: QueryMatcher, key: keyof QueryMatcher): boolean {
|
|
109
|
+
if (!has(matcher, key)) return true;
|
|
110
|
+
if (matcher[key] === undefined) return !(key in descriptor) || (descriptor as Record<string, unknown>)[key] === undefined;
|
|
111
|
+
return (descriptor as Record<string, unknown>)[key] === matcher[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function queryKeyMatches(queryKey: readonly unknown[], matcher: QueryMatcher = {}): boolean {
|
|
115
|
+
const parsed = parseQueryKey(queryKey);
|
|
116
|
+
if (!parsed) return false;
|
|
117
|
+
if (matcher.kind !== undefined && matcher.kind !== parsed.kind) return false;
|
|
118
|
+
if (has(matcher, 'provider')) {
|
|
119
|
+
if (matcher.provider === undefined) return parsed.provider === 'default';
|
|
120
|
+
if (matcher.provider !== parsed.provider) return false;
|
|
121
|
+
}
|
|
122
|
+
return matchField(parsed, matcher, 'tenant') && matchField(parsed, matcher, 'resource') && matchField(parsed, matcher, 'action') && matchField(parsed, matcher, 'id') && matchField(parsed, matcher, 'method');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function dataQueryMatches(queryKey: readonly unknown[], matcher: DataQueryMatcher): boolean {
|
|
126
|
+
return queryKeyMatches(queryKey, { ...matcher, kind: 'data' });
|
|
127
|
+
}
|
package/src/task-hooks.svelte.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-qu
|
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
3
|
import { captureAdminContext } from './context.svelte';
|
|
4
4
|
import type { AdminContextAccessor } from './context.svelte';
|
|
5
|
+
import {
|
|
6
|
+
resolveTenantProviderMeta,
|
|
7
|
+
} from './provider-bundle';
|
|
8
|
+
import { queryKeyMatches } from './query-keys';
|
|
5
9
|
import { checkError, createOvertimeTracker, fireErrorNotification, fireSuccessNotification } from './hook-utils.svelte';
|
|
6
10
|
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
7
11
|
import { useTranslation } from './i18n.svelte';
|
|
@@ -54,27 +58,44 @@ export function useSubmitTask<
|
|
|
54
58
|
const mutation = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
|
|
55
59
|
mutationFn: async (params) => {
|
|
56
60
|
const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
|
|
57
|
-
|
|
61
|
+
const providerMeta = adminContext.tenant
|
|
62
|
+
? {
|
|
63
|
+
...(params.options?.meta ?? {}),
|
|
64
|
+
...resolveTenantProviderMeta(adminContext.tenant, adminContext.tenantAdapter),
|
|
65
|
+
}
|
|
66
|
+
: params.options?.meta;
|
|
67
|
+
const taskOptions = providerMeta === params.options?.meta
|
|
68
|
+
? params.options
|
|
69
|
+
: { ...params.options, meta: providerMeta };
|
|
70
|
+
return provider.submit(params.taskName, taskOptions);
|
|
58
71
|
},
|
|
59
72
|
onSuccess: (data, params, context) => {
|
|
60
|
-
fireSuccessNotification(
|
|
61
|
-
params.successNotification,
|
|
62
|
-
i18n.t('task.submitSuccess'),
|
|
73
|
+
fireSuccessNotification({
|
|
74
|
+
config: params.successNotification,
|
|
75
|
+
defaultMessage: i18n.t('task.submitSuccess'),
|
|
63
76
|
data,
|
|
64
|
-
params.options,
|
|
65
|
-
params.taskName,
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
values: params.options,
|
|
78
|
+
resource: params.taskName,
|
|
79
|
+
provider: adminContext.notificationProvider,
|
|
80
|
+
});
|
|
81
|
+
void queryClient.invalidateQueries({
|
|
82
|
+
predicate: (query) => queryKeyMatches(query.queryKey, {
|
|
83
|
+
...adminContext.queryKeyMatcher(),
|
|
84
|
+
kind: 'task',
|
|
85
|
+
action: 'list',
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
68
88
|
options.mutationOptions?.onSuccess?.(data, params, context);
|
|
69
89
|
},
|
|
70
90
|
onError: (error, params, context) => {
|
|
71
91
|
checkError(error, adminContext);
|
|
72
|
-
fireErrorNotification(
|
|
73
|
-
params.errorNotification,
|
|
74
|
-
i18n.t('task.submitFailed'),
|
|
92
|
+
fireErrorNotification({
|
|
93
|
+
config: params.errorNotification,
|
|
94
|
+
defaultMessage: i18n.t('task.submitFailed'),
|
|
75
95
|
error,
|
|
76
|
-
params.taskName,
|
|
77
|
-
|
|
96
|
+
resource: params.taskName,
|
|
97
|
+
provider: adminContext.notificationProvider,
|
|
98
|
+
});
|
|
78
99
|
options.mutationOptions?.onError?.(error, params, context);
|
|
79
100
|
},
|
|
80
101
|
}));
|
|
@@ -109,7 +130,7 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
109
130
|
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
110
131
|
const queryOptions = options.queryOptions;
|
|
111
132
|
return {
|
|
112
|
-
queryKey:
|
|
133
|
+
queryKey: adminContext.queryKeys().task.one(options.taskId ?? ''),
|
|
113
134
|
queryFn: async () => {
|
|
114
135
|
if (!options.taskId) throw new Error('useTask requires a taskId');
|
|
115
136
|
return provider.get(options.taskId);
|
|
@@ -131,12 +152,24 @@ export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpErro
|
|
|
131
152
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
132
153
|
lastSuccessAt = query.dataUpdatedAt;
|
|
133
154
|
if (options.successNotification) {
|
|
134
|
-
fireSuccessNotification(
|
|
155
|
+
fireSuccessNotification({
|
|
156
|
+
config: options.successNotification,
|
|
157
|
+
defaultMessage: '',
|
|
158
|
+
data: query.data,
|
|
159
|
+
resource: options.taskId,
|
|
160
|
+
provider: adminContext.notificationProvider,
|
|
161
|
+
});
|
|
135
162
|
}
|
|
136
163
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
137
164
|
lastErrorAt = query.errorUpdatedAt;
|
|
138
165
|
checkError(query.error, adminContext);
|
|
139
|
-
fireErrorNotification(
|
|
166
|
+
fireErrorNotification({
|
|
167
|
+
config: options.errorNotification,
|
|
168
|
+
defaultMessage: i18n.t('task.fetchFailed'),
|
|
169
|
+
error: query.error,
|
|
170
|
+
resource: options.taskId,
|
|
171
|
+
provider: adminContext.notificationProvider,
|
|
172
|
+
});
|
|
140
173
|
}
|
|
141
174
|
});
|
|
142
175
|
|
|
@@ -176,7 +209,10 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
176
209
|
const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
|
|
177
210
|
const queryOptions = options.queryOptions;
|
|
178
211
|
return {
|
|
179
|
-
queryKey:
|
|
212
|
+
queryKey: adminContext.queryKeys().task.list({
|
|
213
|
+
list: options.dlq ? 'dlq' : 'default',
|
|
214
|
+
params: options.params,
|
|
215
|
+
}),
|
|
180
216
|
queryFn: async () => {
|
|
181
217
|
if (options.dlq) {
|
|
182
218
|
if (!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
|
|
@@ -202,12 +238,25 @@ export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = Http
|
|
|
202
238
|
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
203
239
|
lastSuccessAt = query.dataUpdatedAt;
|
|
204
240
|
if (options.successNotification) {
|
|
205
|
-
fireSuccessNotification(
|
|
241
|
+
fireSuccessNotification({
|
|
242
|
+
config: options.successNotification,
|
|
243
|
+
defaultMessage: '',
|
|
244
|
+
data: query.data,
|
|
245
|
+
values: options.params,
|
|
246
|
+
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
247
|
+
provider: adminContext.notificationProvider,
|
|
248
|
+
});
|
|
206
249
|
}
|
|
207
250
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
208
251
|
lastErrorAt = query.errorUpdatedAt;
|
|
209
252
|
checkError(query.error, adminContext);
|
|
210
|
-
fireErrorNotification(
|
|
253
|
+
fireErrorNotification({
|
|
254
|
+
config: options.errorNotification,
|
|
255
|
+
defaultMessage: i18n.t('task.fetchListFailed'),
|
|
256
|
+
error: query.error,
|
|
257
|
+
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
258
|
+
provider: adminContext.notificationProvider,
|
|
259
|
+
});
|
|
211
260
|
}
|
|
212
261
|
});
|
|
213
262
|
|
package/src/types.ts
CHANGED
|
@@ -382,6 +382,28 @@ export type MutationMode = 'pessimistic' | 'optimistic' | 'undoable';
|
|
|
382
382
|
|
|
383
383
|
// ─── ResourceDefinition ───────────────────────────────────────
|
|
384
384
|
|
|
385
|
+
/** 声明资源使用的传输协议;具体 URL/序列化行为由对应 provider 实现。 */
|
|
386
|
+
export interface ResourceTransportConfig {
|
|
387
|
+
readonly type: string;
|
|
388
|
+
readonly endpoint?: string;
|
|
389
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** 声明资源使用的后端 adapter,不在 Core 内绑定具体运行时实现。 */
|
|
393
|
+
export interface ResourceAdapterConfig {
|
|
394
|
+
readonly name: string;
|
|
395
|
+
readonly version?: string;
|
|
396
|
+
readonly options?: Readonly<Record<string, unknown>>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 每资源的 provider 选择与声明性 transport/adapter 元数据。 */
|
|
400
|
+
export interface ResourceProviderConfig {
|
|
401
|
+
readonly dataProviderName?: string;
|
|
402
|
+
readonly transport?: string | ResourceTransportConfig;
|
|
403
|
+
readonly adapter?: string | ResourceAdapterConfig;
|
|
404
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
405
|
+
}
|
|
406
|
+
|
|
385
407
|
export interface ResourceDefinition {
|
|
386
408
|
name: string;
|
|
387
409
|
/** Unique identifier — use when multiple resources share the same `name` but target different DataProviders */
|
|
@@ -401,6 +423,9 @@ export interface ResourceDefinition {
|
|
|
401
423
|
menuOrder?: number;
|
|
402
424
|
/** Navigation group name — resources with the same group are displayed in a collapsible section */
|
|
403
425
|
group?: string;
|
|
426
|
+
/** DataProvider 选择及声明性 transport/adapter 元数据。 */
|
|
427
|
+
provider?: ResourceProviderConfig;
|
|
428
|
+
/** @deprecated 新代码优先使用 `provider.dataProviderName` 选择 DataProvider。 */
|
|
404
429
|
meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
|
|
405
430
|
}
|
|
406
431
|
|