@svadmin/core 0.49.1 → 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/README.md +32 -0
- 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 -774
- 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 -28
- 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 -286
- 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 -103
- 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/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;
|
|
@@ -217,90 +224,35 @@ export interface CustomResult<TData = unknown> {
|
|
|
217
224
|
|
|
218
225
|
export interface DataProvider {
|
|
219
226
|
// Required methods
|
|
220
|
-
getList:
|
|
221
|
-
getOne:
|
|
222
|
-
create:
|
|
223
|
-
update:
|
|
224
|
-
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>;
|
|
225
232
|
getApiUrl: () => string;
|
|
226
233
|
|
|
227
234
|
// Optional bulk methods
|
|
228
|
-
getMany?:
|
|
229
|
-
createMany?:
|
|
230
|
-
updateMany?:
|
|
231
|
-
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>;
|
|
232
239
|
|
|
233
240
|
// Optional custom method
|
|
234
|
-
custom?:
|
|
241
|
+
custom?: (params: CustomParams) => Promise<CustomResult>;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
// ─── TaskProvider ─────────────────────────────────────────────
|
|
238
245
|
|
|
239
|
-
export type TaskDateValue = string | Date | null;
|
|
240
|
-
export type TaskMessageValue = string | null;
|
|
241
|
-
|
|
242
|
-
export interface TaskRecord extends BaseRecord {
|
|
243
|
-
id: string;
|
|
244
|
-
name?: string;
|
|
245
|
-
title?: string;
|
|
246
|
-
status?: string;
|
|
247
|
-
progress?: number;
|
|
248
|
-
priority?: number;
|
|
249
|
-
queueName?: string;
|
|
250
|
-
queue_name?: string;
|
|
251
|
-
message?: TaskMessageValue;
|
|
252
|
-
payload?: unknown;
|
|
253
|
-
/**
|
|
254
|
-
* Task output payload. `result` is the canonical camelCase field used by
|
|
255
|
-
* svadmin UI helpers; `result_data` is accepted for Supabase/Postgres-style
|
|
256
|
-
* task mirrors. Consumers should prefer `result`, then fall back to
|
|
257
|
-
* `result_data`, matching `resolveTaskResult`.
|
|
258
|
-
*/
|
|
259
|
-
result?: unknown;
|
|
260
|
-
result_data?: unknown;
|
|
261
|
-
/**
|
|
262
|
-
* Task lifecycle timestamps. Supabase/Postgres mirrors often expose nullable
|
|
263
|
-
* columns for lifecycle phases that have not happened yet, and svadmin UI
|
|
264
|
-
* helpers intentionally skip nullish values with `??` fallback ordering.
|
|
265
|
-
*/
|
|
266
|
-
createdAt?: TaskDateValue;
|
|
267
|
-
created_at?: TaskDateValue;
|
|
268
|
-
updatedAt?: TaskDateValue;
|
|
269
|
-
updated_at?: TaskDateValue;
|
|
270
|
-
startedAt?: TaskDateValue;
|
|
271
|
-
started_at?: TaskDateValue;
|
|
272
|
-
finishedAt?: TaskDateValue;
|
|
273
|
-
finished_at?: TaskDateValue;
|
|
274
|
-
cancelledAt?: TaskDateValue;
|
|
275
|
-
cancelled_at?: TaskDateValue;
|
|
276
|
-
/**
|
|
277
|
-
* Task failure payload/message. `error` carries structured error details
|
|
278
|
-
* when available; `errorMessage` and `error_message` are string fallbacks for
|
|
279
|
-
* camelCase and snake_case providers. Consumers should prefer `error`, then
|
|
280
|
-
* `errorMessage`, then `error_message`, matching `resolveTaskError`.
|
|
281
|
-
*/
|
|
282
|
-
error?: unknown;
|
|
283
|
-
errorMessage?: TaskMessageValue;
|
|
284
|
-
error_message?: TaskMessageValue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export interface SubmitTaskOptions {
|
|
288
|
-
body?: Record<string, unknown>;
|
|
289
|
-
idempotencyKey?: string;
|
|
290
|
-
headers?: Record<string, string>;
|
|
291
|
-
meta?: Record<string, unknown>;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
246
|
export interface TaskSubscription {
|
|
295
247
|
unsubscribe(): void;
|
|
296
248
|
}
|
|
297
249
|
|
|
298
250
|
export interface TaskHandle<TTask extends TaskRecord = TaskRecord> {
|
|
299
|
-
id
|
|
251
|
+
id: string;
|
|
300
252
|
wait(): Promise<TTask>;
|
|
301
|
-
subscribe?(callback: (task: TTask) => void): TaskSubscription | (() => void)
|
|
302
|
-
cancel?(): Promise<
|
|
303
|
-
retry?(): Promise<
|
|
253
|
+
subscribe?(callback: (task: TTask) => void, onError?: (error: TaskError) => void): TaskSubscription | (() => void);
|
|
254
|
+
cancel?(): Promise<TTask>;
|
|
255
|
+
retry?(): Promise<TTask>;
|
|
304
256
|
}
|
|
305
257
|
|
|
306
258
|
export interface TaskListResult<TTask extends TaskRecord = TaskRecord> {
|
|
@@ -313,34 +265,13 @@ export interface TaskProvider<TTask extends TaskRecord = TaskRecord> {
|
|
|
313
265
|
get(taskId: string): Promise<TTask>;
|
|
314
266
|
list?(params?: Record<string, unknown>): Promise<TaskListResult<TTask>>;
|
|
315
267
|
listDlq?(params?: Record<string, unknown>): Promise<TaskListResult<TTask>>;
|
|
316
|
-
cancel?(taskId: string): Promise<
|
|
317
|
-
retry?(taskId: string): Promise<
|
|
318
|
-
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);
|
|
319
271
|
}
|
|
320
272
|
|
|
321
273
|
// ─── AuthProvider ─────────────────────────────────────────────
|
|
322
274
|
|
|
323
|
-
export interface Identity {
|
|
324
|
-
id?: string;
|
|
325
|
-
name?: string;
|
|
326
|
-
email?: string;
|
|
327
|
-
avatar?: string;
|
|
328
|
-
[key: string]: unknown;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export interface AuthActionResult {
|
|
332
|
-
success: boolean;
|
|
333
|
-
redirectTo?: string;
|
|
334
|
-
error?: { message: string; name?: string };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export interface CheckResult {
|
|
338
|
-
authenticated: boolean;
|
|
339
|
-
redirectTo?: string;
|
|
340
|
-
error?: { message: string; name?: string };
|
|
341
|
-
logout?: boolean;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
275
|
export interface Role {
|
|
345
276
|
id: string;
|
|
346
277
|
name: string;
|
|
@@ -374,7 +305,7 @@ export interface AuthProvider {
|
|
|
374
305
|
updateRolePermissions?: (roleId: string, permissions: Record<string, string[]>) => Promise<AuthActionResult>;
|
|
375
306
|
/** Get recent audit logs */
|
|
376
307
|
getAuditLogs?: (params?: { page?: number; pageSize?: number }) => Promise<{ data: AuditLog[]; total: number }>;
|
|
377
|
-
onError?: (error: unknown) => Promise<
|
|
308
|
+
onError?: (error: unknown) => Promise<AuthErrorResult>;
|
|
378
309
|
}
|
|
379
310
|
|
|
380
311
|
export interface AuditLog {
|
|
@@ -401,21 +332,21 @@ export type MutationMode = 'pessimistic' | 'optimistic' | 'undoable';
|
|
|
401
332
|
|
|
402
333
|
// ─── ResourceDefinition ───────────────────────────────────────
|
|
403
334
|
|
|
404
|
-
/**
|
|
335
|
+
/** Declares a resource transport protocol; the corresponding provider owns URL and serialization behavior. */
|
|
405
336
|
export interface ResourceTransportConfig {
|
|
406
337
|
readonly type: string;
|
|
407
338
|
readonly endpoint?: string;
|
|
408
339
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
409
340
|
}
|
|
410
341
|
|
|
411
|
-
/**
|
|
342
|
+
/** Declares a resource backend adapter without binding Core to a concrete runtime implementation. */
|
|
412
343
|
export interface ResourceAdapterConfig {
|
|
413
344
|
readonly name: string;
|
|
414
345
|
readonly version?: string;
|
|
415
346
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
416
347
|
}
|
|
417
348
|
|
|
418
|
-
/**
|
|
349
|
+
/** Per-resource provider selection and declarative transport/adapter metadata. */
|
|
419
350
|
export interface ResourceProviderConfig {
|
|
420
351
|
readonly dataProviderName?: string;
|
|
421
352
|
readonly transport?: string | ResourceTransportConfig;
|
|
@@ -425,6 +356,8 @@ export interface ResourceProviderConfig {
|
|
|
425
356
|
|
|
426
357
|
export interface ResourceDefinition {
|
|
427
358
|
name: string;
|
|
359
|
+
/** Runtime contract required by schema-bound metadata-driven data components. */
|
|
360
|
+
contract?: ResourceContract;
|
|
428
361
|
/** Unique identifier — use when multiple resources share the same `name` but target different DataProviders */
|
|
429
362
|
identifier?: string;
|
|
430
363
|
label: string;
|
|
@@ -442,9 +375,9 @@ export interface ResourceDefinition {
|
|
|
442
375
|
menuOrder?: number;
|
|
443
376
|
/** Navigation group name — resources with the same group are displayed in a collapsible section */
|
|
444
377
|
group?: string;
|
|
445
|
-
/** DataProvider
|
|
378
|
+
/** DataProvider selection and declarative transport/adapter metadata. */
|
|
446
379
|
provider?: ResourceProviderConfig;
|
|
447
|
-
/** @deprecated
|
|
380
|
+
/** @deprecated New code should select the DataProvider through `provider.dataProviderName`. */
|
|
448
381
|
meta?: Record<string, unknown> & { dataProviderName?: string; parent?: string };
|
|
449
382
|
}
|
|
450
383
|
|
|
@@ -513,7 +446,8 @@ export interface FieldDefinition {
|
|
|
513
446
|
|
|
514
447
|
/**
|
|
515
448
|
* Extend this interface via declaration merging to register resource types.
|
|
516
|
-
*
|
|
449
|
+
* Query and mutation hooks infer data from explicit resource names.
|
|
450
|
+
* Extend ResourceInputMap as well to infer mutation payloads.
|
|
517
451
|
*
|
|
518
452
|
* @example
|
|
519
453
|
* ```ts
|
|
@@ -526,12 +460,24 @@ export interface FieldDefinition {
|
|
|
526
460
|
*
|
|
527
461
|
* // Now hooks auto-infer:
|
|
528
462
|
* const list = useList({ resource: 'users' })
|
|
529
|
-
* // list.data
|
|
463
|
+
* // list.data?.data -> { id: string; name: string; email: string }[] | undefined
|
|
530
464
|
* ```
|
|
531
465
|
*/
|
|
532
466
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
533
467
|
export interface ResourceTypeMap {}
|
|
534
468
|
|
|
469
|
+
/** Extend alongside ResourceTypeMap to register create/update/delete payloads. */
|
|
470
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
471
|
+
export interface ResourceInputMap {}
|
|
472
|
+
|
|
473
|
+
export type ResourceInputOperation = 'create' | 'update' | 'delete';
|
|
474
|
+
|
|
475
|
+
/** Unregistered inputs retain the legacy dynamic payload contract. */
|
|
476
|
+
export type InferResourceInput<R extends string, O extends ResourceInputOperation> =
|
|
477
|
+
R extends keyof ResourceInputMap
|
|
478
|
+
? O extends keyof ResourceInputMap[R] ? ResourceInputMap[R][O] : never
|
|
479
|
+
: O extends 'delete' ? Record<string, unknown> | undefined : Record<string, unknown>;
|
|
480
|
+
|
|
535
481
|
/** When ResourceTypeMap is empty → string; otherwise → registered keys */
|
|
536
482
|
export type KnownResources = keyof ResourceTypeMap extends never
|
|
537
483
|
? string
|
|
@@ -541,3 +487,9 @@ export type KnownResources = keyof ResourceTypeMap extends never
|
|
|
541
487
|
export type InferData<R extends string> = R extends keyof ResourceTypeMap
|
|
542
488
|
? ResourceTypeMap[R]
|
|
543
489
|
: Record<string, unknown>
|
|
490
|
+
|
|
491
|
+
// Map interface models without adding an index signature; distribute over unions.
|
|
492
|
+
type RecordShape<T> = T extends object ? { [K in keyof T]: T[K] } : never;
|
|
493
|
+
|
|
494
|
+
/** @internal Normalized record shape for resource-aware hook signatures. */
|
|
495
|
+
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
|
+
}
|