@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.
Files changed (97) hide show
  1. package/package.json +21 -4
  2. package/src/access-control-contract.ts +250 -0
  3. package/src/adapters/casbin.ts +20 -14
  4. package/src/adapters/casl.ts +22 -19
  5. package/src/admin-tool.ts +7 -7
  6. package/src/audit-contract.ts +105 -0
  7. package/src/audit.ts +88 -128
  8. package/src/auth-hooks.svelte.ts +593 -261
  9. package/src/auth-mutation-contract.ts +143 -0
  10. package/src/auth-query-contract.ts +97 -0
  11. package/src/captured-mutation.svelte.ts +45 -0
  12. package/src/chatProvider.svelte.ts +1 -1
  13. package/src/command-contract.ts +195 -0
  14. package/src/context.svelte.ts +217 -205
  15. package/src/contract-context.svelte.ts +37 -0
  16. package/src/contract-form.svelte.ts +566 -0
  17. package/src/create-contract.ts +56 -0
  18. package/src/create-hooks.svelte.ts +347 -0
  19. package/src/create-many-contract.ts +90 -0
  20. package/src/create-many-hooks.svelte.ts +396 -0
  21. package/src/data-transfer.svelte.ts +1 -215
  22. package/src/defined-options.ts +50 -0
  23. package/src/delete-contract.ts +50 -0
  24. package/src/delete-hooks.svelte.ts +403 -0
  25. package/src/delete-many-contract.ts +60 -0
  26. package/src/delete-many-hooks.svelte.ts +402 -0
  27. package/src/enterprise.ts +4 -4
  28. package/src/export-format.ts +16 -14
  29. package/src/export-hooks.svelte.ts +191 -0
  30. package/src/feature-gate-contract.ts +53 -0
  31. package/src/hook-utils.svelte.ts +53 -105
  32. package/src/hooks.svelte.ts +19 -776
  33. package/src/http.ts +88 -87
  34. package/src/i18n.svelte.ts +56 -2
  35. package/src/import-contract.ts +106 -0
  36. package/src/import-hooks.svelte.ts +310 -0
  37. package/src/index.ts +77 -30
  38. package/src/inferencer-graphql.ts +7 -6
  39. package/src/infinite-query.svelte.ts +228 -0
  40. package/src/invalidation-contract.ts +26 -0
  41. package/src/live-sse.ts +91 -127
  42. package/src/live-subscription.svelte.ts +130 -0
  43. package/src/live-transport.ts +127 -0
  44. package/src/live-websocket.ts +85 -118
  45. package/src/live.svelte.ts +66 -68
  46. package/src/notification.svelte.ts +17 -15
  47. package/src/permission-catalog.ts +121 -0
  48. package/src/permission-hints-contract.ts +48 -0
  49. package/src/permissions.svelte.ts +112 -59
  50. package/src/plain-data.ts +61 -0
  51. package/src/provider-bundle.ts +85 -79
  52. package/src/provider-response.ts +35 -0
  53. package/src/query-hooks.svelte.ts +241 -288
  54. package/src/query-invalidation.ts +172 -0
  55. package/src/query-keys.ts +221 -110
  56. package/src/query-session.svelte.ts +216 -0
  57. package/src/query-snapshot.ts +144 -0
  58. package/src/reactive-projection.ts +53 -0
  59. package/src/record-decoder.ts +63 -0
  60. package/src/resource-binding.svelte.ts +19 -0
  61. package/src/resource-contract.ts +595 -0
  62. package/src/resource-mutation-types.ts +30 -0
  63. package/src/resource-schemas.ts +371 -0
  64. package/src/resource-transport-config.ts +156 -0
  65. package/src/route-parsing.ts +11 -11
  66. package/src/router-provider.ts +64 -59
  67. package/src/router.ts +8 -3
  68. package/src/routing-hooks.svelte.ts +90 -62
  69. package/src/schema-validation.ts +92 -0
  70. package/src/schema.ts +13 -0
  71. package/src/select-options.ts +95 -0
  72. package/src/select-query.svelte.ts +191 -0
  73. package/src/session-query.svelte.ts +88 -0
  74. package/src/strict-command-hooks.svelte.ts +333 -0
  75. package/src/strict-hooks.svelte.ts +198 -0
  76. package/src/table-hooks.svelte.ts +180 -174
  77. package/src/table-state.svelte.ts +117 -117
  78. package/src/task-contract.ts +87 -0
  79. package/src/task-hooks.svelte.ts +193 -149
  80. package/src/task-provider.ts +115 -0
  81. package/src/task-subscription.ts +132 -0
  82. package/src/theme.svelte.ts +155 -154
  83. package/src/toast.svelte.ts +38 -37
  84. package/src/types.ts +55 -108
  85. package/src/unsafe.ts +7 -0
  86. package/src/update-contract.ts +65 -0
  87. package/src/update-hooks.svelte.ts +347 -0
  88. package/src/update-many-contract.ts +79 -0
  89. package/src/update-many-hooks.svelte.ts +373 -0
  90. package/src/url-sync.ts +11 -10
  91. package/src/useCan.ts +96 -31
  92. package/src/useParsed.svelte.ts +18 -14
  93. package/src/useStepsForm.svelte.ts +123 -63
  94. package/src/utility-hooks.svelte.ts +101 -158
  95. package/src/form-hooks.svelte.ts +0 -889
  96. package/src/mutation-hooks.svelte.ts +0 -561
  97. package/src/schema-form.ts +0 -31
