@svadmin/core 0.50.0 → 0.52.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 +21 -4
- package/src/access-control-contract.ts +250 -0
- package/src/adapters/casbin.ts +20 -14
- package/src/adapters/casl.ts +22 -19
- package/src/admin-tool.ts +7 -7
- package/src/audit-contract.ts +105 -0
- package/src/audit.ts +88 -128
- package/src/auth-hooks.svelte.ts +593 -261
- package/src/auth-mutation-contract.ts +143 -0
- package/src/auth-query-contract.ts +97 -0
- package/src/captured-mutation.svelte.ts +45 -0
- package/src/chatProvider.svelte.ts +1 -1
- package/src/command-contract.ts +195 -0
- package/src/context.svelte.ts +217 -205
- package/src/contract-context.svelte.ts +37 -0
- package/src/contract-form.svelte.ts +566 -0
- package/src/create-contract.ts +56 -0
- package/src/create-hooks.svelte.ts +347 -0
- package/src/create-many-contract.ts +90 -0
- package/src/create-many-hooks.svelte.ts +396 -0
- package/src/data-transfer.svelte.ts +1 -215
- package/src/defined-options.ts +50 -0
- package/src/delete-contract.ts +50 -0
- package/src/delete-hooks.svelte.ts +403 -0
- package/src/delete-many-contract.ts +60 -0
- package/src/delete-many-hooks.svelte.ts +402 -0
- package/src/enterprise.ts +4 -4
- package/src/export-format.ts +16 -14
- package/src/export-hooks.svelte.ts +191 -0
- package/src/feature-gate-contract.ts +53 -0
- package/src/hook-utils.svelte.ts +53 -105
- package/src/hooks.svelte.ts +19 -776
- package/src/http.ts +88 -87
- package/src/i18n.svelte.ts +56 -2
- package/src/import-contract.ts +106 -0
- package/src/import-hooks.svelte.ts +310 -0
- package/src/index.ts +77 -30
- package/src/inferencer-graphql.ts +7 -6
- package/src/infinite-query.svelte.ts +228 -0
- package/src/invalidation-contract.ts +26 -0
- package/src/live-sse.ts +91 -127
- package/src/live-subscription.svelte.ts +130 -0
- package/src/live-transport.ts +127 -0
- package/src/live-websocket.ts +85 -118
- package/src/live.svelte.ts +66 -68
- package/src/notification.svelte.ts +17 -15
- package/src/permission-catalog.ts +121 -0
- package/src/permission-hints-contract.ts +48 -0
- package/src/permissions.svelte.ts +112 -59
- package/src/plain-data.ts +61 -0
- package/src/provider-bundle.ts +85 -79
- package/src/provider-response.ts +35 -0
- package/src/query-hooks.svelte.ts +241 -288
- package/src/query-invalidation.ts +172 -0
- package/src/query-keys.ts +221 -110
- package/src/query-session.svelte.ts +216 -0
- package/src/query-snapshot.ts +144 -0
- package/src/reactive-projection.ts +53 -0
- package/src/record-decoder.ts +63 -0
- package/src/resource-binding.svelte.ts +19 -0
- package/src/resource-contract.ts +595 -0
- package/src/resource-mutation-types.ts +30 -0
- package/src/resource-schemas.ts +371 -0
- package/src/resource-transport-config.ts +156 -0
- package/src/route-parsing.ts +11 -11
- package/src/router-provider.ts +64 -59
- package/src/router.ts +8 -3
- package/src/routing-hooks.svelte.ts +90 -62
- package/src/schema-validation.ts +92 -0
- package/src/schema.ts +13 -0
- package/src/select-options.ts +95 -0
- package/src/select-query.svelte.ts +191 -0
- package/src/session-query.svelte.ts +88 -0
- package/src/strict-command-hooks.svelte.ts +333 -0
- package/src/strict-hooks.svelte.ts +198 -0
- package/src/table-hooks.svelte.ts +180 -174
- package/src/table-state.svelte.ts +117 -117
- package/src/task-contract.ts +87 -0
- package/src/task-hooks.svelte.ts +193 -149
- package/src/task-provider.ts +115 -0
- package/src/task-subscription.ts +132 -0
- package/src/theme.svelte.ts +155 -154
- package/src/toast.svelte.ts +38 -37
- package/src/types.ts +55 -108
- package/src/unsafe.ts +7 -0
- package/src/update-contract.ts +65 -0
- package/src/update-hooks.svelte.ts +347 -0
- package/src/update-many-contract.ts +79 -0
- package/src/update-many-hooks.svelte.ts +373 -0
- package/src/url-sync.ts +11 -10
- package/src/useCan.ts +96 -31
- package/src/useParsed.svelte.ts +18 -14
- package/src/useStepsForm.svelte.ts +123 -63
- package/src/utility-hooks.svelte.ts +101 -158
- package/src/form-hooks.svelte.ts +0 -889
- package/src/mutation-hooks.svelte.ts +0 -561
- package/src/schema-form.ts +0 -31
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/svelte-query';
|
|
2
|
+
import { captureAdminContext } from './context.svelte';
|
|
3
|
+
import { captureQueryProvider } from './query-snapshot';
|
|
4
|
+
import { contractKey, requireContractCreateSchema, parseContractCreateInput, parseContractRecord, parseContractId,
|
|
5
|
+
type ResourceContract, type ContractSchemas, type ContractCreateInput, type ContractRecord } from './resource-contract';
|
|
6
|
+
import { decodeBaseRecord } from './record-decoder';
|
|
7
|
+
import { snapshotPlainData } from './plain-data';
|
|
8
|
+
import { createInputId } from './create-contract';
|
|
9
|
+
import { definedOptions } from './defined-options';
|
|
10
|
+
import { invalidateOwnedQueries } from './query-invalidation';
|
|
11
|
+
import { captureAuthLiveScope, clearAuthQueries, handleAuthError } from './auth-hooks.svelte';
|
|
12
|
+
import { HttpError } from './types';
|
|
13
|
+
import { decodeImportReceipt, invalidImportInput, parseImportRows, snapshotImportFile,
|
|
14
|
+
newImportProgress, snapshotImportProgress, type ImportOperationProgress, type ImportFormat } from './import-contract';
|
|
15
|
+
|
|
16
|
+
export type ImportProgress = Pick<ImportOperationProgress, 'totalAmount' | 'processedAmount'>;
|
|
17
|
+
export interface ImportResult<S extends ContractSchemas> {
|
|
18
|
+
succeeded: ContractRecord<S>[];
|
|
19
|
+
errored: { row: number; request: ContractCreateInput<S>; error: HttpError }[];
|
|
20
|
+
}
|
|
21
|
+
export interface UseImportOptions<S extends ContractSchemas> {
|
|
22
|
+
resource: ResourceContract<S> & ([ContractCreateInput<NoInfer<S>>] extends [never] ? never : unknown);
|
|
23
|
+
format?: ImportFormat;
|
|
24
|
+
mapData?: (item: Record<string, unknown>) => NoInfer<ContractCreateInput<S>>;
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
28
|
+
dataProviderName?: string;
|
|
29
|
+
onFinish?: (result: ImportResult<NoInfer<S>>) => void | Promise<void>;
|
|
30
|
+
onProgress?: (info: ImportProgress) => void | Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cancelled(progress: ImportOperationProgress = newImportProgress()): HttpError {
|
|
34
|
+
const checked = snapshotImportProgress(progress);
|
|
35
|
+
return new HttpError('Import scope changed', 409, undefined, {
|
|
36
|
+
code: 'IMPORT_CANCELLED', details: { writeMayHaveSucceeded: checked.attemptedAmount > 0, ...checked },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function importFailure(cause: unknown, dispatched = false): HttpError {
|
|
40
|
+
let code: unknown;
|
|
41
|
+
let status: unknown;
|
|
42
|
+
try {
|
|
43
|
+
if (typeof cause === 'object' && cause !== null) {
|
|
44
|
+
code = Object.getOwnPropertyDescriptor(cause, 'code')?.value;
|
|
45
|
+
status = Object.getOwnPropertyDescriptor(cause, 'statusCode')?.value;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Error objects are untrusted diagnostic sources.
|
|
49
|
+
}
|
|
50
|
+
const input = code === 'INVALID_RESOURCE_INPUT' && !dispatched;
|
|
51
|
+
const contract = code === 'INVALID_RESOURCE_CONTRACT' && !dispatched;
|
|
52
|
+
const response = code === 'INVALID_PROVIDER_RESPONSE';
|
|
53
|
+
return new HttpError(input ? 'Invalid import input' : contract ? 'A create schema is required'
|
|
54
|
+
: response ? 'Invalid provider response' : 'Import failed', input ? 422 : contract ? 400 : response ? 502
|
|
55
|
+
: status === 401 || status === 403 ? status : 500, undefined, {
|
|
56
|
+
code: input ? 'INVALID_RESOURCE_INPUT' : contract ? 'INVALID_RESOURCE_CONTRACT'
|
|
57
|
+
: response ? 'INVALID_PROVIDER_RESPONSE' : 'IMPORT_FAILED',
|
|
58
|
+
details: { phase: dispatched ? response ? 'response' : 'request' : 'input', writeMayHaveSucceeded: dispatched },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function copyFailure(error: HttpError): HttpError {
|
|
62
|
+
return new HttpError(error.message, error.statusCode, undefined, {
|
|
63
|
+
...definedOptions({ code: error.code }), details: snapshotPlainData(error.details ?? {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function notify<T>(callback: ((value: T) => void | Promise<void>) | undefined, value: T): void {
|
|
67
|
+
try { void Promise.resolve(callback?.(value)).catch(() => {}); }
|
|
68
|
+
catch { /* Observers cannot turn completed writes into a second import attempt. */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A whole-file preflight precedes writes; each provider receipt remains untrusted until checked. */
|
|
72
|
+
export function useImport<S extends ContractSchemas>(options: UseImportOptions<S>) {
|
|
73
|
+
const context = captureAdminContext();
|
|
74
|
+
const client = useQueryClient();
|
|
75
|
+
contractKey(options.resource);
|
|
76
|
+
let isLoading = $state(false);
|
|
77
|
+
let mutationResult = $state.raw<ImportResult<S> | null>(null);
|
|
78
|
+
let progress = $state.raw<ImportProgress>({ totalAmount: 0, processedAmount: 0 });
|
|
79
|
+
let error = $state.raw<HttpError | null>(null);
|
|
80
|
+
let mounted = true;
|
|
81
|
+
|
|
82
|
+
function captureOrigin() {
|
|
83
|
+
const authProvider = context.authProvider;
|
|
84
|
+
return { contract: options.resource, authProvider, authScope: captureAuthLiveScope(authProvider),
|
|
85
|
+
router: context.routerProvider, tenant: context.tenantCacheKey?.__svadminTenant };
|
|
86
|
+
}
|
|
87
|
+
type Origin = ReturnType<typeof captureOrigin>;
|
|
88
|
+
function targetOriginCurrent(origin: Origin): boolean {
|
|
89
|
+
return mounted && options.enabled !== false && origin.contract === options.resource &&
|
|
90
|
+
origin.authProvider === context.authProvider && origin.router === context.routerProvider &&
|
|
91
|
+
origin.tenant === context.tenantCacheKey?.__svadminTenant;
|
|
92
|
+
}
|
|
93
|
+
function originCurrent(origin: Origin): boolean {
|
|
94
|
+
return targetOriginCurrent(origin) && origin.authScope.isCurrent();
|
|
95
|
+
}
|
|
96
|
+
function captureScope() {
|
|
97
|
+
const origin = captureOrigin();
|
|
98
|
+
const contract = origin.contract;
|
|
99
|
+
const captured = captureQueryProvider(context, {
|
|
100
|
+
resource: contract.name, ...definedOptions({ dataProviderName: options.dataProviderName, meta: options.meta }),
|
|
101
|
+
});
|
|
102
|
+
const name = context.resolveDataProviderName(contract.name, options.dataProviderName);
|
|
103
|
+
const raw = context.providers?.[name];
|
|
104
|
+
if (!raw) throw new HttpError('Data provider is unavailable', 400, undefined, { code: 'DATA_PROVIDER_REQUIRED' });
|
|
105
|
+
return {
|
|
106
|
+
...origin, source: captured.source, meta: captured.meta,
|
|
107
|
+
create: raw.create.bind(raw), createMany: raw.createMany?.bind(raw),
|
|
108
|
+
matcher: { ...context.queryKeyMatcher(contract.name, options.dataProviderName), contract: contractKey(contract) },
|
|
109
|
+
key: JSON.stringify([contractKey(contract), name, captured.source, context.tenantCacheKey?.__svadminTenant, captured.meta]),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
type Scope = ReturnType<typeof captureScope>;
|
|
113
|
+
let current: { token: object; scope: Scope; file: File; promise: Promise<ImportResult<S>>; progress: ImportOperationProgress } | undefined;
|
|
114
|
+
let latest = $state.raw<{ token: object; origin: Origin; scope: Scope | undefined }>();
|
|
115
|
+
let latestToken: object | undefined;
|
|
116
|
+
function targetCurrent(scope: Scope): boolean {
|
|
117
|
+
if (!targetOriginCurrent(scope)) return false;
|
|
118
|
+
try { return captureScope().key === scope.key; }
|
|
119
|
+
catch { return false; }
|
|
120
|
+
}
|
|
121
|
+
function sessionCurrent(scope: Scope): boolean {
|
|
122
|
+
return context.authProvider === scope.authProvider && scope.authScope.isCurrent();
|
|
123
|
+
}
|
|
124
|
+
function isCurrent(token: object, scope: Scope): boolean {
|
|
125
|
+
return latestToken === token && targetCurrent(scope) && sessionCurrent(scope);
|
|
126
|
+
}
|
|
127
|
+
function viewCurrent(): boolean {
|
|
128
|
+
return latest !== undefined && latestToken === latest.token && originCurrent(latest.origin) &&
|
|
129
|
+
(latest.scope === undefined || targetCurrent(latest.scope));
|
|
130
|
+
}
|
|
131
|
+
function clearState() {
|
|
132
|
+
current = undefined;
|
|
133
|
+
latest = undefined;
|
|
134
|
+
isLoading = false;
|
|
135
|
+
mutationResult = null;
|
|
136
|
+
error = null;
|
|
137
|
+
progress = { totalAmount: 0, processedAmount: 0 };
|
|
138
|
+
}
|
|
139
|
+
function reset() {
|
|
140
|
+
latestToken = undefined;
|
|
141
|
+
clearState();
|
|
142
|
+
}
|
|
143
|
+
$effect.pre(() => {
|
|
144
|
+
if (latest && !viewCurrent()) {
|
|
145
|
+
// Auth retirement alone must not cancel the delegate completing its own logout.
|
|
146
|
+
if (!targetOriginCurrent(latest.origin) || (latest.scope && !targetCurrent(latest.scope))) latestToken = undefined;
|
|
147
|
+
clearState();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
$effect(() => () => { mounted = false; reset(); });
|
|
151
|
+
|
|
152
|
+
async function refresh(scope: Scope) {
|
|
153
|
+
try {
|
|
154
|
+
await invalidateOwnedQueries({
|
|
155
|
+
client, matcher: { ...scope.matcher, resource: scope.contract.name },
|
|
156
|
+
owner: { source: scope.source, authSession: scope.authScope.cacheKey },
|
|
157
|
+
scopes: ['list', 'many'], current: () => sessionCurrent(scope),
|
|
158
|
+
});
|
|
159
|
+
} catch { /* Cache failures cannot change completed write receipts into failures. */ }
|
|
160
|
+
}
|
|
161
|
+
function copyResult(scope: Scope, result: ImportResult<S>): ImportResult<S> {
|
|
162
|
+
return {
|
|
163
|
+
succeeded: result.succeeded.map(row => parseContractRecord(scope.contract, row)),
|
|
164
|
+
errored: result.errored.map(item => ({
|
|
165
|
+
row: item.row, request: parseContractCreateInput(scope.contract, item.request),
|
|
166
|
+
error: copyFailure(item.error),
|
|
167
|
+
})),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function detached(operation: NonNullable<typeof current>): Promise<ImportResult<S>> {
|
|
171
|
+
return operation.promise.then(result => {
|
|
172
|
+
if (!isCurrent(operation.token, operation.scope)) throw cancelled(operation.progress);
|
|
173
|
+
return copyResult(operation.scope, result);
|
|
174
|
+
}, (cause: unknown) => {
|
|
175
|
+
if (!isCurrent(operation.token, operation.scope)) throw cancelled(operation.progress);
|
|
176
|
+
throw cause instanceof HttpError ? copyFailure(cause) : importFailure(cause, operation.progress.attemptedAmount > 0);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleChange(info: { file: File }): Promise<ImportResult<S>> {
|
|
181
|
+
if (!mounted) return Promise.reject(cancelled());
|
|
182
|
+
const origin = captureOrigin();
|
|
183
|
+
const token = {};
|
|
184
|
+
if (!originCurrent(origin)) {
|
|
185
|
+
latestToken = token;
|
|
186
|
+
return Promise.reject(cancelled());
|
|
187
|
+
}
|
|
188
|
+
let captured: Scope | undefined;
|
|
189
|
+
try {
|
|
190
|
+
const file = snapshotImportFile(info);
|
|
191
|
+
const scope = captureScope();
|
|
192
|
+
captured = scope;
|
|
193
|
+
if (!originCurrent(origin)) return Promise.reject(cancelled());
|
|
194
|
+
if (current && isCurrent(current.token, current.scope)) {
|
|
195
|
+
if (current.file === file.file) return detached(current);
|
|
196
|
+
return Promise.reject(new HttpError('Import is already running', 409, undefined, { code: 'IMPORT_BUSY' }));
|
|
197
|
+
}
|
|
198
|
+
requireContractCreateSchema(scope.contract);
|
|
199
|
+
const batchSize = options.batchSize ?? 1;
|
|
200
|
+
const format = options.format ?? 'auto';
|
|
201
|
+
const mapData = options.mapData;
|
|
202
|
+
const onFinish = options.onFinish;
|
|
203
|
+
const onProgress = options.onProgress;
|
|
204
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || !['auto', 'csv', 'json'].includes(format) ||
|
|
205
|
+
(options.enabled !== undefined && typeof options.enabled !== 'boolean') ||
|
|
206
|
+
[mapData, onFinish, onProgress].some(callback => callback !== undefined && typeof callback !== 'function')) invalidImportInput();
|
|
207
|
+
if (!originCurrent(origin)) return Promise.reject(cancelled());
|
|
208
|
+
latestToken = token;
|
|
209
|
+
latest = Object.freeze({ token, origin, scope });
|
|
210
|
+
const tracked = newImportProgress();
|
|
211
|
+
const ensureActive = () => { if (!isCurrent(token, scope) || current?.token !== token) throw cancelled(tracked); };
|
|
212
|
+
isLoading = true;
|
|
213
|
+
error = null;
|
|
214
|
+
mutationResult = null;
|
|
215
|
+
progress = { totalAmount: 0, processedAmount: 0 };
|
|
216
|
+
const promise = Promise.resolve().then(async () => {
|
|
217
|
+
const result: ImportResult<S> = { succeeded: [], errored: [] };
|
|
218
|
+
try {
|
|
219
|
+
ensureActive();
|
|
220
|
+
const text = await file.read();
|
|
221
|
+
ensureActive();
|
|
222
|
+
const records = parseImportRows(text, file.name, format).map((row, index) => {
|
|
223
|
+
ensureActive();
|
|
224
|
+
try {
|
|
225
|
+
const input = mapData ? mapData(decodeBaseRecord(snapshotPlainData(row))) : row;
|
|
226
|
+
ensureActive();
|
|
227
|
+
const checked = parseContractCreateInput(scope.contract, input);
|
|
228
|
+
createInputId(scope.contract, checked);
|
|
229
|
+
return checked;
|
|
230
|
+
} catch { return invalidImportInput(index + 1); }
|
|
231
|
+
});
|
|
232
|
+
ensureActive();
|
|
233
|
+
const seen = new Set<string | number>();
|
|
234
|
+
tracked.totalAmount = records.length;
|
|
235
|
+
progress = { totalAmount: records.length, processedAmount: 0 };
|
|
236
|
+
for (let start = 0; start < records.length; start += scope.createMany ? batchSize : 1) {
|
|
237
|
+
ensureActive();
|
|
238
|
+
const batch = records.slice(start, start + (scope.createMany ? batchSize : 1));
|
|
239
|
+
try {
|
|
240
|
+
const request = { resource: scope.contract.name, ...definedOptions({
|
|
241
|
+
meta: scope.meta === undefined ? undefined : decodeBaseRecord(snapshotPlainData(scope.meta)),
|
|
242
|
+
}) };
|
|
243
|
+
tracked.attemptedAmount += batch.length;
|
|
244
|
+
const response = batchSize > 1 && scope.createMany
|
|
245
|
+
? await scope.createMany({ ...request, variables: batch.map(row => snapshotPlainData(row)) })
|
|
246
|
+
: await scope.create({ ...request, variables: snapshotPlainData(batch[0]) });
|
|
247
|
+
if (!sessionCurrent(scope)) throw cancelled(tracked);
|
|
248
|
+
const rows = decodeImportReceipt(scope.contract, response, batch, batchSize > 1 && !!scope.createMany, seen);
|
|
249
|
+
for (const row of rows) seen.add(parseContractId(scope.contract, row.id));
|
|
250
|
+
result.succeeded.push(...rows);
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
if (!sessionCurrent(scope)) throw cancelled(tracked);
|
|
253
|
+
const failure = importFailure(cause, true);
|
|
254
|
+
for (const [index, request] of batch.entries()) result.errored.push({ row: start + index + 1, request, error: failure });
|
|
255
|
+
}
|
|
256
|
+
tracked.processedAmount += batch.length;
|
|
257
|
+
ensureActive();
|
|
258
|
+
progress = { totalAmount: tracked.totalAmount, processedAmount: tracked.processedAmount };
|
|
259
|
+
notify(onProgress, { ...progress });
|
|
260
|
+
ensureActive();
|
|
261
|
+
}
|
|
262
|
+
} catch (cause) {
|
|
263
|
+
if (!isCurrent(token, scope) || current?.token !== token) throw cancelled(tracked);
|
|
264
|
+
const failure = importFailure(cause, tracked.attemptedAmount > 0);
|
|
265
|
+
error = failure;
|
|
266
|
+
throw failure;
|
|
267
|
+
} finally {
|
|
268
|
+
if (tracked.attemptedAmount > 0 && sessionCurrent(scope)) await refresh(scope);
|
|
269
|
+
}
|
|
270
|
+
ensureActive();
|
|
271
|
+
mutationResult = copyResult(scope, result);
|
|
272
|
+
error = result.errored[0] ? copyFailure(result.errored[0].error) : null;
|
|
273
|
+
const authError = result.errored.find(item => item.error.statusCode === 401 || item.error.statusCode === 403)?.error ?? error;
|
|
274
|
+
if (authError) notify(async () => {
|
|
275
|
+
await handleAuthError(copyFailure(authError), context, () => targetCurrent(scope) && latestToken === token,
|
|
276
|
+
() => clearAuthQueries(client, scope.authProvider), scope.authScope,
|
|
277
|
+
() => mounted && latestToken !== token);
|
|
278
|
+
}, undefined);
|
|
279
|
+
if (isCurrent(token, scope)) notify(onFinish, copyResult(scope, result));
|
|
280
|
+
ensureActive();
|
|
281
|
+
return copyResult(scope, result);
|
|
282
|
+
}).finally(() => {
|
|
283
|
+
if (current?.token === token) { current = undefined; isLoading = false; }
|
|
284
|
+
});
|
|
285
|
+
current = { token, scope, file: file.file, promise, progress: tracked };
|
|
286
|
+
return detached(current);
|
|
287
|
+
} catch (cause) {
|
|
288
|
+
const failure = importFailure(cause);
|
|
289
|
+
if (current && isCurrent(current.token, current.scope)) return Promise.reject(copyFailure(failure));
|
|
290
|
+
latestToken = token;
|
|
291
|
+
latest = Object.freeze({ token, origin, scope: captured });
|
|
292
|
+
error = failure;
|
|
293
|
+
mutationResult = null;
|
|
294
|
+
isLoading = false;
|
|
295
|
+
progress = { totalAmount: 0, processedAmount: 0 };
|
|
296
|
+
return Promise.resolve().then(() => {
|
|
297
|
+
if (!originCurrent(origin) || latestToken !== token) throw cancelled();
|
|
298
|
+
throw copyFailure(failure);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
inputProps: { type: 'file' as const, accept: '.csv,.json' },
|
|
304
|
+
handleChange, reset,
|
|
305
|
+
get isLoading() { return viewCurrent() && isLoading; },
|
|
306
|
+
get mutationResult() { return viewCurrent() && latest?.scope && mutationResult ? copyResult(latest.scope, mutationResult) : null; },
|
|
307
|
+
get progress(): Readonly<ImportProgress> { return viewCurrent() ? { ...progress } : { totalAmount: 0, processedAmount: 0 }; },
|
|
308
|
+
get error() { return viewCurrent() && error ? copyFailure(error) : null; },
|
|
309
|
+
};
|
|
310
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export {
|
|
|
11
11
|
provideTenantContext, getTenantContext, getProviderBundle,
|
|
12
12
|
resetContext,
|
|
13
13
|
} from './context.svelte';
|
|
14
|
-
export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor } from './context.svelte';
|
|
14
|
+
export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor, ResolvedProviderBundle } from './context.svelte';
|
|
15
15
|
export {
|
|
16
16
|
createProviderBundle,
|
|
17
17
|
createTenantCacheKey,
|
|
@@ -50,24 +50,36 @@ export type {
|
|
|
50
50
|
TenantId,
|
|
51
51
|
} from './provider-bundle';
|
|
52
52
|
export {
|
|
53
|
-
|
|
54
|
-
useOne, useShow,
|
|
55
|
-
useSelect, useMany,
|
|
56
|
-
useCustom, useApiUrl,
|
|
57
|
-
useCustomMutation, useInvalidate,
|
|
58
|
-
useCreate, useCreateMany,
|
|
59
|
-
useUpdate, useUpdateMany,
|
|
60
|
-
useDelete, useDeleteMany,
|
|
61
|
-
useForm, useTable,
|
|
53
|
+
useApiUrl,
|
|
62
54
|
useNavigation, useGo, useBack,
|
|
63
55
|
useGetToPath, useLink,
|
|
64
56
|
useResource,
|
|
65
|
-
|
|
66
|
-
useOvertime,
|
|
67
|
-
useNotification,
|
|
57
|
+
useModal,
|
|
58
|
+
useOvertime,
|
|
59
|
+
useNotification,
|
|
68
60
|
useMenu, useBreadcrumb, useThemedLayoutContext,
|
|
69
|
-
|
|
61
|
+
resetSidebarCollapsed,
|
|
70
62
|
} from './hooks.svelte';
|
|
63
|
+
export {
|
|
64
|
+
useList, useOne, useShow, useMany, useInfiniteList, useSelect, useTable,
|
|
65
|
+
useCreate, useUpdate, useDelete, useCreateMany, useUpdateMany, useDeleteMany, useForm,
|
|
66
|
+
useInvalidate,
|
|
67
|
+
} from './strict-hooks.svelte';
|
|
68
|
+
export { defineResource, getContractFormFields } from './resource-contract';
|
|
69
|
+
export { UpdateManyPartialError } from './update-many-contract';
|
|
70
|
+
export { CreateManyPartialError } from './create-many-contract';
|
|
71
|
+
export type { ContractCreateOptions as UseCreateOptions } from './create-hooks.svelte';
|
|
72
|
+
export type { ContractCreateParams as UseCreateMutateParams } from './create-contract';
|
|
73
|
+
export type { ContractUpdateOptions as UseUpdateOptions } from './update-hooks.svelte';
|
|
74
|
+
export type { ContractUpdateParams as UseUpdateMutateParams } from './update-contract';
|
|
75
|
+
export type { ContractDeleteOptions as UseDeleteOptions } from './delete-hooks.svelte';
|
|
76
|
+
export type { ContractDeleteParams as UseDeleteMutateParams } from './delete-contract';
|
|
77
|
+
export type { ContractFormAction, ContractFormDraft, ContractFormValues } from './resource-contract';
|
|
78
|
+
export type { ResourceContract, ContractSchemas, ContractRecord, ContractId, ContractInput } from './resource-contract';
|
|
79
|
+
export type { ContractFilter, ContractSort } from './strict-hooks.svelte';
|
|
80
|
+
export { defineCommand } from './command-contract';
|
|
81
|
+
export type { CommandContract, CommandInput, CommandOutput } from './command-contract';
|
|
82
|
+
export { useCustom, useCustomMutation } from './strict-command-hooks.svelte';
|
|
71
83
|
export { matchRoute, navigate, currentPath, setActiveRouterProvider, beforeEach, afterEach, resetRouter } from './router';
|
|
72
84
|
export type { RouteGuard } from './router';
|
|
73
85
|
export {
|
|
@@ -77,7 +89,19 @@ export {
|
|
|
77
89
|
sanitizeListQueryParams,
|
|
78
90
|
writeURLState,
|
|
79
91
|
} from './url-sync';
|
|
80
|
-
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, canAccessAsync, createFeatureGate } from './permissions.svelte';
|
|
92
|
+
export { setAccessControlProvider, getAccessControlProvider, getAccessControlOptions, resetAccessControlProvider, canAccessAsync, createFeatureGate } from './permissions.svelte';
|
|
93
|
+
export {
|
|
94
|
+
canonicalPermission,
|
|
95
|
+
createPermissionAccessControlProvider,
|
|
96
|
+
decodePermissionCatalog,
|
|
97
|
+
decodePermissionSnapshot,
|
|
98
|
+
} from './permission-catalog';
|
|
99
|
+
export type {
|
|
100
|
+
PermissionCatalog,
|
|
101
|
+
PermissionCatalogBinding,
|
|
102
|
+
PermissionProjectionOptions,
|
|
103
|
+
PermissionSnapshot,
|
|
104
|
+
} from './permission-catalog';
|
|
81
105
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
82
106
|
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, getToasts, removeToast, resetToast } from './toast.svelte';
|
|
83
107
|
export {
|
|
@@ -90,8 +114,11 @@ export {
|
|
|
90
114
|
export type { NotificationParams } from './notification.svelte';
|
|
91
115
|
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
|
|
92
116
|
export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
|
|
93
|
-
export { audit, auditWithProvider, writeAuditEntry, recordMutationRollback, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
94
|
-
export type { AuditLogProvider } from './audit';
|
|
117
|
+
export { audit, auditWithProvider, writeAuditEntry, recordMutationRollback, setAuditHandler, setAuditLogProvider, getAuditLogProvider, withValidatedAuditProvider, AuditError } from './audit';
|
|
118
|
+
export type { AuditLogProvider, AuditLogTransport, AuditCreateParams, AuditQueryParams, AuditDraft } from './audit';
|
|
119
|
+
export { TaskError } from './task-contract';
|
|
120
|
+
export { withValidatedTaskProvider } from './task-provider';
|
|
121
|
+
export type { TaskTransport } from './task-provider';
|
|
95
122
|
export type {
|
|
96
123
|
ApiCredentialSummary,
|
|
97
124
|
CreatedApiCredential,
|
|
@@ -131,6 +158,8 @@ export { checkError } from './hook-utils.svelte';
|
|
|
131
158
|
export type { NotificationConfig, OvertimeOptions, OvertimeResult, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
132
159
|
|
|
133
160
|
export { DeleteManyPartialError, HttpError, UndoError } from './types';
|
|
161
|
+
export { withResourceSchemas } from './resource-schemas';
|
|
162
|
+
export type { ResourceSchemas, ResourceSchemaMap, InferSchemaResourceMap, InferSchemaInputMap } from './resource-schemas';
|
|
134
163
|
export type {
|
|
135
164
|
DataProvider, AuthProvider, NotificationProvider, MutationMode,
|
|
136
165
|
ValidationErrors, HttpErrorOptions, CrudOperator, LogicalFilter, FieldFilter,
|
|
@@ -148,20 +177,24 @@ export type {
|
|
|
148
177
|
ResourceDefinition, ResourceProviderConfig, ResourceTransportConfig, ResourceAdapterConfig,
|
|
149
178
|
FieldDefinition, MenuItem,
|
|
150
179
|
AuthActionResult, CheckResult,
|
|
151
|
-
ResourceTypeMap, KnownResources, InferData,
|
|
180
|
+
ResourceTypeMap, ResourceInputMap, ResourceInputOperation, InferResourceInput, KnownResources, InferData,
|
|
152
181
|
BaseRecord, Role, AuditLog
|
|
153
182
|
} from './types';
|
|
154
183
|
export type { InvalidateScope } from './options.svelte';
|
|
155
184
|
export type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
|
156
|
-
export type { Action, CanParams, CanResult, AccessControlProvider, FeatureGateConfig, FeatureGateUser } from './permissions.svelte';
|
|
185
|
+
export type { Action, CanParams, CanResult, AccessControlProvider, RegisteredAccessControlProvider, AccessControlOptions, FeatureGateConfig, FeatureGateUser } from './permissions.svelte';
|
|
157
186
|
export type { AuditEntry, AuditHandler, AuditAction } from './audit';
|
|
158
187
|
export { useCan } from './useCan';
|
|
159
188
|
export type { UseCanOptions, UseCanResult } from './useCan';
|
|
160
189
|
export { createCaslAccessControl } from './adapters/casl';
|
|
161
190
|
export { createCasbinAccessControl } from './adapters/casbin';
|
|
162
191
|
export type { CasbinAdapterOptions } from './adapters/casbin';
|
|
163
|
-
export {
|
|
164
|
-
export
|
|
192
|
+
export { downloadData, toCsv, toJson, toXlsx, parseCSV } from './data-transfer.svelte';
|
|
193
|
+
export { useExport } from './export-hooks.svelte';
|
|
194
|
+
export type { UseExportOptions } from './export-hooks.svelte';
|
|
195
|
+
export { useImport } from './import-hooks.svelte';
|
|
196
|
+
export type { UseImportOptions, ImportProgress, ImportResult } from './import-hooks.svelte';
|
|
197
|
+
export type { ExportFormat } from './data-transfer.svelte';
|
|
165
198
|
export {
|
|
166
199
|
useLogin, useLogout,
|
|
167
200
|
useRegister, useForgotPassword, useUpdatePassword,
|
|
@@ -169,16 +202,25 @@ export {
|
|
|
169
202
|
useGetIdentity, useIsAuthenticated,
|
|
170
203
|
useOnError, usePermissions,
|
|
171
204
|
getLogoutVersion, resetLogoutVersion,
|
|
205
|
+
captureAuthLiveScope as captureAuthSession,
|
|
172
206
|
} from './auth-hooks.svelte';
|
|
173
207
|
export type { AuthNotificationOptions } from './auth-hooks.svelte';
|
|
208
|
+
export { AuthQueryError, AuthErrorHandlingError } from './auth-query-contract';
|
|
209
|
+
export type { AuthErrorHandlingResult } from './auth-query-contract';
|
|
210
|
+
export { AuthMutationError } from './auth-mutation-contract';
|
|
211
|
+
export { useResourceContract } from './resource-binding.svelte';
|
|
212
|
+
export { PermissionHintsError } from './permission-hints-contract';
|
|
213
|
+
export type { PermissionHints } from './permission-hints-contract';
|
|
174
214
|
export {
|
|
175
215
|
useSubmitTask,
|
|
176
216
|
useTask,
|
|
177
217
|
useTaskList,
|
|
178
218
|
useTaskSubscription,
|
|
179
219
|
} from './task-hooks.svelte';
|
|
220
|
+
export type { UseSubmitTaskOptions, UseSubmitTaskMutateParams } from './task-hooks.svelte';
|
|
180
221
|
export { useParsed, resetGlobalPath, syncGlobalPath } from './useParsed.svelte';
|
|
181
|
-
export
|
|
222
|
+
export type { UseStepsFormOptions, UseStepsFormReturn } from './useStepsForm.svelte';
|
|
223
|
+
export { useStepsForm } from './useStepsForm.svelte';
|
|
182
224
|
export { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
|
|
183
225
|
export type { RouterNavigationResult, RouterProvider } from './router-provider';
|
|
184
226
|
export {
|
|
@@ -210,12 +252,19 @@ export {
|
|
|
210
252
|
createTypeBoxValidator,
|
|
211
253
|
} from './helpers';
|
|
212
254
|
export type { TypeBoxValidatorLike } from './helpers';
|
|
213
|
-
export {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
export type {
|
|
256
|
+
StrictUseListOptions as UseListOptions,
|
|
257
|
+
StrictUseOneOptions as UseOneOptions,
|
|
258
|
+
StrictUseManyOptions as UseManyOptions,
|
|
259
|
+
StrictUseInfiniteListOptions as UseInfiniteListOptions,
|
|
260
|
+
StrictUseSelectOptions as UseSelectOptions,
|
|
261
|
+
StrictUseDeleteManyOptions as UseDeleteManyOptions,
|
|
262
|
+
StrictUseDeleteManyMutateParams as UseDeleteManyMutateParams,
|
|
263
|
+
StrictUseCreateManyMutateParams as UseCreateManyMutateParams,
|
|
264
|
+
StrictUseUpdateManyMutateParams as UseUpdateManyMutateParams,
|
|
265
|
+
StrictUseFormOptions as UseFormOptions,
|
|
266
|
+
StrictUseFormReturn as UseFormReturn,
|
|
267
|
+
} from './strict-hooks.svelte';
|
|
219
268
|
export type {
|
|
220
269
|
TaskProvider,
|
|
221
270
|
TaskDateValue,
|
|
@@ -230,5 +279,3 @@ export type {
|
|
|
230
279
|
// HTTP fetch utilities
|
|
231
280
|
export { createFetchWithInterceptor, fetchWithInterceptor } from './http';
|
|
232
281
|
export type { FetchWithInterceptor, FetchWithInterceptorOptions } from './http';
|
|
233
|
-
export { createSchemaFormValidator } from './schema-form';
|
|
234
|
-
export type { SchemaFormIssue, SchemaFormOptions } from './schema-form';
|
|
@@ -208,8 +208,9 @@ function parseGraphQLSDL(sdl: string): GraphQLSchemaBody {
|
|
|
208
208
|
const enumRegex = /enum\s+([A-Za-z0-9_]+)\s*\{([^}]+)\}/g;
|
|
209
209
|
let match: RegExpExecArray | null;
|
|
210
210
|
while ((match = enumRegex.exec(cleanSDL)) !== null) {
|
|
211
|
-
const enumName = match
|
|
212
|
-
|
|
211
|
+
const [, enumName, enumBody] = match;
|
|
212
|
+
if (enumName === undefined || enumBody === undefined) continue;
|
|
213
|
+
const rawValues = enumBody.split(/\s+/).filter(Boolean);
|
|
213
214
|
types.push({
|
|
214
215
|
kind: 'ENUM',
|
|
215
216
|
name: enumName,
|
|
@@ -220,8 +221,8 @@ function parseGraphQLSDL(sdl: string): GraphQLSchemaBody {
|
|
|
220
221
|
// Extract types: type Post { id: ID! title: String ... }
|
|
221
222
|
const typeRegex = /type\s+([A-Za-z0-9_]+)\s*(?:implements\s+[A-Za-z0-9_&,\s]+)?\s*\{([^}]+)\}/g;
|
|
222
223
|
while ((match = typeRegex.exec(cleanSDL)) !== null) {
|
|
223
|
-
const typeName = match
|
|
224
|
-
|
|
224
|
+
const [, typeName, fieldsBody] = match;
|
|
225
|
+
if (typeName === undefined || fieldsBody === undefined) continue;
|
|
225
226
|
const fieldLines = fieldsBody.split('\n').map(l => l.trim()).filter(Boolean);
|
|
226
227
|
const fields: GraphQLFieldDescriptor[] = [];
|
|
227
228
|
|
|
@@ -230,8 +231,8 @@ function parseGraphQLSDL(sdl: string): GraphQLSchemaBody {
|
|
|
230
231
|
const fieldMatch = /^([A-Za-z0-9_]+)(?:\([^)]*\))?\s*:\s*([[\]A-Za-z0-9_!]+)/.exec(line);
|
|
231
232
|
if (!fieldMatch) continue;
|
|
232
233
|
|
|
233
|
-
const fName = fieldMatch
|
|
234
|
-
|
|
234
|
+
const [, fName, rawType] = fieldMatch;
|
|
235
|
+
if (fName === undefined || rawType === undefined) continue;
|
|
235
236
|
|
|
236
237
|
const isNonNullable = rawType.endsWith('!');
|
|
237
238
|
const cleanType = isNonNullable ? rawType.slice(0, -1) : rawType;
|