@svadmin/core 0.50.0 → 0.53.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 +64 -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/inferencer.ts +9 -0
- 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 +220 -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 +103 -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 +190 -180
- 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 +68 -110
- 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
package/src/toast.svelte.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { definedOptions } from './defined-options';
|
|
2
|
+
export type ToastType='success'|'error'|'info'|'warning'|'undoable';
|
|
2
3
|
|
|
3
4
|
export interface ToastItem {
|
|
4
5
|
id: number;
|
|
@@ -10,69 +11,69 @@ export interface ToastItem {
|
|
|
10
11
|
onTimeout?: () => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
let nextId
|
|
14
|
-
const undoableTimers
|
|
14
|
+
let nextId=0;
|
|
15
|
+
const undoableTimers=new Map<number,ReturnType<typeof setTimeout>>();
|
|
15
16
|
|
|
16
|
-
let queue
|
|
17
|
+
let queue=$state<ToastItem[]>([]);
|
|
17
18
|
export function getToastQueue() { return queue; }
|
|
18
|
-
export function consumeToastQueue() { queue
|
|
19
|
+
export function consumeToastQueue() { queue=[]; }
|
|
19
20
|
|
|
20
|
-
let promiseQueue
|
|
21
|
+
let promiseQueue=$state<{ id: number; promise: Promise<unknown>; opts: { loading: string; success: string; error: string } }[]>([]);
|
|
21
22
|
export function getPromiseQueue() { return promiseQueue; }
|
|
22
|
-
export function consumePromiseQueue() { promiseQueue
|
|
23
|
+
export function consumePromiseQueue() { promiseQueue=[]; }
|
|
23
24
|
|
|
24
|
-
let toasts
|
|
25
|
+
let toasts=$state<ToastItem[]>([]);
|
|
25
26
|
export function getToasts(): ToastItem[] { return toasts; }
|
|
26
27
|
|
|
27
28
|
export function addToast(
|
|
28
|
-
type: ToastType,
|
|
29
|
-
message: string,
|
|
30
|
-
duration
|
|
31
|
-
options?: { key?: string,
|
|
29
|
+
type: ToastType,
|
|
30
|
+
message: string,
|
|
31
|
+
duration=3000,
|
|
32
|
+
options?: { key?: string,onUndo?: () => void,onTimeout?: () => void }
|
|
32
33
|
): void {
|
|
33
|
-
if
|
|
34
|
-
if
|
|
34
|
+
if(options?.key) {
|
|
35
|
+
if(queue.some(q => q.key===options.key)||toasts.some(t => t.key===options.key)) return;
|
|
35
36
|
}
|
|
36
|
-
if
|
|
37
|
-
queue.push({ id: nextId++,
|
|
37
|
+
if(type!=='undoable') {
|
|
38
|
+
queue.push({ id: nextId++,type,message,duration,...definedOptions({ key: options?.key }) });
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
|
-
const id
|
|
41
|
-
toasts
|
|
42
|
-
const timer
|
|
41
|
+
const id=nextId++;
|
|
42
|
+
toasts=[...toasts,{ id,type,message,duration,...options }];
|
|
43
|
+
const timer=setTimeout(() => {
|
|
43
44
|
undoableTimers.delete(id);
|
|
44
45
|
options?.onTimeout?.();
|
|
45
46
|
removeToast(id);
|
|
46
|
-
},
|
|
47
|
-
undoableTimers.set(id,
|
|
47
|
+
},duration);
|
|
48
|
+
undoableTimers.set(id,timer);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export function removeToast(id: number): void {
|
|
51
|
-
const timer
|
|
52
|
-
if
|
|
52
|
+
const timer=undoableTimers.get(id);
|
|
53
|
+
if(timer) {
|
|
53
54
|
clearTimeout(timer);
|
|
54
55
|
undoableTimers.delete(id);
|
|
55
56
|
}
|
|
56
|
-
toasts
|
|
57
|
+
toasts=toasts.filter(t => t.id!==id);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export function resetToast(): void {
|
|
60
|
-
for
|
|
61
|
+
for(const timer of undoableTimers.values()) clearTimeout(timer);
|
|
61
62
|
undoableTimers.clear();
|
|
62
|
-
queue
|
|
63
|
-
promiseQueue
|
|
64
|
-
toasts
|
|
63
|
+
queue=[];
|
|
64
|
+
promiseQueue=[];
|
|
65
|
+
toasts=[];
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
export const toast
|
|
68
|
-
success: (msg: string,
|
|
69
|
-
error: (msg: string,
|
|
70
|
-
info: (msg: string,
|
|
71
|
-
warning: (msg: string,
|
|
72
|
-
undoable: (msg: string,
|
|
73
|
-
addToast('undoable',
|
|
74
|
-
promise: <T>(promise: Promise<T>,
|
|
75
|
-
promiseQueue.push({ id: nextId++,
|
|
68
|
+
export const toast={
|
|
69
|
+
success: (msg: string,duration?: number,opts?: { key?: string }) => addToast('success',msg,duration,opts),
|
|
70
|
+
error: (msg: string,duration?: number,opts?: { key?: string }) => addToast('error',msg,duration??5000,opts),
|
|
71
|
+
info: (msg: string,duration?: number,opts?: { key?: string }) => addToast('info',msg,duration,opts),
|
|
72
|
+
warning: (msg: string,duration?: number,opts?: { key?: string }) => addToast('warning',msg,duration??4000,opts),
|
|
73
|
+
undoable: (msg: string,duration: number,onUndo: () => void,onTimeout: () => void) =>
|
|
74
|
+
addToast('undoable',msg,duration,{ onUndo,onTimeout }),
|
|
75
|
+
promise: <T>(promise: Promise<T>,opts: { loading: string; success: string; error: string }) => {
|
|
76
|
+
promiseQueue.push({ id: nextId++,promise,opts });
|
|
76
77
|
return promise;
|
|
77
78
|
},
|
|
78
79
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Core type definitions — DataProvider + AuthProvider + Providers
|
|
2
2
|
|
|
3
3
|
import type { Component } from 'svelte';
|
|
4
|
+
import type { TaskRecord, SubmitTaskOptions, TaskError } from './task-contract';
|
|
5
|
+
import type { Identity, CheckResult, AuthErrorResult } from './auth-query-contract';
|
|
6
|
+
export type { Identity, CheckResult } from './auth-query-contract';
|
|
7
|
+
import type { AuthActionResult } from './auth-mutation-contract';
|
|
8
|
+
import type { ResourceContract } from './resource-contract';
|
|
9
|
+
export type { AuthActionResult } from './auth-mutation-contract';
|
|
10
|
+
export type { TaskRecord, SubmitTaskOptions, TaskDateValue, TaskMessageValue } from './task-contract';
|
|
4
11
|
|
|
5
12
|
// ─── HttpError ─────────────────────────────────────────────────
|
|
6
13
|
|
|
@@ -15,8 +22,8 @@ export interface HttpErrorOptions {
|
|
|
15
22
|
|
|
16
23
|
export class HttpError extends Error {
|
|
17
24
|
statusCode: number;
|
|
18
|
-
errors
|
|
19
|
-
code
|
|
25
|
+
errors: ValidationErrors | undefined;
|
|
26
|
+
code: string | undefined;
|
|
20
27
|
details?: unknown;
|
|
21
28
|
body?: unknown;
|
|
22
29
|
override cause?: unknown;
|
|
@@ -103,8 +110,6 @@ export interface LogicalFilter {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
export interface GetListParams {
|
|
106
|
-
/** Query-owned cancellation signal. Never include it in a cache key or URL. */
|
|
107
|
-
signal?: AbortSignal;
|
|
108
113
|
resource: string;
|
|
109
114
|
pagination?: Pagination;
|
|
110
115
|
sorters?: Sort[];
|
|
@@ -119,7 +124,6 @@ export interface GetListResult<TData extends BaseRecord = BaseRecord> {
|
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
export interface GetOneParams {
|
|
122
|
-
signal?: AbortSignal;
|
|
123
127
|
resource: string;
|
|
124
128
|
id: string | number;
|
|
125
129
|
meta?: Record<string, unknown>;
|
|
@@ -130,7 +134,6 @@ export interface GetOneResult<TData extends BaseRecord = BaseRecord> {
|
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
export interface GetManyParams {
|
|
133
|
-
signal?: AbortSignal;
|
|
134
137
|
resource: string;
|
|
135
138
|
ids: (string | number)[];
|
|
136
139
|
meta?: Record<string, unknown>;
|
|
@@ -205,7 +208,6 @@ export interface DeleteManyResult<TData extends BaseRecord = BaseRecord> {
|
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
export interface CustomParams<TVariables = unknown> {
|
|
208
|
-
signal?: AbortSignal;
|
|
209
211
|
url: string;
|
|
210
212
|
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
211
213
|
payload?: TVariables;
|
|
@@ -222,90 +224,35 @@ export interface CustomResult<TData = unknown> {
|
|
|
222
224
|
|
|
223
225
|
export interface DataProvider {
|
|
224
226
|
// Required methods
|
|
225
|
-
getList:
|
|
226
|
-
getOne:
|
|
227
|
-
create:
|
|
228
|
-
update:
|
|
229
|
-
deleteOne:
|
|
227
|
+
getList: (params: GetListParams) => Promise<GetListResult>;
|
|
228
|
+
getOne: (params: GetOneParams) => Promise<GetOneResult>;
|
|
229
|
+
create: (params: CreateParams) => Promise<CreateResult>;
|
|
230
|
+
update: (params: UpdateParams) => Promise<UpdateResult>;
|
|
231
|
+
deleteOne: (params: DeleteParams) => Promise<DeleteResult>;
|
|
230
232
|
getApiUrl: () => string;
|
|
231
233
|
|
|
232
234
|
// Optional bulk methods
|
|
233
|
-
getMany?:
|
|
234
|
-
createMany?:
|
|
235
|
-
updateMany?:
|
|
236
|
-
deleteMany?:
|
|
235
|
+
getMany?: (params: GetManyParams) => Promise<GetManyResult>;
|
|
236
|
+
createMany?: (params: CreateManyParams) => Promise<CreateManyResult>;
|
|
237
|
+
updateMany?: (params: UpdateManyParams) => Promise<UpdateManyResult>;
|
|
238
|
+
deleteMany?: (params: DeleteManyParams) => Promise<DeleteManyResult>;
|
|
237
239
|
|
|
238
240
|
// Optional custom method
|
|
239
|
-
custom?:
|
|
241
|
+
custom?: (params: CustomParams) => Promise<CustomResult>;
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
// ─── TaskProvider ─────────────────────────────────────────────
|
|
243
245
|
|
|
244
|
-
export type TaskDateValue = string | Date | null;
|
|
245
|
-
export type TaskMessageValue = string | null;
|
|
246
|
-
|
|
247
|
-
export interface TaskRecord extends BaseRecord {
|
|
248
|
-
id: string;
|
|
249
|
-
name?: string;
|
|
250
|
-
title?: string;
|
|
251
|
-
status?: string;
|
|
252
|
-
progress?: number;
|
|
253
|
-
priority?: number;
|
|
254
|
-
queueName?: string;
|
|
255
|
-
queue_name?: string;
|
|
256
|
-
message?: TaskMessageValue;
|
|
257
|
-
payload?: unknown;
|
|
258
|
-
/**
|
|
259
|
-
* Task output payload. `result` is the canonical camelCase field used by
|
|
260
|
-
* svadmin UI helpers; `result_data` is accepted for Supabase/Postgres-style
|
|
261
|
-
* task mirrors. Consumers should prefer `result`, then fall back to
|
|
262
|
-
* `result_data`, matching `resolveTaskResult`.
|
|
263
|
-
*/
|
|
264
|
-
result?: unknown;
|
|
265
|
-
result_data?: unknown;
|
|
266
|
-
/**
|
|
267
|
-
* Task lifecycle timestamps. Supabase/Postgres mirrors often expose nullable
|
|
268
|
-
* columns for lifecycle phases that have not happened yet, and svadmin UI
|
|
269
|
-
* helpers intentionally skip nullish values with `??` fallback ordering.
|
|
270
|
-
*/
|
|
271
|
-
createdAt?: TaskDateValue;
|
|
272
|
-
created_at?: TaskDateValue;
|
|
273
|
-
updatedAt?: TaskDateValue;
|
|
274
|
-
updated_at?: TaskDateValue;
|
|
275
|
-
startedAt?: TaskDateValue;
|
|
276
|
-
started_at?: TaskDateValue;
|
|
277
|
-
finishedAt?: TaskDateValue;
|
|
278
|
-
finished_at?: TaskDateValue;
|
|
279
|
-
cancelledAt?: TaskDateValue;
|
|
280
|
-
cancelled_at?: TaskDateValue;
|
|
281
|
-
/**
|
|
282
|
-
* Task failure payload/message. `error` carries structured error details
|
|
283
|
-
* when available; `errorMessage` and `error_message` are string fallbacks for
|
|
284
|
-
* camelCase and snake_case providers. Consumers should prefer `error`, then
|
|
285
|
-
* `errorMessage`, then `error_message`, matching `resolveTaskError`.
|
|
286
|
-
*/
|
|
287
|
-
error?: unknown;
|
|
288
|
-
errorMessage?: TaskMessageValue;
|
|
289
|
-
error_message?: TaskMessageValue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export interface SubmitTaskOptions {
|
|
293
|
-
body?: Record<string, unknown>;
|
|
294
|
-
idempotencyKey?: string;
|
|
295
|
-
headers?: Record<string, string>;
|
|
296
|
-
meta?: Record<string, unknown>;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
246
|
export interface TaskSubscription {
|
|
300
247
|
unsubscribe(): void;
|
|
301
248
|
}
|
|
302
249
|
|
|
303
250
|
export interface TaskHandle<TTask extends TaskRecord = TaskRecord> {
|
|
304
|
-
id
|
|
251
|
+
id: string;
|
|
305
252
|
wait(): Promise<TTask>;
|
|
306
|
-
subscribe?(callback: (task: TTask) => void): TaskSubscription | (() => void)
|
|
307
|
-
cancel?(): Promise<
|
|
308
|
-
retry?(): Promise<
|
|
253
|
+
subscribe?(callback: (task: TTask) => void, onError?: (error: TaskError) => void): TaskSubscription | (() => void);
|
|
254
|
+
cancel?(): Promise<TTask>;
|
|
255
|
+
retry?(): Promise<TTask>;
|
|
309
256
|
}
|
|
310
257
|
|
|
311
258
|
export interface TaskListResult<TTask extends TaskRecord = TaskRecord> {
|
|
@@ -318,34 +265,13 @@ export interface TaskProvider<TTask extends TaskRecord = TaskRecord> {
|
|
|
318
265
|
get(taskId: string): Promise<TTask>;
|
|
319
266
|
list?(params?: Record<string, unknown>): Promise<TaskListResult<TTask>>;
|
|
320
267
|
listDlq?(params?: Record<string, unknown>): Promise<TaskListResult<TTask>>;
|
|
321
|
-
cancel?(taskId: string): Promise<
|
|
322
|
-
retry?(taskId: string): Promise<
|
|
323
|
-
subscribe?(taskId: string, callback: (task: TTask) => void): TaskSubscription | (() => void)
|
|
268
|
+
cancel?(taskId: string): Promise<TTask>;
|
|
269
|
+
retry?(taskId: string): Promise<TTask>;
|
|
270
|
+
subscribe?(taskId: string, callback: (task: TTask) => void, onError?: (error: TaskError) => void): TaskSubscription | (() => void);
|
|
324
271
|
}
|
|
325
272
|
|
|
326
273
|
// ─── AuthProvider ─────────────────────────────────────────────
|
|
327
274
|
|
|
328
|
-
export interface Identity {
|
|
329
|
-
id?: string;
|
|
330
|
-
name?: string;
|
|
331
|
-
email?: string;
|
|
332
|
-
avatar?: string;
|
|
333
|
-
[key: string]: unknown;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export interface AuthActionResult {
|
|
337
|
-
success: boolean;
|
|
338
|
-
redirectTo?: string;
|
|
339
|
-
error?: { message: string; name?: string };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export interface CheckResult {
|
|
343
|
-
authenticated: boolean;
|
|
344
|
-
redirectTo?: string;
|
|
345
|
-
error?: { message: string; name?: string };
|
|
346
|
-
logout?: boolean;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
275
|
export interface Role {
|
|
350
276
|
id: string;
|
|
351
277
|
name: string;
|
|
@@ -379,7 +305,7 @@ export interface AuthProvider {
|
|
|
379
305
|
updateRolePermissions?: (roleId: string, permissions: Record<string, string[]>) => Promise<AuthActionResult>;
|
|
380
306
|
/** Get recent audit logs */
|
|
381
307
|
getAuditLogs?: (params?: { page?: number; pageSize?: number }) => Promise<{ data: AuditLog[]; total: number }>;
|
|
382
|
-
onError?: (error: unknown) => Promise<
|
|
308
|
+
onError?: (error: unknown) => Promise<AuthErrorResult>;
|
|
383
309
|
}
|
|
384
310
|
|
|
385
311
|
export interface AuditLog {
|
|
@@ -406,21 +332,21 @@ export type MutationMode = 'pessimistic' | 'optimistic' | 'undoable';
|
|
|
406
332
|
|
|
407
333
|
// ─── ResourceDefinition ───────────────────────────────────────
|
|
408
334
|
|
|
409
|
-
/**
|
|
335
|
+
/** Declares a resource transport protocol; the corresponding provider owns URL and serialization behavior. */
|
|
410
336
|
export interface ResourceTransportConfig {
|
|
411
337
|
readonly type: string;
|
|
412
338
|
readonly endpoint?: string;
|
|
413
339
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
414
340
|
}
|
|
415
341
|
|
|
416
|
-
/**
|
|
342
|
+
/** Declares a resource backend adapter without binding Core to a concrete runtime implementation. */
|
|
417
343
|
export interface ResourceAdapterConfig {
|
|
418
344
|
readonly name: string;
|
|
419
345
|
readonly version?: string;
|
|
420
346
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
421
347
|
}
|
|
422
348
|
|
|
423
|
-
/**
|
|
349
|
+
/** Per-resource provider selection and declarative transport/adapter metadata. */
|
|
424
350
|
export interface ResourceProviderConfig {
|
|
425
351
|
readonly dataProviderName?: string;
|
|
426
352
|
readonly transport?: string | ResourceTransportConfig;
|
|
@@ -430,6 +356,8 @@ export interface ResourceProviderConfig {
|
|
|
430
356
|
|
|
431
357
|
export interface ResourceDefinition {
|
|
432
358
|
name: string;
|
|
359
|
+
/** Runtime contract required by schema-bound metadata-driven data components. */
|
|
360
|
+
contract?: ResourceContract;
|
|
433
361
|
/** Unique identifier — use when multiple resources share the same `name` but target different DataProviders */
|
|
434
362
|
identifier?: string;
|
|
435
363
|
label: string;
|
|
@@ -447,9 +375,9 @@ export interface ResourceDefinition {
|
|
|
447
375
|
menuOrder?: number;
|
|
448
376
|
/** Navigation group name — resources with the same group are displayed in a collapsible section */
|
|
449
377
|
group?: string;
|
|
450
|
-
/** DataProvider
|
|
378
|
+
/** DataProvider selection and declarative transport/adapter metadata. */
|
|
451
379
|
provider?: ResourceProviderConfig;
|
|
452
|
-
/** @deprecated
|
|
380
|
+
/** @deprecated New code should select the DataProvider through `provider.dataProviderName`. */
|
|
453
381
|
meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
|
|
454
382
|
}
|
|
455
383
|
|
|
@@ -480,9 +408,9 @@ export interface MenuItem {
|
|
|
480
408
|
export interface FieldDefinition {
|
|
481
409
|
key: string;
|
|
482
410
|
label: string;
|
|
483
|
-
type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiselect' | 'tags'
|
|
411
|
+
type: 'text' | 'number' | 'boolean' | 'date' | 'time' | 'datetime' | 'daterange' | 'select' | 'multiselect' | 'tags'
|
|
484
412
|
| 'textarea' | 'richtext' | 'image' | 'images' | 'json' | 'relation' | 'color'
|
|
485
|
-
| 'url' | 'email' | 'phone' | 'currency' | 'file' | 'markdown' | 'password' | 'array'
|
|
413
|
+
| 'url' | 'email' | 'phone' | 'currency' | 'percent' | 'file' | 'markdown' | 'password' | 'array'
|
|
486
414
|
| 'tree-select' | 'treeselect' | 'cascader' | 'transfer' | 'rate' | 'rating' | 'avatar' | 'copy' | 'code';
|
|
487
415
|
required?: boolean;
|
|
488
416
|
searchable?: boolean;
|
|
@@ -502,6 +430,17 @@ export interface FieldDefinition {
|
|
|
502
430
|
changeOnSelect?: boolean;
|
|
503
431
|
separator?: string;
|
|
504
432
|
defaultValue?: unknown;
|
|
433
|
+
// Numeric input contract
|
|
434
|
+
min?: number;
|
|
435
|
+
max?: number;
|
|
436
|
+
step?: number;
|
|
437
|
+
precision?: number;
|
|
438
|
+
currency?: string;
|
|
439
|
+
locale?: string;
|
|
440
|
+
scale?: '100' | '1';
|
|
441
|
+
// File upload contract
|
|
442
|
+
accept?: string;
|
|
443
|
+
maxSize?: number;
|
|
505
444
|
// Relation support
|
|
506
445
|
resource?: string; // related resource name
|
|
507
446
|
optionLabel?: string; // field to use as label
|
|
@@ -518,7 +457,8 @@ export interface FieldDefinition {
|
|
|
518
457
|
|
|
519
458
|
/**
|
|
520
459
|
* Extend this interface via declaration merging to register resource types.
|
|
521
|
-
*
|
|
460
|
+
* Query and mutation hooks infer data from explicit resource names.
|
|
461
|
+
* Extend ResourceInputMap as well to infer mutation payloads.
|
|
522
462
|
*
|
|
523
463
|
* @example
|
|
524
464
|
* ```ts
|
|
@@ -531,12 +471,24 @@ export interface FieldDefinition {
|
|
|
531
471
|
*
|
|
532
472
|
* // Now hooks auto-infer:
|
|
533
473
|
* const list = useList({ resource: 'users' })
|
|
534
|
-
* // list.data
|
|
474
|
+
* // list.data?.data -> { id: string; name: string; email: string }[] | undefined
|
|
535
475
|
* ```
|
|
536
476
|
*/
|
|
537
477
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
538
478
|
export interface ResourceTypeMap {}
|
|
539
479
|
|
|
480
|
+
/** Extend alongside ResourceTypeMap to register create/update/delete payloads. */
|
|
481
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
482
|
+
export interface ResourceInputMap {}
|
|
483
|
+
|
|
484
|
+
export type ResourceInputOperation = 'create' | 'update' | 'delete';
|
|
485
|
+
|
|
486
|
+
/** Unregistered inputs retain the legacy dynamic payload contract. */
|
|
487
|
+
export type InferResourceInput<R extends string, O extends ResourceInputOperation> =
|
|
488
|
+
R extends keyof ResourceInputMap
|
|
489
|
+
? O extends keyof ResourceInputMap[R] ? ResourceInputMap[R][O] : never
|
|
490
|
+
: O extends 'delete' ? Record<string, unknown> | undefined : Record<string, unknown>;
|
|
491
|
+
|
|
540
492
|
/** When ResourceTypeMap is empty → string; otherwise → registered keys */
|
|
541
493
|
export type KnownResources = keyof ResourceTypeMap extends never
|
|
542
494
|
? string
|
|
@@ -546,3 +498,9 @@ export type KnownResources = keyof ResourceTypeMap extends never
|
|
|
546
498
|
export type InferData<R extends string> = R extends keyof ResourceTypeMap
|
|
547
499
|
? ResourceTypeMap[R]
|
|
548
500
|
: Record<string, unknown>
|
|
501
|
+
|
|
502
|
+
// Map interface models without adding an index signature; distribute over unions.
|
|
503
|
+
type RecordShape<T> = T extends object ? { [K in keyof T]: T[K] } : never;
|
|
504
|
+
|
|
505
|
+
/** @internal Normalized record shape for resource-aware hook signatures. */
|
|
506
|
+
export type ResourceRecord<R extends string> = RecordShape<InferData<R>>;
|
package/src/unsafe.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unchecked transport and metadata-driven UI APIs.
|
|
3
|
+
* Application CRUD should import contract-bound hooks from @svadmin/core.
|
|
4
|
+
*/
|
|
5
|
+
export * from './hooks.svelte';
|
|
6
|
+
export { TableState } from './table-state.svelte';
|
|
7
|
+
export type { TableStateOptions } from './table-state.svelte';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { snapshotPlainData } from './plain-data';
|
|
3
|
+
import { checkExact } from './schema-validation';
|
|
4
|
+
import { HttpError } from './types';
|
|
5
|
+
import { definedOptions } from './defined-options';
|
|
6
|
+
import { parseContractId, parseContractRecord, parseContractUpdateInput,
|
|
7
|
+
type ContractSchemas, type ContractUpdateInput, type ResourceContract } from './resource-contract';
|
|
8
|
+
import { decodeOneResult, rejectProviderResponse } from './record-decoder';
|
|
9
|
+
|
|
10
|
+
export interface ContractUpdateParams<S extends ContractSchemas> {
|
|
11
|
+
variables: ContractUpdateInput<S>;
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
13
|
+
dataProviderName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const envelope = Type.Object({
|
|
17
|
+
variables: Type.Unknown(),
|
|
18
|
+
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
19
|
+
dataProviderName: Type.Optional(Type.String({ minLength: 1 })),
|
|
20
|
+
}, { additionalProperties: false });
|
|
21
|
+
|
|
22
|
+
export function snapshotUpdateParams(value: unknown) {
|
|
23
|
+
try {
|
|
24
|
+
const snapshot = snapshotPlainData(value);
|
|
25
|
+
if (checkExact(envelope, snapshot)) return snapshot;
|
|
26
|
+
} catch {
|
|
27
|
+
// Reject accessors and serialization hooks without disclosing input.
|
|
28
|
+
}
|
|
29
|
+
throw new HttpError('Invalid update input', 422, undefined, {
|
|
30
|
+
code: 'INVALID_RESOURCE_INPUT', details: { phase: 'input', writeMayHaveSucceeded: false },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validateUpdateIdentity(variables: unknown, id: string | number): void {
|
|
35
|
+
if (typeof variables !== 'object' || variables === null || Array.isArray(variables)) return;
|
|
36
|
+
const property = Object.getOwnPropertyDescriptor(variables, 'id');
|
|
37
|
+
if (!property) return;
|
|
38
|
+
const candidate: unknown = property.value;
|
|
39
|
+
if (candidate === id) return;
|
|
40
|
+
throw new HttpError('Updates cannot change the record ID', 422, undefined, {
|
|
41
|
+
code: 'INVALID_RESOURCE_INPUT', details: { phase: 'input', writeMayHaveSucceeded: false },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Private schemas validate the input before establishing its public type. */
|
|
46
|
+
export function parseUpdateParams<S extends ContractSchemas>(
|
|
47
|
+
contract: ResourceContract<S>, id: string | number, value: unknown,
|
|
48
|
+
): ContractUpdateParams<S>;
|
|
49
|
+
export function parseUpdateParams(contract: ResourceContract, id: string | number, value: unknown): object {
|
|
50
|
+
const input = snapshotUpdateParams(value);
|
|
51
|
+
const identity = parseContractId(contract, id);
|
|
52
|
+
const variables = parseContractUpdateInput(contract, input.variables);
|
|
53
|
+
validateUpdateIdentity(variables, identity);
|
|
54
|
+
return { variables, ...definedOptions({ meta: input.meta, dataProviderName: input.dataProviderName }) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function decodeUpdateReceipt<S extends ContractSchemas>(
|
|
58
|
+
contract: ResourceContract<S>, id: string | number, response: unknown,
|
|
59
|
+
) {
|
|
60
|
+
try {
|
|
61
|
+
const result = decodeOneResult(snapshotPlainData(response), value => parseContractRecord(contract, value), true);
|
|
62
|
+
if (parseContractId(contract, result.data.id) !== id) return rejectProviderResponse(true);
|
|
63
|
+
return result;
|
|
64
|
+
} catch { return rejectProviderResponse(true); }
|
|
65
|
+
}
|