@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.
Files changed (98) 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 +64 -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/inferencer.ts +9 -0
  40. package/src/infinite-query.svelte.ts +228 -0
  41. package/src/invalidation-contract.ts +26 -0
  42. package/src/live-sse.ts +91 -127
  43. package/src/live-subscription.svelte.ts +130 -0
  44. package/src/live-transport.ts +127 -0
  45. package/src/live-websocket.ts +85 -118
  46. package/src/live.svelte.ts +66 -68
  47. package/src/notification.svelte.ts +17 -15
  48. package/src/permission-catalog.ts +121 -0
  49. package/src/permission-hints-contract.ts +48 -0
  50. package/src/permissions.svelte.ts +112 -59
  51. package/src/plain-data.ts +61 -0
  52. package/src/provider-bundle.ts +85 -79
  53. package/src/provider-response.ts +35 -0
  54. package/src/query-hooks.svelte.ts +241 -288
  55. package/src/query-invalidation.ts +172 -0
  56. package/src/query-keys.ts +221 -110
  57. package/src/query-session.svelte.ts +220 -0
  58. package/src/query-snapshot.ts +144 -0
  59. package/src/reactive-projection.ts +53 -0
  60. package/src/record-decoder.ts +63 -0
  61. package/src/resource-binding.svelte.ts +19 -0
  62. package/src/resource-contract.ts +595 -0
  63. package/src/resource-mutation-types.ts +30 -0
  64. package/src/resource-schemas.ts +371 -0
  65. package/src/resource-transport-config.ts +156 -0
  66. package/src/route-parsing.ts +11 -11
  67. package/src/router-provider.ts +64 -59
  68. package/src/router.ts +8 -3
  69. package/src/routing-hooks.svelte.ts +90 -62
  70. package/src/schema-validation.ts +92 -0
  71. package/src/schema.ts +13 -0
  72. package/src/select-options.ts +95 -0
  73. package/src/select-query.svelte.ts +191 -0
  74. package/src/session-query.svelte.ts +103 -0
  75. package/src/strict-command-hooks.svelte.ts +333 -0
  76. package/src/strict-hooks.svelte.ts +198 -0
  77. package/src/table-hooks.svelte.ts +180 -174
  78. package/src/table-state.svelte.ts +117 -117
  79. package/src/task-contract.ts +87 -0
  80. package/src/task-hooks.svelte.ts +190 -180
  81. package/src/task-provider.ts +115 -0
  82. package/src/task-subscription.ts +132 -0
  83. package/src/theme.svelte.ts +155 -154
  84. package/src/toast.svelte.ts +38 -37
  85. package/src/types.ts +68 -110
  86. package/src/unsafe.ts +7 -0
  87. package/src/update-contract.ts +65 -0
  88. package/src/update-hooks.svelte.ts +347 -0
  89. package/src/update-many-contract.ts +79 -0
  90. package/src/update-many-hooks.svelte.ts +373 -0
  91. package/src/url-sync.ts +11 -10
  92. package/src/useCan.ts +96 -31
  93. package/src/useParsed.svelte.ts +18 -14
  94. package/src/useStepsForm.svelte.ts +123 -63
  95. package/src/utility-hooks.svelte.ts +101 -158
  96. package/src/form-hooks.svelte.ts +0 -889
  97. package/src/mutation-hooks.svelte.ts +0 -561
  98. 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
 
@@ -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
- * When registered, all hooks automatically infer data types from resource names.
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 → { id: string; name: string; email: string }[]
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
+ }