@@ -1,4 +1,5 @@
1
- export type ToastType = 'success' | 'error' | 'info' | 'warning' | 'undoable';
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 = 0;
14
- const undoableTimers = new Map<number, ReturnType<typeof setTimeout>>();
14
+ let nextId=0;
15
+ const undoableTimers=new Map<number,ReturnType<typeof setTimeout>>();
15
16
 
16
- let queue = $state<ToastItem[]>([]);
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 = $state<{ id: number; promise: Promise<unknown>; opts: { loading: string; success: string; error: string } }[]>([]);
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 = $state<ToastItem[]>([]);
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 = 3000,
31
- options?: { key?: string, onUndo?: () => void, onTimeout?: () => void }
29
+ type: ToastType,
30
+ message: string,
31
+ duration=3000,
32
+ options?: { key?: string,onUndo?: () => void,onTimeout?: () => void }
32
33
  ): void {
33
- if (options?.key) {
34
- if (queue.some(q => q.key === options.key) || toasts.some(t => t.key === options.key)) return;
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 (type !== 'undoable') {
37
- queue.push({ id: nextId++, type, message, duration, key: options?.key });
37
+ if(type!=='undoable') {
38
+ queue.push({ id: nextId++,type,message,duration,...definedOptions({ key: options?.key }) });
38
39
  return;
39
40
  }
40
- const id = nextId++;
41
- toasts = [...toasts, { id, type, message, duration, ...options }];
42
- const timer = setTimeout(() => {
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
- }, duration);
47
- undoableTimers.set(id, timer);
47
+ },duration);
48
+ undoableTimers.set(id,timer);
48
49
  }
49
50
 
