@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.
@@ -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: [opts.dataProviderName, resource, 'list', pagination, sorters, filters, meta],
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(opts.successNotification, '', query.data, undefined, getResource());
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(opts.errorNotification, 'Fetch failed', query.error, getResource());
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: [opts.dataProviderName, resource, 'one', id, opts.meta],
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(opts.successNotification, '', query.data, undefined, getResource());
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(opts.errorNotification, 'Fetch failed', query.error, getResource());
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: [dataProviderName, resource, 'many', ids, meta],
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(opts.successNotification, '', query.data, undefined, opts.resource);
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(opts.errorNotification, 'Fetch failed', query.error, opts.resource);
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 keys = () => {
2
- let _dataProviderName: string | undefined = undefined;
3
- let _resource = '';
4
- let _action = '';
5
- let _params: unknown = undefined;
6
-
7
- const builder = {
8
- data: (name: string | undefined) => { _dataProviderName = name; return builder; },
9
- resource: (name: string) => { _resource = name; return builder; },
10
- action: (name: string) => { _action = name; return builder; },
11
- params: (params: unknown) => { _params = params; return builder; },
12
- get: (): unknown[] => {
13
- const parts: unknown[] = [_dataProviderName, _resource];
14
- if (_action) parts.push(_action);
15
- if (_params !== undefined) parts.push(_params);
16
- return parts;
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
- return builder;
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
+ }
@@ -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
- return provider.submit(params.taskName, params.options);
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
- void queryClient.invalidateQueries({ queryKey: ['taskList'] });
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: ['task', options.taskId],
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(options.successNotification, '', query.data, undefined, options.taskId);
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(options.errorNotification, i18n.t('task.fetchFailed'), query.error, options.taskId);
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: ['taskList', options.dlq ? 'dlq' : 'default', options.params],
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(options.successNotification, '', query.data, options.params, options.dlq ? 'taskDlq' : 'tasks');
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(options.errorNotification, i18n.t('task.fetchListFailed'), query.error, options.dlq ? 'taskDlq' : 'tasks');
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