@svadmin/core 0.22.0 → 0.23.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
package/src/audit.ts CHANGED
@@ -45,6 +45,11 @@ export function getAuditLogProvider(): AuditLogProvider | null {
45
45
  return auditLogProvider;
46
46
  }
47
47
 
48
+ export function resetAuditLogProvider(): void {
49
+ auditLogProvider = null;
50
+ handler = (entry) => { console.info('[audit]', entry.action, entry.resource, entry.recordId); };
51
+ }
52
+
48
53
  export function audit(entry: Omit<AuditEntry, 'timestamp'>): void {
49
54
  const fullEntry: AuditEntry = { ...entry, timestamp: new Date().toISOString() };
50
55
  try {
@@ -13,7 +13,7 @@ import type { AuthActionResult, CheckResult, Identity, AuthProvider } from './ty
13
13
  interface CreateAuthMutationOptions {
14
14
  method: keyof AuthProvider;
15
15
  successMessage?: string | null;
16
- errorMessage?: string;
16
+ errorMessage?: string | false;
17
17
  onSuccess?: (result: AuthActionResult) => void;
18
18
  }
19
19
 
@@ -33,13 +33,13 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
33
33
  if (options.successMessage) notify({ type: 'success', message: options.successMessage });
34
34
  if (options.onSuccess) options.onSuccess(result);
35
35
  } else {
36
- const msg = result.error?.message ?? options.errorMessage ?? t('common.operationFailed');
37
- notify({ type: 'error', message: msg });
36
+ const msg = result.error?.message ?? (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
37
+ if (options.errorMessage !== false) notify({ type: 'error', message: msg });
38
38
  }
39
39
  return result;
40
40
  } catch (err) {
41
- const msg = err instanceof Error ? err.message : (options.errorMessage ?? t('common.operationFailed'));
42
- notify({ type: 'error', message: msg });
41
+ const msg = err instanceof Error ? err.message : (typeof options.errorMessage === 'string' ? options.errorMessage : t('common.operationFailed'));
42
+ if (options.errorMessage !== false) notify({ type: 'error', message: msg });
43
43
  return { success: false, error: { message: msg } };
44
44
  } finally {
45
45
  isLoading = false;
@@ -54,11 +54,11 @@ function createAuthMutation(options: CreateAuthMutationOptions) {
54
54
 
55
55
  // ─── useLogin ─────────────────────────────────────────────────
56
56
 
57
- export function useLogin() {
57
+ export function useLogin(opts?: { errorMessage?: string | false }) {
58
58
  return createAuthMutation({
59
59
  method: 'login',
60
60
  successMessage: t('common.operationSuccess'),
61
- errorMessage: t('common.loginFailed'),
61
+ errorMessage: opts?.errorMessage ?? t('common.loginFailed'),
62
62
  onSuccess: (result) => { if (result.redirectTo) navigate(result.redirectTo); }
63
63
  });
64
64
  }
@@ -105,7 +105,7 @@ export function useUpdatePassword() {
105
105
  // ─── useGetIdentity ──────────────────────────────────────────
106
106
 
107
107
  export function useGetIdentity() {
108
- const provider = getAuthProvider();
108
+ const provider = getAuthProvider({ optional: true });
109
109
  let data = $state<Identity | null>(null);
110
110
  let isLoading = $state(true);
111
111
  let error = $state<Error | null>(null);
@@ -138,33 +138,43 @@ export function useGetIdentity() {
138
138
  // ─── useIsAuthenticated ──────────────────────────────────────
139
139
 
140
140
  export function useIsAuthenticated() {
141
- const provider = getAuthProvider();
141
+ const provider = getAuthProvider({ optional: true });
142
142
  let isAuthenticated = $state(false);
143
143
  let isLoading = $state(true);
144
+ let checkResult = $state<CheckResult | null>(null);
144
145
 
145
- if (provider) {
146
- if (typeof window !== 'undefined') {
147
- provider.check().then((result: CheckResult) => {
148
- isAuthenticated = result.authenticated;
149
- isLoading = false;
150
- }).catch(() => {
151
- isAuthenticated = false;
152
- isLoading = false;
153
- });
154
- } else {
146
+ function check() {
147
+ if (!provider) {
148
+ isAuthenticated = true;
149
+ isLoading = false;
150
+ return;
151
+ }
152
+ if (typeof window === 'undefined') {
155
153
  // In SSR we assume false to prevent hydration mismatch before check
156
154
  isAuthenticated = false;
157
155
  isLoading = false;
156
+ return;
158
157
  }
159
- } else {
160
- // No auth provider — treat as authenticated
161
- isAuthenticated = true;
162
- isLoading = false;
158
+ isLoading = true;
159
+ provider.check().then((result: CheckResult) => {
160
+ isAuthenticated = result.authenticated;
161
+ checkResult = result;
162
+ isLoading = false;
163
+ }).catch(() => {
164
+ isAuthenticated = false;
165
+ checkResult = { authenticated: false };
166
+ isLoading = false;
167
+ });
163
168
  }
164
169
 
170
+ // Initial check
171
+ check();
172
+
165
173
  return {
166
174
  get isAuthenticated() { return isAuthenticated; },
167
175
  get isLoading() { return isLoading; },
176
+ get data() { return checkResult; },
177
+ refetch: check,
168
178
  };
169
179
  }
170
180
 
@@ -176,7 +186,7 @@ export function useIsAuthenticated() {
176
186
  * If it returns { redirectTo }, navigates there.
177
187
  */
178
188
  export function useOnError() {
179
- const provider = getAuthProvider();
189
+ const provider = getAuthProvider({ optional: true });
180
190
 
181
191
  async function mutate(error: unknown) {
182
192
  if (!provider?.onError) {
@@ -242,6 +252,14 @@ export function usePermissions<T = unknown>() {
242
252
  fetch();
243
253
  }
244
254
 
255
+ const hasFn = (perm: string): boolean => {
256
+ if (!permissions) return false;
257
+ if (Array.isArray(permissions)) return permissions.includes(perm);
258
+ if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
259
+ if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
260
+ return false;
261
+ };
262
+
245
263
  return {
246
264
  get isLoading() { return isLoading; },
247
265
  get error() { return error; },
@@ -250,18 +268,10 @@ export function usePermissions<T = unknown>() {
250
268
  refetch: fetch,
251
269
 
252
270
  /** Check if a specific permission string exists */
253
- has(perm: string): boolean {
254
- if (!permissions) return false;
255
- if (Array.isArray(permissions)) return permissions.includes(perm);
256
- if (permissions instanceof Set) return (permissions as Set<string>).has(perm);
257
- if (typeof permissions === 'object') return !!(permissions as Record<string, boolean>)[perm];
258
- return false;
259
- },
271
+ has: hasFn,
260
272
 
261
273
  /** Check resource:action style permission */
262
- can(resource: string, action: string): boolean {
263
- return this.has(`${resource}:${action}`);
264
- },
274
+ can: (resource: string, action: string): boolean => hasFn(`${resource}:${action}`),
265
275
  };
266
276
  }
267
277
 
@@ -186,36 +186,23 @@ export function getAgentProvider(): AgentProvider | null {
186
186
  interface ApprovalCallback {
187
187
  (approved: boolean): void;
188
188
  }
189
- const pendingApprovals = new Map<string, ApprovalCallback>();
189
+ interface ApprovalEntry { id: string; callback: ApprovalCallback }
190
+ let pendingApprovals = $state<ApprovalEntry[]>([]);
190
191
 
191
- /**
192
- * Register a pending approval request. Called internally by the framework
193
- * when an `approval_request` event is received from the agent.
194
- */
195
192
  export function registerApproval(id: string, callback: ApprovalCallback): void {
196
- pendingApprovals.set(id, callback);
193
+ pendingApprovals = [...pendingApprovals, { id, callback }];
197
194
  }
198
195
 
199
- /**
200
- * Approve or reject a pending tool call.
201
- *
202
- * @example
203
- * ```svelte
204
- * <button onclick={() => resolveApproval('req-1', true)}>Approve</button>
205
- * <button onclick={() => resolveApproval('req-1', false)}>Reject</button>
206
- * ```
207
- */
208
196
  export function resolveApproval(id: string, approved: boolean): boolean {
209
- const cb = pendingApprovals.get(id);
210
- if (!cb) return false;
211
- cb(approved);
212
- pendingApprovals.delete(id);
197
+ const entry = pendingApprovals.find(e => e.id === id);
198
+ if (!entry) return false;
199
+ entry.callback(approved);
200
+ pendingApprovals = pendingApprovals.filter(e => e.id !== id);
213
201
  return true;
214
202
  }
215
203
 
216
- /** Check if there are any pending approval requests. */
217
204
  export function hasPendingApprovals(): boolean {
218
- return pendingApprovals.size > 0;
205
+ return pendingApprovals.length > 0;
219
206
  }
220
207
 
221
208
  // ─── Chat Context singleton ────────────────────────────────────
@@ -3,6 +3,11 @@
3
3
 
4
4
  import type { DataProvider, AuthProvider, ResourceDefinition } from './types';
5
5
  import type { RouterProvider } from './router-provider';
6
+ import { resetNotificationProvider } from './notification.svelte';
7
+ import { resetAuditLogProvider } from './audit';
8
+ import { resetAccessControlProvider } from './permissions';
9
+ import { resetAdminOptions } from './options.svelte';
10
+ import { resetI18n } from './i18n.svelte';
6
11
 
7
12
  // ─── DataProvider (supports single or multiple) ─────────────────
8
13
 
@@ -68,8 +73,8 @@ function isDataProvider(value: unknown): value is DataProvider {
68
73
 
69
74
  let authProvider = $state<AuthProvider | null>(null);
70
75
 
71
- export function setAuthProvider(provider: AuthProvider): void {
72
- authProvider = provider;
76
+ export function setAuthProvider(provider: AuthProvider | null | undefined): void {
77
+ authProvider = provider || null;
73
78
  }
74
79
 
75
80
  export function getAuthProvider(): AuthProvider;
@@ -122,3 +127,18 @@ export function setLiveProvider(provider: import('./live.svelte').LiveProvider):
122
127
  export function getLiveProvider(): import('./live.svelte').LiveProvider | undefined {
123
128
  return liveProviderState;
124
129
  }
130
+
131
+ // ─── Reset — for testing / HMR ─────────────────────────────────
132
+
133
+ export function resetContext(): void {
134
+ providers = null;
135
+ authProvider = null;
136
+ resources = [];
137
+ routerProvider = undefined;
138
+ liveProviderState = undefined;
139
+ resetNotificationProvider();
140
+ resetAuditLogProvider();
141
+ resetAccessControlProvider();
142
+ resetAdminOptions();
143
+ resetI18n();
144
+ }
@@ -57,14 +57,13 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
57
57
 
58
58
  if (options.download !== false && typeof document !== 'undefined') {
59
59
  const fields = Object.keys(mapped[0]);
60
- const header = fields.join(',');
60
+ const escapeCsvField = (s: string) => s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r') ? `"${s.replace(/"/g, '""')}"` : s;
61
+ const header = fields.map(escapeCsvField).join(',');
61
62
  const rows = mapped.map(record =>
62
63
  fields.map(f => {
63
64
  const val = record[f];
64
- const str = val === null || val === undefined ? '' : String(val);
65
- return str.includes(',') || str.includes('"') || str.includes('\n')
66
- ? `"${str.replace(/"/g, '""')}"`
67
- : str;
65
+ const str = val === null || val === undefined ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
66
+ return escapeCsvField(str);
68
67
  }).join(',')
69
68
  );
70
69
 
@@ -120,15 +119,20 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
120
119
  const errored: { request: unknown; error: unknown }[] = [];
121
120
 
122
121
  try {
123
- const text = await info.file.text();
124
- const lines = text.split('\n').filter(l => l.trim() !== '');
125
- if (lines.length < 2) return;
122
+ let text = await info.file.text();
123
+ // Strip BOM
124
+ if (text.charCodeAt(0) === 0xFEFF) {
125
+ text = text.slice(1);
126
+ }
127
+
128
+ const rows = parseCSV(text);
129
+ if (rows.length < 2) return;
126
130
 
127
- const headers = parseCSVLine(lines[0]);
131
+ const headers = rows[0];
128
132
  const records: Record<string, unknown>[] = [];
129
133
 
130
- for (let i = 1; i < lines.length; i++) {
131
- const values = parseCSVLine(lines[i]);
134
+ for (let i = 1; i < rows.length; i++) {
135
+ const values = rows[i];
132
136
  const record: Record<string, unknown> = {};
133
137
  headers.forEach((h, idx) => { record[h] = values[idx] ?? ''; });
134
138
  records.push(options.mapData ? (options.mapData(record) as unknown as Record<string, unknown>) : record);
@@ -151,18 +155,23 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
151
155
  } else {
152
156
  for (let i = 0; i < records.length; i += batchSize) {
153
157
  const batch = records.slice(i, i + batchSize);
154
- try {
155
- if (provider.createMany) {
158
+ if (provider.createMany) {
159
+ try {
156
160
  const res = await provider.createMany({ resource, variables: batch, meta: options.meta });
157
161
  succeeded.push(res);
158
- } else {
159
- for (const record of batch) {
162
+ } catch (error) {
163
+ errored.push({ request: batch, error });
164
+ }
165
+ } else {
166
+ // Fallback: loop internal items inside batch securely instead of breaking whole batch
167
+ for (const record of batch) {
168
+ try {
160
169
  const res = await provider.create({ resource, variables: record, meta: options.meta });
161
170
  succeeded.push(res);
171
+ } catch (error) {
172
+ errored.push({ request: record, error });
162
173
  }
163
174
  }
164
- } catch (error) {
165
- errored.push({ request: batch, error });
166
175
  }
167
176
  processed += batch.length;
168
177
  options.onProgress?.({ totalAmount: records.length, processedAmount: processed });
@@ -184,34 +193,57 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
184
193
  };
185
194
  }
186
195
 
187
- /** Parse a single CSV line respecting quoted fields */
188
- function parseCSVLine(line: string): string[] {
189
- const result: string[] = [];
190
- let current = '';
196
+ /** Parse full CSV text respecting quoted multi-line fields */
197
+ function parseCSV(text: string): string[][] {
198
+ const rows: string[][] = [];
199
+ let currentRow: string[] = [];
200
+ let currentVal = '';
191
201
  let inQuotes = false;
202
+ let wasQuoted = false;
192
203
 
193
- for (let i = 0; i < line.length; i++) {
194
- const ch = line[i];
204
+ for (let i = 0; i < text.length; i++) {
205
+ const ch = text[i];
195
206
  if (inQuotes) {
196
- if (ch === '"' && line[i + 1] === '"') {
197
- current += '"';
198
- i++;
199
- } else if (ch === '"') {
200
- inQuotes = false;
207
+ if (ch === '"') {
208
+ if (i + 1 < text.length && text[i + 1] === '"') {
209
+ currentVal += '"';
210
+ i++;
211
+ } else {
212
+ inQuotes = false;
213
+ }
201
214
  } else {
202
- current += ch;
215
+ currentVal += ch;
203
216
  }
204
217
  } else {
205
218
  if (ch === '"') {
206
219
  inQuotes = true;
220
+ wasQuoted = true;
207
221
  } else if (ch === ',') {
208
- result.push(current.trim());
209
- current = '';
222
+ currentRow.push(wasQuoted ? currentVal : currentVal.trim());
223
+ currentVal = '';
224
+ wasQuoted = false;
225
+ } else if (ch === '\n' || ch === '\r') {
226
+ currentRow.push(wasQuoted ? currentVal : currentVal.trim());
227
+ if (currentRow.some(v => v !== '')) {
228
+ rows.push(currentRow);
229
+ }
230
+ currentRow = [];
231
+ currentVal = '';
232
+ wasQuoted = false;
233
+ if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
234
+ i++;
235
+ }
210
236
  } else {
211
- current += ch;
237
+ currentVal += ch;
212
238
  }
213
239
  }
214
240
  }
215
- result.push(current.trim());
216
- return result;
241
+
242
+ if (currentVal || currentRow.length > 0) {
243
+ currentRow.push(wasQuoted ? currentVal : currentVal.trim());
244
+ if (currentRow.some(v => v !== '')) {
245
+ rows.push(currentRow);
246
+ }
247
+ }
248
+ return rows;
217
249
  }