@svadmin/core 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "src/index.ts",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
13
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
12
|
+
"@happy-dom/global-registrator": "^20.11.2",
|
|
13
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
14
14
|
"@testing-library/dom": "^10.4.1",
|
|
15
15
|
"@testing-library/svelte": "^5.4.2",
|
|
16
|
-
"@testing-library/user-event": "^14.6.
|
|
16
|
+
"@testing-library/user-event": "^14.6.3",
|
|
17
17
|
"@types/bun": "^1.3.14",
|
|
18
|
-
"@types/jsdom": "^
|
|
19
|
-
"happy-dom": "^20.11.
|
|
18
|
+
"@types/jsdom": "^30.0.0",
|
|
19
|
+
"happy-dom": "^20.11.2",
|
|
20
20
|
"jsdom": "^30.0.1",
|
|
21
21
|
"vitest": "^4.1.10"
|
|
22
22
|
},
|
package/src/audit.ts
CHANGED
|
@@ -50,7 +50,11 @@ export function resetAuditLogProvider(): void {
|
|
|
50
50
|
handler = (entry) => { console.info('[audit]', entry.action, entry.resource, entry.recordId); };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/** Record an entry through an explicitly scoped provider and the compatibility handler. */
|
|
54
|
+
export function auditWithProvider(
|
|
55
|
+
entry: Omit<AuditEntry, 'timestamp'>,
|
|
56
|
+
provider: AuditLogProvider | null | undefined,
|
|
57
|
+
): void {
|
|
54
58
|
const fullEntry: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
|
|
55
59
|
try {
|
|
56
60
|
const result = handler(fullEntry);
|
|
@@ -60,8 +64,8 @@ export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
|
60
64
|
} catch (e) {
|
|
61
65
|
console.error('[audit] handler error:', e);
|
|
62
66
|
}
|
|
63
|
-
if (
|
|
64
|
-
|
|
67
|
+
if (provider) {
|
|
68
|
+
provider.create({
|
|
65
69
|
resource: entry.resource ?? '',
|
|
66
70
|
action: entry.action,
|
|
67
71
|
data: entry.data ?? entry.details,
|
|
@@ -70,3 +74,7 @@ export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
|
70
74
|
}).catch(e => console.error('[audit] provider create error:', e));
|
|
71
75
|
}
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
|
|
79
|
+
auditWithProvider(entry, auditLogProvider);
|
|
80
|
+
}
|
package/src/auth-hooks.svelte.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { captureAdminContext } from './context.svelte';
|
|
6
6
|
import type { AdminContextAccessor } from './context.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import { notifyWithProvider } from './notification.svelte';
|
|
8
8
|
import { t, useTranslation } from './i18n.svelte';
|
|
9
9
|
import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './types';
|
|
10
10
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
@@ -26,33 +26,72 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
|
|
|
26
26
|
const adminContext = captureAdminContext();
|
|
27
27
|
const i18n = useTranslation();
|
|
28
28
|
let isLoading = $state(false);
|
|
29
|
+
let mutationEpoch = 0;
|
|
30
|
+
|
|
31
|
+
const activeTenantIdentity = () => adminContext.tenantCacheKey?.__svadminTenant;
|
|
32
|
+
let observedProvider = adminContext.authProvider;
|
|
33
|
+
let observedTenantIdentity = activeTenantIdentity();
|
|
34
|
+
|
|
35
|
+
function isActiveMutation(
|
|
36
|
+
epoch: number,
|
|
37
|
+
provider: AuthProvider,
|
|
38
|
+
tenantIdentity: string | number | undefined,
|
|
39
|
+
): boolean {
|
|
40
|
+
return epoch === mutationEpoch
|
|
41
|
+
&& provider === adminContext.authProvider
|
|
42
|
+
&& tenantIdentity === activeTenantIdentity();
|
|
43
|
+
}
|
|
29
44
|
|
|
30
45
|
async function mutate(params?: Record<string, unknown>): Promise<AuthActionResult> {
|
|
31
46
|
const provider = adminContext.authProvider;
|
|
32
47
|
if (!provider) throw new Error('AuthProvider not configured');
|
|
33
48
|
const fn = provider[options.method] as ((params?: Record<string, unknown>) => Promise<AuthActionResult>) | undefined;
|
|
34
49
|
if (!fn) throw new Error(`AuthProvider.${options.method} not implemented`);
|
|
35
|
-
|
|
50
|
+
|
|
51
|
+
const epoch = ++mutationEpoch;
|
|
52
|
+
const tenantIdentity = activeTenantIdentity();
|
|
53
|
+
const notificationProvider = adminContext.notificationProvider;
|
|
54
|
+
const sendNotification = (notification: Parameters<typeof notifyWithProvider>[0]) =>
|
|
55
|
+
notifyWithProvider(notification, notificationProvider);
|
|
36
56
|
isLoading = true;
|
|
37
57
|
try {
|
|
38
58
|
const result = await fn.call(provider, params);
|
|
59
|
+
if (!isActiveMutation(epoch, provider, tenantIdentity)) return result;
|
|
39
60
|
if (result.success) {
|
|
40
|
-
if (options.successMessage)
|
|
61
|
+
if (options.successMessage) sendNotification({ type: 'success', message: options.successMessage });
|
|
41
62
|
if (options.onSuccess) await options.onSuccess(result, adminContext);
|
|
42
63
|
} else {
|
|
43
64
|
const msg = result.error?.message ?? (typeof options.errorMessage === 'string' ? options.errorMessage : i18n.t('common.operationFailed'));
|
|
44
|
-
if (options.errorMessage !== false)
|
|
65
|
+
if (options.errorMessage !== false) sendNotification({ type: 'error', message: msg });
|
|
45
66
|
}
|
|
46
67
|
return result;
|
|
47
68
|
} catch (err) {
|
|
48
69
|
const msg = err instanceof Error ? err.message : (typeof options.errorMessage === 'string' ? options.errorMessage : i18n.t('common.operationFailed'));
|
|
49
|
-
if (options.errorMessage !== false)
|
|
70
|
+
if (isActiveMutation(epoch, provider, tenantIdentity) && options.errorMessage !== false) {
|
|
71
|
+
sendNotification({ type: 'error', message: msg });
|
|
72
|
+
}
|
|
50
73
|
return { success: false, error: { message: msg } };
|
|
51
74
|
} finally {
|
|
52
|
-
isLoading = false;
|
|
75
|
+
if (isActiveMutation(epoch, provider, tenantIdentity)) isLoading = false;
|
|
53
76
|
}
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
if (typeof window !== 'undefined') {
|
|
80
|
+
$effect(() => {
|
|
81
|
+
const provider = adminContext.authProvider;
|
|
82
|
+
const tenantIdentity = activeTenantIdentity();
|
|
83
|
+
if (provider !== observedProvider || tenantIdentity !== observedTenantIdentity) {
|
|
84
|
+
observedProvider = provider;
|
|
85
|
+
observedTenantIdentity = tenantIdentity;
|
|
86
|
+
mutationEpoch++;
|
|
87
|
+
isLoading = false;
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
mutationEpoch++;
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
56
95
|
return {
|
|
57
96
|
mutate,
|
|
58
97
|
get isLoading() { return isLoading; },
|
|
@@ -152,26 +191,57 @@ export function useGetIdentity() {
|
|
|
152
191
|
let data = $state<Identity | null>(null);
|
|
153
192
|
let isLoading = $state(true);
|
|
154
193
|
let error = $state<Error | null>(null);
|
|
194
|
+
let requestEpoch = 0;
|
|
195
|
+
let observedLogoutVersion = _logoutVersion;
|
|
155
196
|
|
|
156
|
-
function
|
|
157
|
-
const
|
|
158
|
-
|
|
197
|
+
function requestIdentity(provider: AuthProvider | null): void {
|
|
198
|
+
const epoch = ++requestEpoch;
|
|
199
|
+
error = null;
|
|
200
|
+
if (!provider) {
|
|
201
|
+
data = null;
|
|
202
|
+
isLoading = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
159
205
|
isLoading = true;
|
|
160
206
|
provider.getIdentity().then(identity => {
|
|
207
|
+
if (epoch !== requestEpoch) return;
|
|
161
208
|
data = identity;
|
|
162
209
|
isLoading = false;
|
|
163
210
|
}).catch(err => {
|
|
211
|
+
if (epoch !== requestEpoch) return;
|
|
164
212
|
error = err instanceof Error ? err : new Error(String(err));
|
|
165
213
|
isLoading = false;
|
|
166
214
|
console.warn('[svadmin] useGetIdentity failed:', err);
|
|
167
215
|
});
|
|
168
216
|
}
|
|
169
217
|
|
|
218
|
+
function refetchIdentity(): void {
|
|
219
|
+
requestIdentity(adminContext.authProvider);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function loadScopedIdentity(provider: AuthProvider | null): void {
|
|
223
|
+
data = null;
|
|
224
|
+
requestIdentity(provider);
|
|
225
|
+
}
|
|
226
|
+
|
|
170
227
|
if (typeof window !== 'undefined') {
|
|
171
|
-
fetch();
|
|
172
228
|
$effect(() => {
|
|
173
|
-
|
|
174
|
-
|
|
229
|
+
const provider = adminContext.authProvider;
|
|
230
|
+
void adminContext.tenantCacheKey?.__svadminTenant;
|
|
231
|
+
const logoutVersion = _logoutVersion;
|
|
232
|
+
if (logoutVersion !== observedLogoutVersion) {
|
|
233
|
+
observedLogoutVersion = logoutVersion;
|
|
234
|
+
requestEpoch++;
|
|
235
|
+
data = null;
|
|
236
|
+
error = null;
|
|
237
|
+
isLoading = false;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
loadScopedIdentity(provider);
|
|
242
|
+
return () => {
|
|
243
|
+
requestEpoch++;
|
|
244
|
+
};
|
|
175
245
|
});
|
|
176
246
|
}
|
|
177
247
|
|
|
@@ -179,7 +249,7 @@ export function useGetIdentity() {
|
|
|
179
249
|
get data() { return data; },
|
|
180
250
|
get isLoading() { return isLoading; },
|
|
181
251
|
get error() { return error; },
|
|
182
|
-
refetch:
|
|
252
|
+
refetch: refetchIdentity,
|
|
183
253
|
};
|
|
184
254
|
}
|
|
185
255
|
|
package/src/context.svelte.ts
CHANGED
|
@@ -5,12 +5,33 @@ import { createContext } from 'svelte';
|
|
|
5
5
|
import type { DataProvider, AuthProvider, ResourceDefinition, TaskProvider } from './types';
|
|
6
6
|
import type { RouterProvider } from './router-provider';
|
|
7
7
|
import type { LiveProvider } from './live.svelte';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import type { AccessControlProvider } from './permissions.svelte';
|
|
9
|
+
import type { AuditLogProvider } from './audit';
|
|
10
|
+
import type { AgentProvider, ChatProvider } from './chatProvider.svelte';
|
|
11
|
+
import {
|
|
12
|
+
appendTenantCacheKey,
|
|
13
|
+
createTenantCacheKey,
|
|
14
|
+
queryKeyMatchesTenant,
|
|
15
|
+
resolveTenantProviderMeta,
|
|
16
|
+
withProviderMeta,
|
|
17
|
+
} from './provider-bundle';
|
|
18
|
+
import type {
|
|
19
|
+
DataProviderInput,
|
|
20
|
+
ProviderBundle,
|
|
21
|
+
TenantAdapter,
|
|
22
|
+
TenantCacheKey,
|
|
23
|
+
TenantContext,
|
|
24
|
+
} from './provider-bundle';
|
|
25
|
+
import { getNotificationProvider as getLegacyNotificationProvider, resetNotificationProvider } from './notification.svelte';
|
|
26
|
+
import { getAuditLogProvider as getLegacyAuditLogProvider, resetAuditLogProvider } from './audit';
|
|
27
|
+
import { getAccessControlProvider as getLegacyAccessControlProvider, resetAccessControlProvider } from './permissions.svelte';
|
|
11
28
|
import { resetAdminOptions } from './options.svelte';
|
|
12
29
|
import { resetI18n } from './i18n.svelte';
|
|
13
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getAgentProvider as getLegacyAgentProvider,
|
|
32
|
+
getChatProvider as getLegacyChatProvider,
|
|
33
|
+
resetChatProvider,
|
|
34
|
+
} from './chatProvider.svelte';
|
|
14
35
|
import { resetToast } from './toast.svelte';
|
|
15
36
|
import { resetTheme } from './theme.svelte';
|
|
16
37
|
import {
|
|
@@ -29,29 +50,35 @@ import { resetSidebarCollapsed } from './hooks.svelte';
|
|
|
29
50
|
|
|
30
51
|
// ─── DataProvider (supports single or multiple) ─────────────────
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
* Accepts either:
|
|
34
|
-
* - A single DataProvider
|
|
35
|
-
* - A record of named providers: { default: rest, cms: graphql }
|
|
36
|
-
*/
|
|
37
|
-
export type DataProviderInput = DataProvider | Record<string, DataProvider>;
|
|
53
|
+
export type { DataProviderInput } from './provider-bundle';
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
readonly dataProvider: DataProviderInput;
|
|
41
|
-
readonly authProvider?: AuthProvider | null;
|
|
55
|
+
interface AdminContextSourceBase extends Omit<Partial<ProviderBundle>, 'dataProvider'> {
|
|
42
56
|
readonly resources: ResourceDefinition[];
|
|
43
|
-
|
|
44
|
-
readonly
|
|
45
|
-
readonly taskProvider?: TaskProvider;
|
|
57
|
+
/** 请求树内租户;未传入时继承最近的 provideTenantContext。 */
|
|
58
|
+
readonly tenant?: TenantContext;
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/** 必须提供旧式 dataProvider 或一等 ProviderBundle;同名顶层字段优先。 */
|
|
62
|
+
export type AdminContextSource = AdminContextSourceBase & (
|
|
63
|
+
| { readonly dataProvider: DataProviderInput; readonly providerBundle?: ProviderBundle }
|
|
64
|
+
| { readonly dataProvider?: DataProviderInput; readonly providerBundle: ProviderBundle }
|
|
65
|
+
);
|
|
66
|
+
|
|
48
67
|
export interface AdminContextValue {
|
|
68
|
+
readonly providerBundle: ProviderBundle;
|
|
49
69
|
readonly providers: Record<string, DataProvider>;
|
|
50
70
|
readonly authProvider: AuthProvider | null;
|
|
51
71
|
readonly resources: ResourceDefinition[];
|
|
52
72
|
readonly routerProvider: RouterProvider | undefined;
|
|
53
73
|
readonly liveProvider: LiveProvider | undefined;
|
|
54
74
|
readonly taskProvider: TaskProvider | undefined;
|
|
75
|
+
readonly accessControlProvider: AccessControlProvider | null;
|
|
76
|
+
readonly auditLogProvider: AuditLogProvider | null;
|
|
77
|
+
readonly notificationProvider: ProviderBundle['notificationProvider'];
|
|
78
|
+
readonly chatProvider: ChatProvider | null;
|
|
79
|
+
readonly agentProvider: AgentProvider | null;
|
|
80
|
+
readonly tenant: TenantContext | undefined;
|
|
81
|
+
readonly tenantAdapter: TenantAdapter | undefined;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
/**
|
|
@@ -63,15 +90,27 @@ export interface AdminContextValue {
|
|
|
63
90
|
* read the module-level compatibility setters dynamically.
|
|
64
91
|
*/
|
|
65
92
|
export interface AdminContextAccessor {
|
|
93
|
+
readonly providerBundle: ProviderBundle;
|
|
66
94
|
readonly providers: Record<string, DataProvider> | null;
|
|
67
95
|
readonly authProvider: AuthProvider | null;
|
|
68
96
|
readonly resources: ResourceDefinition[];
|
|
69
97
|
readonly routerProvider: RouterProvider | undefined;
|
|
70
98
|
readonly liveProvider: LiveProvider | undefined;
|
|
71
99
|
readonly taskProvider: TaskProvider | undefined;
|
|
100
|
+
readonly accessControlProvider: AccessControlProvider | null;
|
|
101
|
+
readonly auditLogProvider: AuditLogProvider | null;
|
|
102
|
+
readonly notificationProvider: ProviderBundle['notificationProvider'];
|
|
103
|
+
readonly chatProvider: ChatProvider | null;
|
|
104
|
+
readonly agentProvider: AgentProvider | null;
|
|
105
|
+
readonly tenant: TenantContext | undefined;
|
|
106
|
+
readonly tenantAdapter: TenantAdapter | undefined;
|
|
107
|
+
readonly tenantCacheKey: TenantCacheKey | undefined;
|
|
72
108
|
getDataProvider(name?: string): DataProvider;
|
|
73
109
|
getDataProviderNames(): string[];
|
|
74
110
|
getDataProviderForResource(resourceName: string, overrideName?: string): DataProvider;
|
|
111
|
+
getProviderMeta(resourceName: string | undefined, meta?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
112
|
+
withTenantCacheKey(queryKey: readonly unknown[]): readonly unknown[];
|
|
113
|
+
matchesTenantQuery(queryKey: readonly unknown[]): boolean;
|
|
75
114
|
getResource(nameOrIdentifier: string): ResourceDefinition;
|
|
76
115
|
currentPath(): string;
|
|
77
116
|
formatLink(path: string): string;
|
|
@@ -80,6 +119,22 @@ export interface AdminContextAccessor {
|
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
const [getRequiredAdminContext, setAdminContext] = createContext<AdminContextValue>();
|
|
122
|
+
const [getRequiredTenantContext, setScopedTenantContext] = createContext<TenantContext>();
|
|
123
|
+
|
|
124
|
+
/** Provide tenant state to a request/component subtree. No module-level fallback is created. */
|
|
125
|
+
export function provideTenantContext(tenant: TenantContext): TenantContext {
|
|
126
|
+
setScopedTenantContext(tenant);
|
|
127
|
+
return tenant;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Return the nearest request/tree-scoped tenant when called during initialization. */
|
|
131
|
+
export function getTenantContext(): TenantContext | undefined {
|
|
132
|
+
try {
|
|
133
|
+
return getRequiredTenantContext();
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
83
138
|
|
|
84
139
|
function normalizeDataProviders(provider: DataProviderInput): Record<string, DataProvider> {
|
|
85
140
|
return isDataProvider(provider)
|
|
@@ -87,14 +142,49 @@ function normalizeDataProviders(provider: DataProviderInput): Record<string, Dat
|
|
|
87
142
|
: provider;
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
function resolveProviderBundle(source: AdminContextSource): ProviderBundle {
|
|
146
|
+
const bundled = source.providerBundle;
|
|
147
|
+
const dataProvider = source.dataProvider ?? bundled?.dataProvider;
|
|
148
|
+
if (!dataProvider) {
|
|
149
|
+
throw new Error('ProviderBundle requires a dataProvider.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
dataProvider,
|
|
154
|
+
authProvider: source.authProvider !== undefined ? source.authProvider : bundled?.authProvider,
|
|
155
|
+
accessControlProvider: source.accessControlProvider !== undefined
|
|
156
|
+
? source.accessControlProvider
|
|
157
|
+
: bundled?.accessControlProvider,
|
|
158
|
+
liveProvider: source.liveProvider ?? bundled?.liveProvider,
|
|
159
|
+
auditLogProvider: source.auditLogProvider !== undefined ? source.auditLogProvider : bundled?.auditLogProvider,
|
|
160
|
+
notificationProvider: source.notificationProvider !== undefined
|
|
161
|
+
? source.notificationProvider
|
|
162
|
+
: bundled?.notificationProvider,
|
|
163
|
+
chatProvider: source.chatProvider !== undefined ? source.chatProvider : bundled?.chatProvider,
|
|
164
|
+
agentProvider: source.agentProvider !== undefined ? source.agentProvider : bundled?.agentProvider,
|
|
165
|
+
taskProvider: source.taskProvider ?? bundled?.taskProvider,
|
|
166
|
+
routerProvider: source.routerProvider ?? bundled?.routerProvider,
|
|
167
|
+
tenantAdapter: source.tenantAdapter ?? bundled?.tenantAdapter,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
90
171
|
export function createAdminContext(source: AdminContextSource): AdminContextValue {
|
|
172
|
+
const inheritedTenant = getTenantContext();
|
|
91
173
|
return {
|
|
92
|
-
get
|
|
93
|
-
get
|
|
174
|
+
get providerBundle() { return resolveProviderBundle(source); },
|
|
175
|
+
get providers() { return normalizeDataProviders(resolveProviderBundle(source).dataProvider); },
|
|
176
|
+
get authProvider() { return resolveProviderBundle(source).authProvider ?? null; },
|
|
94
177
|
get resources() { return source.resources; },
|
|
95
|
-
get routerProvider() { return source.routerProvider; },
|
|
96
|
-
get liveProvider() { return source.liveProvider; },
|
|
97
|
-
get taskProvider() { return source.taskProvider; },
|
|
178
|
+
get routerProvider() { return resolveProviderBundle(source).routerProvider; },
|
|
179
|
+
get liveProvider() { return resolveProviderBundle(source).liveProvider; },
|
|
180
|
+
get taskProvider() { return resolveProviderBundle(source).taskProvider; },
|
|
181
|
+
get accessControlProvider() { return resolveProviderBundle(source).accessControlProvider ?? null; },
|
|
182
|
+
get auditLogProvider() { return resolveProviderBundle(source).auditLogProvider ?? null; },
|
|
183
|
+
get notificationProvider() { return resolveProviderBundle(source).notificationProvider ?? null; },
|
|
184
|
+
get chatProvider() { return resolveProviderBundle(source).chatProvider ?? null; },
|
|
185
|
+
get agentProvider() { return resolveProviderBundle(source).agentProvider ?? null; },
|
|
186
|
+
get tenant() { return source.tenant ?? inheritedTenant; },
|
|
187
|
+
get tenantAdapter() { return resolveProviderBundle(source).tenantAdapter; },
|
|
98
188
|
};
|
|
99
189
|
}
|
|
100
190
|
|
|
@@ -119,14 +209,55 @@ let providers = $state<Record<string, DataProvider> | null>(null);
|
|
|
119
209
|
/** Capture the current tree once so delayed callbacks never re-enter getContext. */
|
|
120
210
|
export function captureAdminContext(): AdminContextAccessor {
|
|
121
211
|
const scopedContext = getAdminContext();
|
|
212
|
+
const adaptedProviders = new Map<DataProvider, DataProvider>();
|
|
213
|
+
|
|
214
|
+
function adaptProvider(provider: DataProvider): DataProvider {
|
|
215
|
+
const hasResourceMetadata = accessor.resources.some((resource) => resource.provider !== undefined);
|
|
216
|
+
if (!accessor.tenant && !hasResourceMetadata) return provider;
|
|
217
|
+
const cached = adaptedProviders.get(provider);
|
|
218
|
+
if (cached) return cached;
|
|
219
|
+
const adapted = withProviderMeta(provider, ({ resource, meta }) => accessor.getProviderMeta(resource, meta));
|
|
220
|
+
adaptedProviders.set(provider, adapted);
|
|
221
|
+
return adapted;
|
|
222
|
+
}
|
|
122
223
|
|
|
123
224
|
const accessor: AdminContextAccessor = {
|
|
225
|
+
get providerBundle() {
|
|
226
|
+
if (scopedContext) return scopedContext.providerBundle;
|
|
227
|
+
if (!providers) throw new Error('DataProvider not found. Did you call setDataProvider in App.svelte?');
|
|
228
|
+
return {
|
|
229
|
+
dataProvider: providers,
|
|
230
|
+
authProvider,
|
|
231
|
+
accessControlProvider: getLegacyAccessControlProvider(),
|
|
232
|
+
liveProvider: liveProviderState,
|
|
233
|
+
auditLogProvider: getLegacyAuditLogProvider(),
|
|
234
|
+
notificationProvider: getLegacyNotificationProvider(),
|
|
235
|
+
chatProvider: getLegacyChatProvider(),
|
|
236
|
+
agentProvider: getLegacyAgentProvider(),
|
|
237
|
+
taskProvider: taskProviderState,
|
|
238
|
+
routerProvider,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
124
241
|
get providers() { return scopedContext ? scopedContext.providers : providers; },
|
|
125
242
|
get authProvider() { return scopedContext ? scopedContext.authProvider : authProvider; },
|
|
126
243
|
get resources() { return scopedContext ? scopedContext.resources : resources ?? []; },
|
|
127
244
|
get routerProvider() { return scopedContext ? scopedContext.routerProvider : routerProvider; },
|
|
128
245
|
get liveProvider() { return scopedContext ? scopedContext.liveProvider : liveProviderState; },
|
|
129
246
|
get taskProvider() { return scopedContext ? scopedContext.taskProvider : taskProviderState; },
|
|
247
|
+
get accessControlProvider() {
|
|
248
|
+
return scopedContext ? scopedContext.accessControlProvider : getLegacyAccessControlProvider();
|
|
249
|
+
},
|
|
250
|
+
get auditLogProvider() { return scopedContext ? scopedContext.auditLogProvider : getLegacyAuditLogProvider(); },
|
|
251
|
+
get notificationProvider() {
|
|
252
|
+
return scopedContext ? scopedContext.notificationProvider : getLegacyNotificationProvider();
|
|
253
|
+
},
|
|
254
|
+
get chatProvider() { return scopedContext ? scopedContext.chatProvider : getLegacyChatProvider(); },
|
|
255
|
+
get agentProvider() { return scopedContext ? scopedContext.agentProvider : getLegacyAgentProvider(); },
|
|
256
|
+
get tenant() { return scopedContext?.tenant; },
|
|
257
|
+
get tenantAdapter() { return scopedContext?.tenantAdapter; },
|
|
258
|
+
get tenantCacheKey() {
|
|
259
|
+
return accessor.tenant ? createTenantCacheKey(accessor.tenant, accessor.tenantAdapter) : undefined;
|
|
260
|
+
},
|
|
130
261
|
getDataProvider(name) {
|
|
131
262
|
const activeProviders = accessor.providers;
|
|
132
263
|
if (!activeProviders) throw new Error('DataProvider not found. Did you call setDataProvider in App.svelte?');
|
|
@@ -135,7 +266,7 @@ export function captureAdminContext(): AdminContextAccessor {
|
|
|
135
266
|
if (!provider) {
|
|
136
267
|
throw new Error(`DataProvider "${key}" not found. Available: ${Object.keys(activeProviders).join(', ')}`);
|
|
137
268
|
}
|
|
138
|
-
return provider;
|
|
269
|
+
return adaptProvider(provider);
|
|
139
270
|
},
|
|
140
271
|
getDataProviderNames() {
|
|
141
272
|
return accessor.providers ? Object.keys(accessor.providers) : [];
|
|
@@ -150,13 +281,35 @@ export function captureAdminContext(): AdminContextAccessor {
|
|
|
150
281
|
}
|
|
151
282
|
try {
|
|
152
283
|
const resource = accessor.getResource(resourceName);
|
|
153
|
-
const dataProviderName = resource.
|
|
284
|
+
const dataProviderName = resource.provider?.dataProviderName ?? resource.meta?.dataProviderName;
|
|
154
285
|
if (dataProviderName) return accessor.getDataProvider(dataProviderName);
|
|
155
286
|
} catch {
|
|
156
287
|
// Preserve the historical fallback when a resource is not registered.
|
|
157
288
|
}
|
|
158
289
|
return accessor.getDataProvider();
|
|
159
290
|
},
|
|
291
|
+
getProviderMeta(resourceName, meta) {
|
|
292
|
+
const resource = resourceName
|
|
293
|
+
? accessor.resources.find((candidate) => candidate.identifier === resourceName || candidate.name === resourceName)
|
|
294
|
+
: undefined;
|
|
295
|
+
const providerConfig = resource?.provider;
|
|
296
|
+
const tenant = accessor.tenant;
|
|
297
|
+
if (!providerConfig && !tenant) return meta;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
...(providerConfig?.meta ?? {}),
|
|
301
|
+
...(meta ?? {}),
|
|
302
|
+
...(providerConfig?.transport === undefined ? {} : { transport: providerConfig.transport }),
|
|
303
|
+
...(providerConfig?.adapter === undefined ? {} : { adapter: providerConfig.adapter }),
|
|
304
|
+
...(tenant ? resolveTenantProviderMeta(tenant, accessor.tenantAdapter) : {}),
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
withTenantCacheKey(queryKey) {
|
|
308
|
+
return appendTenantCacheKey(queryKey, accessor.tenantCacheKey);
|
|
309
|
+
},
|
|
310
|
+
matchesTenantQuery(queryKey) {
|
|
311
|
+
return queryKeyMatchesTenant(queryKey, accessor.tenantCacheKey);
|
|
312
|
+
},
|
|
160
313
|
getResource(nameOrIdentifier) {
|
|
161
314
|
const resource = accessor.resources.find(
|
|
162
315
|
(candidate) => candidate.identifier === nameOrIdentifier || candidate.name === nameOrIdentifier,
|
|
@@ -208,6 +361,10 @@ export function getDataProviderForResource(resourceName: string, overrideName?:
|
|
|
208
361
|
return captureAdminContext().getDataProviderForResource(resourceName, overrideName);
|
|
209
362
|
}
|
|
210
363
|
|
|
364
|
+
export function getProviderBundle(): ProviderBundle {
|
|
365
|
+
return captureAdminContext().providerBundle;
|
|
366
|
+
}
|
|
367
|
+
|
|
211
368
|
function isDataProvider(value: unknown): value is DataProvider {
|
|
212
369
|
return (
|
|
213
370
|
value !== null &&
|