@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
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
2
|
-
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import { captureAdminContext } from './context.svelte';
|
|
4
|
-
import { dataQueryMatches } from './query-keys';
|
|
5
|
-
import type { QueryMatcher } from './query-keys';
|
|
6
|
-
import type { AdminContextAccessor } from './context.svelte';
|
|
7
|
-
import { useParsed } from './useParsed.svelte';
|
|
8
|
-
import { auditWithProvider } from './audit';
|
|
9
|
-
import { UndoError } from './types';
|
|
10
|
-
import type { BaseRecord, HttpError, KnownResources } from './types';
|
|
11
|
-
import { checkError, createOvertimeTracker, fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
|
12
|
-
import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
|
|
13
|
-
import { toast } from './toast.svelte';
|
|
14
|
-
|
|
15
|
-
// ─── Helper: publish live events after mutations ────────────────
|
|
16
|
-
|
|
17
|
-
export function publishLiveEvent(
|
|
18
|
-
resource: string,
|
|
19
|
-
type: 'created' | 'updated' | 'deleted',
|
|
20
|
-
ids?: (string | number)[],
|
|
21
|
-
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
22
|
-
) {
|
|
23
|
-
try {
|
|
24
|
-
const liveProvider = adminContext.liveProvider;
|
|
25
|
-
if (liveProvider?.publish) {
|
|
26
|
-
liveProvider.publish({
|
|
27
|
-
type: type === 'created' ? 'INSERT' : type === 'updated' ? 'UPDATE' : 'DELETE',
|
|
28
|
-
resource,
|
|
29
|
-
payload: { ids },
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
} catch { /* LiveProvider not configured — skip silently */ }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Invalidate queries by scope. Respects per-mutation `invalidates` override.
|
|
37
|
-
* Default invalidation scopes per operation:
|
|
38
|
-
* - create: ['list', 'many']
|
|
39
|
-
* - update: ['list', 'many', 'detail']
|
|
40
|
-
* - delete: ['list', 'many']
|
|
41
|
-
*/
|
|
42
|
-
export interface InvalidateByScopesRequest {
|
|
43
|
-
queryClient: ReturnType<typeof useQueryClient>;
|
|
44
|
-
resource: string;
|
|
45
|
-
scopes?: string[] | false;
|
|
46
|
-
defaults: string[];
|
|
47
|
-
id?: string | number;
|
|
48
|
-
matcher: Pick<QueryMatcher, 'provider' | 'tenant'>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function invalidateByScopes(request: InvalidateByScopesRequest): void {
|
|
52
|
-
const { queryClient, resource, scopes, defaults, id, matcher } = request;
|
|
53
|
-
if (scopes === false) return;
|
|
54
|
-
const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
|
|
55
|
-
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
56
|
-
...matcher,
|
|
57
|
-
resource,
|
|
58
|
-
...fields,
|
|
59
|
-
});
|
|
60
|
-
for (const scope of effectiveScopes) {
|
|
61
|
-
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => ['list', 'infiniteList', 'select', 'selectDefaults'].some((action) => dataMatch(q.queryKey, { action })) });
|
|
62
|
-
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) });
|
|
63
|
-
else if ((scope === 'detail' || scope === 'one') && id != null) queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id }) });
|
|
64
|
-
else if (scope === 'detail' || scope === 'one') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey, { action: 'one' }) });
|
|
65
|
-
else if (scope === 'resourceAll') queryClient.invalidateQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function deepMerge(target: unknown, source: unknown): unknown {
|
|
70
|
-
if (!target || typeof target !== 'object') return source;
|
|
71
|
-
if (!source || typeof source !== 'object') return target;
|
|
72
|
-
if (Array.isArray(source) || source instanceof Date) return source;
|
|
73
|
-
const targetRecord = target as Record<string, unknown>;
|
|
74
|
-
const sourceRecord = source as Record<string, unknown>;
|
|
75
|
-
const result: Record<string, unknown> = { ...targetRecord };
|
|
76
|
-
for (const [key, value] of Object.entries(sourceRecord)) {
|
|
77
|
-
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
78
|
-
result[key] = deepMerge(targetRecord[key] ?? {}, value);
|
|
79
|
-
} else {
|
|
80
|
-
result[key] = value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─── useCreate ─────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
interface MutationContext {
|
|
89
|
-
_svadmin_ctx?: boolean;
|
|
90
|
-
userContext?: unknown;
|
|
91
|
-
previousQueries?: [readonly unknown[], unknown][];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface MutationCallbacks {
|
|
95
|
-
onMutate?: (variables: unknown) => unknown | Promise<unknown>;
|
|
96
|
-
onSuccess?: (data: unknown, variables: unknown, context: unknown) => void;
|
|
97
|
-
onError?: (error: unknown, variables: unknown, context: unknown) => void;
|
|
98
|
-
onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown) => void;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface UseCreateOptions {
|
|
102
|
-
resource?: KnownResources;
|
|
103
|
-
mutationOptions?: MutationCallbacks;
|
|
104
|
-
overtimeOptions?: OvertimeOptions;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface UseCreateMutateParams<TVariables> {
|
|
108
|
-
resource?: KnownResources;
|
|
109
|
-
variables: TVariables;
|
|
110
|
-
successNotification?: NotificationConfig;
|
|
111
|
-
errorNotification?: NotificationConfig;
|
|
112
|
-
invalidates?: string[] | false;
|
|
113
|
-
meta?: Record<string, unknown>;
|
|
114
|
-
dataProviderName?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseCreateOptions = {}) {
|
|
118
|
-
const adminContext = captureAdminContext();
|
|
119
|
-
const parsed = useParsed();
|
|
120
|
-
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
121
|
-
const adminOptions = getAdminOptions();
|
|
122
|
-
const queryClient = useQueryClient();
|
|
123
|
-
|
|
124
|
-
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
125
|
-
|
|
126
|
-
const mutation = createMutation<
|
|
127
|
-
{ data: TData },
|
|
128
|
-
TError,
|
|
129
|
-
UseCreateMutateParams<TVariables>
|
|
130
|
-
>(() => ({
|
|
131
|
-
...options.mutationOptions,
|
|
132
|
-
mutationFn: async (params) => {
|
|
133
|
-
const resName = params.resource ?? defaultResource;
|
|
134
|
-
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
135
|
-
return await provider.create<TData, TVariables>({
|
|
136
|
-
resource: resName,
|
|
137
|
-
variables: params.variables,
|
|
138
|
-
meta: params.meta,
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
onMutate: options.mutationOptions?.onMutate,
|
|
142
|
-
onSuccess: (data, params, context) => {
|
|
143
|
-
const resName = params.resource ?? defaultResource;
|
|
144
|
-
fireSuccessNotification({
|
|
145
|
-
config: params.successNotification,
|
|
146
|
-
defaultMessage: 'Created successfully',
|
|
147
|
-
data: data.data,
|
|
148
|
-
values: params.variables,
|
|
149
|
-
resource: resName,
|
|
150
|
-
provider: adminContext.notificationProvider,
|
|
151
|
-
});
|
|
152
|
-
const res = adminContext.getResource(resName);
|
|
153
|
-
const pk = res.primaryKey ?? 'id';
|
|
154
|
-
const newId = (data.data as Record<string, unknown>)[pk];
|
|
155
|
-
auditWithProvider(
|
|
156
|
-
{
|
|
157
|
-
action: 'create',
|
|
158
|
-
resource: resName,
|
|
159
|
-
recordId: String(newId),
|
|
160
|
-
meta: adminContext.getProviderMeta(resName),
|
|
161
|
-
},
|
|
162
|
-
adminContext.auditLogProvider,
|
|
163
|
-
);
|
|
164
|
-
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
|
|
165
|
-
// Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
|
|
166
|
-
invalidateByScopes({
|
|
167
|
-
queryClient,
|
|
168
|
-
resource: resName,
|
|
169
|
-
scopes: params.invalidates,
|
|
170
|
-
defaults: ['list', 'many'],
|
|
171
|
-
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
172
|
-
});
|
|
173
|
-
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
174
|
-
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, context);
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
onError: (error, params, context) => {
|
|
178
|
-
checkError(error, adminContext);
|
|
179
|
-
fireErrorNotification({
|
|
180
|
-
config: params.errorNotification,
|
|
181
|
-
defaultMessage: 'Create failed',
|
|
182
|
-
error,
|
|
183
|
-
resource: params.resource ?? defaultResource,
|
|
184
|
-
provider: adminContext.notificationProvider,
|
|
185
|
-
});
|
|
186
|
-
if (typeof options.mutationOptions?.onError === 'function') {
|
|
187
|
-
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
}));
|
|
191
|
-
|
|
192
|
-
return { mutation, get overtime() { return overtime; } };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─── useUpdate ─────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
export interface UseUpdateOptions {
|
|
198
|
-
resource?: KnownResources;
|
|
199
|
-
id?: string | number;
|
|
200
|
-
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
|
|
201
|
-
undoableTimeout?: number;
|
|
202
|
-
mutationOptions?: MutationCallbacks;
|
|
203
|
-
overtimeOptions?: OvertimeOptions;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface UseUpdateMutateParams<TVariables> {
|
|
207
|
-
resource?: KnownResources;
|
|
208
|
-
id?: string | number;
|
|
209
|
-
variables: TVariables;
|
|
210
|
-
successNotification?: NotificationConfig;
|
|
211
|
-
errorNotification?: NotificationConfig;
|
|
212
|
-
invalidates?: string[] | false;
|
|
213
|
-
meta?: Record<string, unknown>;
|
|
214
|
-
dataProviderName?: string;
|
|
215
|
-
onCancel?: () => void;
|
|
216
|
-
optimisticUpdateMap?: {
|
|
217
|
-
detail?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
218
|
-
list?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
219
|
-
many?: boolean | ((previous: unknown, variables: TVariables, id?: string | number) => unknown);
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseUpdateOptions = {}) {
|
|
224
|
-
const adminContext = captureAdminContext();
|
|
225
|
-
const parsed = useParsed();
|
|
226
|
-
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
227
|
-
const defaultId = options.id ?? parsed.id;
|
|
228
|
-
const adminOptions = getAdminOptions();
|
|
229
|
-
const queryClient = useQueryClient();
|
|
230
|
-
const mutationMode = options.mutationMode ?? adminOptions.mutationMode ?? 'pessimistic';
|
|
231
|
-
const undoableTimeout = options.undoableTimeout ?? adminOptions.undoableTimeout ?? 5000;
|
|
232
|
-
|
|
233
|
-
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
234
|
-
|
|
235
|
-
const mutation = createMutation<
|
|
236
|
-
{ data: TData },
|
|
237
|
-
TError,
|
|
238
|
-
UseUpdateMutateParams<TVariables>
|
|
239
|
-
>(() => ({
|
|
240
|
-
...options.mutationOptions,
|
|
241
|
-
mutationFn: async (params) => {
|
|
242
|
-
const resName = params.resource ?? defaultResource;
|
|
243
|
-
const targetId = params.id ?? defaultId;
|
|
244
|
-
if (targetId == null) throw new Error('useUpdate requires an id');
|
|
245
|
-
|
|
246
|
-
if (mutationMode === 'undoable') {
|
|
247
|
-
await new Promise<void>((resolve, reject) => {
|
|
248
|
-
toast.undoable('Action can be undone', undoableTimeout, () => {
|
|
249
|
-
params.onCancel?.();
|
|
250
|
-
reject(new UndoError());
|
|
251
|
-
}, resolve);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
256
|
-
return await provider.update<TData, TVariables>({
|
|
257
|
-
resource: resName,
|
|
258
|
-
id: targetId,
|
|
259
|
-
variables: params.variables,
|
|
260
|
-
meta: params.meta,
|
|
261
|
-
});
|
|
262
|
-
},
|
|
263
|
-
onMutate: async (params) => {
|
|
264
|
-
const userOnMutate = options.mutationOptions?.onMutate;
|
|
265
|
-
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
266
|
-
|
|
267
|
-
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
268
|
-
const resName = params.resource ?? defaultResource;
|
|
269
|
-
const targetId = params.id ?? defaultId;
|
|
270
|
-
const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
|
|
271
|
-
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
272
|
-
...matcher,
|
|
273
|
-
resource: resName,
|
|
274
|
-
...fields,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
278
|
-
|
|
279
|
-
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
|
|
280
|
-
|
|
281
|
-
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
282
|
-
const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
|
|
283
|
-
|
|
284
|
-
if (updater.detail !== false) {
|
|
285
|
-
const detailFn = updater.detail;
|
|
286
|
-
if (typeof detailFn === 'function') {
|
|
287
|
-
queryClient.setQueriesData(
|
|
288
|
-
{ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) },
|
|
289
|
-
(old: unknown) => detailFn(old, params.variables, targetId),
|
|
290
|
-
);
|
|
291
|
-
} else {
|
|
292
|
-
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'one', id: targetId }) }, (old: Record<string, unknown> | undefined) => old ? { ...old, data: deepMerge((old as Record<string, unknown>).data || {}, params.variables) } : old);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (updater.list !== false) {
|
|
297
|
-
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
|
|
298
|
-
if (typeof updater.list === 'function') return updater.list(old, params.variables, targetId);
|
|
299
|
-
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
300
|
-
const o = old as { data: Record<string, unknown>[] };
|
|
301
|
-
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? deepMerge(item, params.variables) : item) };
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (updater.many !== false) {
|
|
306
|
-
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
|
|
307
|
-
if (typeof updater.many === 'function') return updater.many(old, params.variables, targetId);
|
|
308
|
-
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
309
|
-
const o = old as { data: Record<string, unknown>[] };
|
|
310
|
-
return { ...o, data: o.data.map((item) => String(item[pk]) === String(targetId) ? deepMerge(item, params.variables) : item) };
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return { _svadmin_ctx: true, userContext, previousQueries };
|
|
315
|
-
},
|
|
316
|
-
onSuccess: (data, params, context) => {
|
|
317
|
-
const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
|
|
318
|
-
const resName = params.resource ?? defaultResource;
|
|
319
|
-
const targetId = params.id ?? defaultId;
|
|
320
|
-
fireSuccessNotification({
|
|
321
|
-
config: params.successNotification,
|
|
322
|
-
defaultMessage: 'Updated successfully',
|
|
323
|
-
data: data.data,
|
|
324
|
-
values: params.variables,
|
|
325
|
-
resource: resName,
|
|
326
|
-
provider: adminContext.notificationProvider,
|
|
327
|
-
});
|
|
328
|
-
auditWithProvider(
|
|
329
|
-
{
|
|
330
|
-
action: 'update',
|
|
331
|
-
resource: resName,
|
|
332
|
-
recordId: String(targetId),
|
|
333
|
-
meta: adminContext.getProviderMeta(resName),
|
|
334
|
-
},
|
|
335
|
-
adminContext.auditLogProvider,
|
|
336
|
-
);
|
|
337
|
-
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
|
|
338
|
-
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
339
|
-
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
340
|
-
}
|
|
341
|
-
},
|
|
342
|
-
onSettled: (_data, error, params) => {
|
|
343
|
-
if (error instanceof UndoError) return;
|
|
344
|
-
const resName = params.resource ?? defaultResource;
|
|
345
|
-
const targetId = params.id ?? defaultId;
|
|
346
|
-
invalidateByScopes({
|
|
347
|
-
queryClient,
|
|
348
|
-
resource: resName,
|
|
349
|
-
scopes: params.invalidates,
|
|
350
|
-
defaults: ['list', 'many', 'detail'],
|
|
351
|
-
id: targetId != null ? targetId : undefined,
|
|
352
|
-
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
353
|
-
});
|
|
354
|
-
if (typeof options.mutationOptions?.onSettled === 'function') {
|
|
355
|
-
(options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
onError: (error, params, context: unknown) => {
|
|
359
|
-
const ctx = context as MutationContext | undefined;
|
|
360
|
-
if (ctx?.previousQueries) {
|
|
361
|
-
for (const [queryKey, data] of ctx.previousQueries) {
|
|
362
|
-
queryClient.setQueryData(queryKey, data);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (error instanceof UndoError) return;
|
|
366
|
-
checkError(error, adminContext);
|
|
367
|
-
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
368
|
-
fireErrorNotification({
|
|
369
|
-
config: params.errorNotification,
|
|
370
|
-
defaultMessage: 'Update failed',
|
|
371
|
-
error,
|
|
372
|
-
resource: params.resource ?? defaultResource,
|
|
373
|
-
provider: adminContext.notificationProvider,
|
|
374
|
-
});
|
|
375
|
-
if (typeof options.mutationOptions?.onError === 'function') {
|
|
376
|
-
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
}));
|
|
380
|
-
|
|
381
|
-
return { mutation, get overtime() { return overtime; } };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// ─── useDelete ─────────────────────────────────────────────────
|
|
385
|
-
|
|
386
|
-
export interface UseDeleteOptions {
|
|
387
|
-
resource?: KnownResources;
|
|
388
|
-
id?: string | number;
|
|
389
|
-
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
|
|
390
|
-
undoableTimeout?: number;
|
|
391
|
-
mutationOptions?: MutationCallbacks;
|
|
392
|
-
overtimeOptions?: OvertimeOptions;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export interface UseDeleteMutateParams<TVariables> {
|
|
396
|
-
resource?: KnownResources;
|
|
397
|
-
id?: string | number;
|
|
398
|
-
variables?: TVariables;
|
|
399
|
-
successNotification?: NotificationConfig;
|
|
400
|
-
errorNotification?: NotificationConfig;
|
|
401
|
-
invalidates?: string[] | false;
|
|
402
|
-
meta?: Record<string, unknown>;
|
|
403
|
-
dataProviderName?: string;
|
|
404
|
-
onCancel?: () => void;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseDeleteOptions = {}) {
|
|
408
|
-
const adminContext = captureAdminContext();
|
|
409
|
-
const parsed = useParsed();
|
|
410
|
-
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
411
|
-
const defaultId = options.id ?? parsed.id;
|
|
412
|
-
const adminOptions = getAdminOptions();
|
|
413
|
-
const queryClient = useQueryClient();
|
|
414
|
-
const mutationMode = options.mutationMode ?? adminOptions.mutationMode ?? 'pessimistic';
|
|
415
|
-
const undoableTimeout = options.undoableTimeout ?? adminOptions.undoableTimeout ?? 5000;
|
|
416
|
-
|
|
417
|
-
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
418
|
-
|
|
419
|
-
const mutation = createMutation<
|
|
420
|
-
{ data: TData },
|
|
421
|
-
TError,
|
|
422
|
-
UseDeleteMutateParams<TVariables>
|
|
423
|
-
>(() => ({
|
|
424
|
-
...options.mutationOptions,
|
|
425
|
-
mutationFn: async (params) => {
|
|
426
|
-
const resName = params.resource ?? defaultResource;
|
|
427
|
-
const targetId = params.id ?? defaultId;
|
|
428
|
-
if (targetId == null) throw new Error('useDelete requires an id');
|
|
429
|
-
|
|
430
|
-
if (mutationMode === 'undoable') {
|
|
431
|
-
await new Promise<void>((resolve, reject) => {
|
|
432
|
-
toast.undoable('Action can be undone', undoableTimeout, () => {
|
|
433
|
-
params.onCancel?.();
|
|
434
|
-
reject(new UndoError());
|
|
435
|
-
}, resolve);
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
440
|
-
return await provider.deleteOne<TData, TVariables>({
|
|
441
|
-
resource: resName,
|
|
442
|
-
id: targetId,
|
|
443
|
-
variables: params.variables,
|
|
444
|
-
meta: params.meta,
|
|
445
|
-
});
|
|
446
|
-
},
|
|
447
|
-
onMutate: async (params) => {
|
|
448
|
-
const userOnMutate = options.mutationOptions?.onMutate;
|
|
449
|
-
const userContext = typeof userOnMutate === 'function' ? await userOnMutate(params) : undefined;
|
|
450
|
-
|
|
451
|
-
if (mutationMode === 'pessimistic') return { _svadmin_ctx: true, userContext };
|
|
452
|
-
const resName = params.resource ?? defaultResource;
|
|
453
|
-
const targetId = params.id ?? defaultId;
|
|
454
|
-
const matcher = adminContext.queryKeyMatcher(resName, params.dataProviderName);
|
|
455
|
-
const dataMatch = (queryKey: readonly unknown[], fields: QueryMatcher = {}) => dataQueryMatches(queryKey, {
|
|
456
|
-
...matcher,
|
|
457
|
-
resource: resName,
|
|
458
|
-
...fields,
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
await queryClient.cancelQueries({ predicate: (q) => dataMatch(q.queryKey) });
|
|
462
|
-
|
|
463
|
-
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dataMatch(q.queryKey) });
|
|
464
|
-
|
|
465
|
-
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
466
|
-
|
|
467
|
-
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'list' }) }, (old: unknown) => {
|
|
468
|
-
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
469
|
-
const o = old as { data: Record<string, unknown>[]; total?: number };
|
|
470
|
-
const filtered = o.data.filter((item) => String(item[pk]) !== String(targetId));
|
|
471
|
-
return { ...o, data: filtered, total: (o.total ?? o.data.length) - (o.data.length - filtered.length) };
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
queryClient.setQueriesData({ predicate: (q) => dataMatch(q.queryKey, { action: 'many' }) }, (old: unknown) => {
|
|
475
|
-
if (!old || typeof old !== 'object' || !('data' in old)) return old;
|
|
476
|
-
const o = old as { data: Record<string, unknown>[] };
|
|
477
|
-
return { ...o, data: o.data.filter((item) => String(item[pk]) !== String(targetId)) };
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
return { _svadmin_ctx: true, userContext, previousQueries };
|
|
481
|
-
},
|
|
482
|
-
onSuccess: (data, params, context) => {
|
|
483
|
-
const extractedCtx = (context as MutationContext)?._svadmin_ctx ? (context as MutationContext).userContext : context;
|
|
484
|
-
const resName = params.resource ?? defaultResource;
|
|
485
|
-
const targetId = params.id ?? defaultId;
|
|
486
|
-
|
|
487
|
-
// Remove detail cache entry (refine pattern — no stale show page)
|
|
488
|
-
if (targetId != null) {
|
|
489
|
-
queryClient.removeQueries({
|
|
490
|
-
predicate: (q) => dataQueryMatches(q.queryKey, {
|
|
491
|
-
...adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
492
|
-
resource: resName,
|
|
493
|
-
action: 'one',
|
|
494
|
-
id: targetId,
|
|
495
|
-
}),
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
fireSuccessNotification({
|
|
500
|
-
config: params.successNotification,
|
|
501
|
-
defaultMessage: 'Deleted successfully',
|
|
502
|
-
data: data.data,
|
|
503
|
-
values: params.variables,
|
|
504
|
-
resource: resName,
|
|
505
|
-
provider: adminContext.notificationProvider,
|
|
506
|
-
});
|
|
507
|
-
auditWithProvider(
|
|
508
|
-
{
|
|
509
|
-
action: 'delete',
|
|
510
|
-
resource: resName,
|
|
511
|
-
recordId: String(targetId),
|
|
512
|
-
meta: adminContext.getProviderMeta(resName),
|
|
513
|
-
},
|
|
514
|
-
adminContext.auditLogProvider,
|
|
515
|
-
);
|
|
516
|
-
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
|
|
517
|
-
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
518
|
-
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
onSettled: (_data, error, params) => {
|
|
522
|
-
if (error instanceof UndoError) return;
|
|
523
|
-
const resName = params.resource ?? defaultResource;
|
|
524
|
-
const targetId = params.id ?? defaultId;
|
|
525
|
-
invalidateByScopes({
|
|
526
|
-
queryClient,
|
|
527
|
-
resource: resName,
|
|
528
|
-
scopes: params.invalidates,
|
|
529
|
-
defaults: ['list', 'many'],
|
|
530
|
-
id: targetId != null ? targetId : undefined,
|
|
531
|
-
matcher: adminContext.queryKeyMatcher(resName, params.dataProviderName),
|
|
532
|
-
});
|
|
533
|
-
if (typeof options.mutationOptions?.onSettled === 'function') {
|
|
534
|
-
(options.mutationOptions.onSettled as (...a: unknown[]) => unknown)(_data, error, params, undefined);
|
|
535
|
-
}
|
|
536
|
-
},
|
|
537
|
-
onError: (error, params, context: unknown) => {
|
|
538
|
-
const ctx = context as MutationContext | undefined;
|
|
539
|
-
if (ctx?.previousQueries) {
|
|
540
|
-
for (const [queryKey, data] of ctx.previousQueries) {
|
|
541
|
-
queryClient.setQueryData(queryKey, data);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (error instanceof UndoError) return;
|
|
545
|
-
checkError(error, adminContext);
|
|
546
|
-
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
547
|
-
fireErrorNotification({
|
|
548
|
-
config: params.errorNotification,
|
|
549
|
-
defaultMessage: 'Delete failed',
|
|
550
|
-
error,
|
|
551
|
-
resource: params.resource ?? defaultResource,
|
|
552
|
-
provider: adminContext.notificationProvider,
|
|
553
|
-
});
|
|
554
|
-
if (typeof options.mutationOptions?.onError === 'function') {
|
|
555
|
-
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, extractedCtx);
|
|
556
|
-
}
|
|
557
|
-
},
|
|
558
|
-
}));
|
|
559
|
-
|
|
560
|
-
return { mutation, get overtime() { return overtime; } };
|
|
561
|
-
}
|
package/src/schema-form.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { TSchema } from '@sinclair/typebox';
|
|
2
|
-
import { Value } from '@sinclair/typebox/value';
|
|
3
|
-
|
|
4
|
-
export interface SchemaFormIssue {
|
|
5
|
-
/** JSON Pointer from the schema validator; never includes submitted values. */
|
|
6
|
-
path: string;
|
|
7
|
-
type: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface SchemaFormOptions {
|
|
11
|
-
references?: TSchema[];
|
|
12
|
-
message?: (issue: SchemaFormIssue) => string;
|
|
13
|
-
field?: (pointer: string) => string;
|
|
14
|
-
formField?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Adapter for useForm.validate. It validates without coercing or clearing input. */
|
|
18
|
-
export function createSchemaFormValidator(schema: TSchema, options: SchemaFormOptions = {}) {
|
|
19
|
-
return (values: unknown): Record<string, string> | null => {
|
|
20
|
-
const errors: Record<string, string> = Object.create(null);
|
|
21
|
-
for (const error of Value.Errors(schema, options.references ?? [], values)) {
|
|
22
|
-
const field = options.field?.(error.path) ?? (error.path
|
|
23
|
-
? error.path.slice(1).split('/').map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~')).join('.')
|
|
24
|
-
: options.formField ?? '_form');
|
|
25
|
-
if (!Object.hasOwn(errors, field)) {
|
|
26
|
-
errors[field] = options.message?.({ path: error.path, type: error.type }) ?? 'Invalid value';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return Object.keys(errors).length ? errors : null;
|
|
30
|
-
};
|
|
31
|
-
}
|