@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.
Files changed (97) hide show
  1. package/README.md +32 -0
  2. package/package.json +21 -4
  3. package/src/access-control-contract.ts +250 -0
  4. package/src/adapters/casbin.ts +20 -14
  5. package/src/adapters/casl.ts +22 -19
  6. package/src/admin-tool.ts +7 -7
  7. package/src/audit-contract.ts +105 -0
  8. package/src/audit.ts +88 -128
  9. package/src/auth-hooks.svelte.ts +593 -261
  10. package/src/auth-mutation-contract.ts +143 -0
  11. package/src/auth-query-contract.ts +97 -0
  12. package/src/captured-mutation.svelte.ts +45 -0
  13. package/src/chatProvider.svelte.ts +1 -1
  14. package/src/command-contract.ts +195 -0
  15. package/src/context.svelte.ts +217 -205
  16. package/src/contract-context.svelte.ts +37 -0
  17. package/src/contract-form.svelte.ts +566 -0
  18. package/src/create-contract.ts +56 -0
  19. package/src/create-hooks.svelte.ts +347 -0
  20. package/src/create-many-contract.ts +90 -0
  21. package/src/create-many-hooks.svelte.ts +396 -0
  22. package/src/data-transfer.svelte.ts +1 -215
  23. package/src/defined-options.ts +50 -0
  24. package/src/delete-contract.ts +50 -0
  25. package/src/delete-hooks.svelte.ts +403 -0
  26. package/src/delete-many-contract.ts +60 -0
  27. package/src/delete-many-hooks.svelte.ts +402 -0
  28. package/src/enterprise.ts +4 -4
  29. package/src/export-format.ts +16 -14
  30. package/src/export-hooks.svelte.ts +191 -0
  31. package/src/feature-gate-contract.ts +53 -0
  32. package/src/hook-utils.svelte.ts +53 -105
  33. package/src/hooks.svelte.ts +19 -774
  34. package/src/http.ts +88 -87
  35. package/src/i18n.svelte.ts +56 -2
  36. package/src/import-contract.ts +106 -0
  37. package/src/import-hooks.svelte.ts +310 -0
  38. package/src/index.ts +77 -28
  39. package/src/inferencer-graphql.ts +7 -6
  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 -286
  55. package/src/query-invalidation.ts +172 -0
  56. package/src/query-keys.ts +221 -110
  57. package/src/query-session.svelte.ts +216 -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 +88 -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 +193 -149
  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 +55 -103
  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
@@ -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;
@@ -217,90 +224,35 @@ export interface CustomResult<TData = unknown> {
217
224
 
218
225
  export interface DataProvider {
219
226
  // Required methods
220
- getList: <TData extends BaseRecord = BaseRecord>(params: GetListParams) => Promise<GetListResult<TData>>;
221
- getOne: <TData extends BaseRecord = BaseRecord>(params: GetOneParams) => Promise<GetOneResult<TData>>;
222
- create: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateParams<TVariables>) => Promise<CreateResult<TData>>;
223
- update: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateParams<TVariables>) => Promise<UpdateResult<TData>>;
224
- 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>;
225
232
  getApiUrl: () => string;
226
233
 
227
234
  // Optional bulk methods
228
- getMany?: <TData extends BaseRecord = BaseRecord>(params: GetManyParams) => Promise<GetManyResult<TData>>;
229
- createMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: CreateManyParams<TVariables>) => Promise<CreateManyResult<TData>>;
230
- updateMany?: <TData extends BaseRecord = BaseRecord, TVariables = unknown>(params: UpdateManyParams<TVariables>) => Promise<UpdateManyResult<TData>>;
231
- 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>;
232
239
 
233
240
  // Optional custom method
234
- custom?: <TData = unknown, TVariables = unknown>(params: CustomParams<TVariables>) => Promise<CustomResult<TData>>;
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?: string;
251
+ id: string;
300
252
  wait(): Promise<TTask>;
301
- subscribe?(callback: (task: TTask) => void): TaskSubscription | (() => void) | undefined;
302
- cancel?(): Promise<unknown>;
303
- retry?(): Promise<unknown>;
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<unknown>;
317
- retry?(taskId: string): Promise<unknown>;
318
- 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);
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<{ redirectTo?: string; logout?: boolean }>;
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
- /** 声明资源使用的传输协议;具体 URL/序列化行为由对应 provider 实现。 */
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
- /** 声明资源使用的后端 adapter,不在 Core 内绑定具体运行时实现。 */
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
- /** 每资源的 provider 选择与声明性 transport/adapter 元数据。 */
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 选择及声明性 transport/adapter 元数据。 */
378
+ /** DataProvider selection and declarative transport/adapter metadata. */
446
379
  provider?: ResourceProviderConfig;
447
- /** @deprecated 新代码优先使用 `provider.dataProviderName` 选择 DataProvider。 */
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
- * 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.
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 → { id: string; name: string; email: string }[]
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
+ }