50
51
  export function removeToast(id: number): void {
51
- const timer = undoableTimers.get(id);
52
- if (timer) {
52
+ const timer=undoableTimers.get(id);
53
+ if(timer) {
53
54
  clearTimeout(timer);
54
55
  undoableTimers.delete(id);
55
56
  }
56
- toasts = toasts.filter(t => t.id !== id);
57
+ toasts=toasts.filter(t => t.id!==id);
57
58
  }
58
59
 
59
60
  export function resetToast(): void {
60
- for (const timer of undoableTimers.values()) clearTimeout(timer);
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, duration?: number, opts?: { key?: string }) => addToast('success', msg, duration, opts),
69
- error: (msg: string, duration?: number, opts?: { key?: string }) => addToast('error', msg, duration ?? 5000, opts),
70
- info: (msg: string, duration?: number, opts?: { key?: string }) => addToast('info', msg, duration, opts),
71
- warning: (msg: string, duration?: number, opts?: { key?: string }) => addToast('warning', msg, duration ?? 4000, opts),
72
- undoable: (msg: string, duration: number, onUndo: () => void, onTimeout: () => void) =>
73
- addToast('undoable', msg, duration, { onUndo, onTimeout }),
74
- promise: <T>(promise: Promise<T>, opts: { loading: string; success: string; error: string }) => {
75
- promiseQueue.push({ id: nextId++, promise, opts });
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?: ValidationErrors;
19
- code?: string;
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: <TData extends BaseRecord = BaseRecord>(params: GetListParams) => Promise<GetListResult<TData>>;
226
- getOne: <TData extends BaseRecord = BaseRecord>(params: GetOneParams) => Promise<GetOneResult<TData>>;
227
- create: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>) => Promise<CreateResult<TData>>;
228
- update: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>) => Promise<UpdateResult<TData>>;
229
- deleteOne: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteParams<TVariables>) => Promise<DeleteResult<TData>>;
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?: <TData extends BaseRecord = BaseRecord>(params: GetManyParams) => Promise<GetManyResult<TData>>;
234
- createMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateManyParams<TVariables>) => Promise<CreateManyResult<TData>>;
235
- updateMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateManyParams<TVariables>) => Promise<UpdateManyResult<TData>>;
236
- deleteMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: DeleteManyParams<TVariables>) => Promise<DeleteManyResult<TData>>;
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?: <TData = unknown, TVariables = unknown>(params: CustomParams<TVariables>) => Promise<CustomResult<TData>>;
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?: string;
251
+ id: string;
305
252
  wait(): Promise<TTask>;
306
- subscribe?(callback: (task: TTask) => void): TaskSubscription | (() => void) | undefined;
307
- cancel?(): Promise<unknown>;
308
- retry?(): Promise<unknown>;
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<unknown>;
322
- retry?(taskId: string): Promise<unknown>;
323
- subscribe?(taskId: string, callback: (task: TTask) => void): TaskSubscription | (() => void) | undefined;
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<{ redirectTo?: string; logout?: boolean }>;
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
- /** 声明资源使用的传输协议;具体 URL/序列化行为由对应 provider 实现。 */
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
- /** 声明资源使用的后端 adapter,不在 Core 内绑定具体运行时实现。 */
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
- /** 每资源的 provider 选择与声明性 transport/adapter 元数据。 */
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 选择及声明性 transport/adapter 元数据。 */
378
+ /** DataProvider selection and declarative transport/adapter metadata. */
451
379
  provider?: ResourceProviderConfig;
452
- /** @deprecated 新代码优先使用 `provider.dataProviderName` 选择 DataProvider。 */
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
 
@@ -518,7 +446,8 @@ export interface FieldDefinition {
518
446
 
519
447
  /**
520
448
  * Extend this interface via declaration merging to register resource types.
521
- * When registered, all hooks automatically infer data types from resource names.
449
+ * Query and mutation hooks infer data from explicit resource names.
450
+ * Extend ResourceInputMap as well to infer mutation payloads.
522
451
  *
523
452
  * @example
524
453
  * ```ts
@@ -531,12 +460,24 @@ export interface FieldDefinition {
531
460
  *
532
461
  * // Now hooks auto-infer:
533
462
  * const list = useList({ resource: 'users' })
534
- * // list.data → { id: string; name: string; email: string }[]
463
+ * // list.data?.data -> { id: string; name: string; email: string }[] | undefined
535
464
  * ```
536
465
  */
537
466
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
538
467
  export interface ResourceTypeMap {}
539
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
+
540
481
  /** When ResourceTypeMap is empty → string; otherwise → registered keys */
541
482
  export type KnownResources = keyof ResourceTypeMap extends never
542
483
  ? string
@@ -546,3 +487,9 @@ export type KnownResources = keyof ResourceTypeMap extends never
546
487
  export type InferData<R extends string> = R extends keyof ResourceTypeMap
547
488
  ? ResourceTypeMap[R]
548
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
+ }