@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,29 +1,29 @@
1
+ import { definedOptions } from './defined-options';
1
2
  // TableState — Svelte 5 Class State Pattern for table data management
2
3
  // Reactive class that encapsulates all table state with fine-grained $state/$derived
3
4
 
4
- import { useParsed } from './useParsed.svelte';
5
5
  import { useList } from './query-hooks.svelte';
6
- import { readURLState, writeURLState } from './url-sync';
6
+ import { readURLState,writeURLState } from './url-sync';
7
7
  import { captureAdminContext } from './context.svelte';
8
8
  import type { AdminContextAccessor } from './context.svelte';
9
- import type { Pagination, Sort, Filter, BaseRecord, HttpError, KnownResources, GetListResult } from './types';
9
+ import type { Pagination,Sort,Filter,BaseRecord,HttpError,KnownResources } from './types';
10
10
  import type { UseListOptions } from './query-hooks.svelte';
11
11
 
12
- export type FilterSetMode = 'merge' | 'replace';
12
+ export type FilterSetMode='merge'|'replace';
13
13
 
14
- export interface TableStateOptions<TData extends BaseRecord = BaseRecord, TError = HttpError>
15
- extends Omit<UseListOptions<TData, TError>, 'pagination' | 'sorters' | 'filters'> {
14
+ export interface TableStateOptions<TData extends BaseRecord=BaseRecord,TError=HttpError>
15
+ extends Omit<UseListOptions<TData,TError>,'pagination'|'sorters'|'filters'> {
16
16
  resource?: KnownResources;
17
17
  pagination?: Pagination;
18
18
  sorters?: {
19
19
  initial?: Sort[];
20
20
  permanent?: Sort[];
21
- mode?: 'off' | 'server';
21
+ mode?: 'off'|'server';
22
22
  };
23
23
  filters?: {
24
24
  initial?: Filter[];
25
25
  permanent?: Filter[];
26
- mode?: 'off' | 'server';
26
+ mode?: 'off'|'server';
27
27
  defaultBehavior?: FilterSetMode;
28
28
  };
29
29
  syncWithLocation?: boolean;
@@ -40,108 +40,108 @@ export interface TableStateOptions<TData extends BaseRecord = BaseRecord, TError
40
40
  * console.log(table.data); // reactive
41
41
  * ```
42
42
  */
43
- export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpError> {
43
+ export class TableState {
44
44
  // ─── Reactive state ───────────────────────────────────────────
45
- pagination = $state<Pagination>({ current: 1, pageSize: 10 });
46
- currentSorters = $state<Sort[]>([]);
47
- currentFilters = $state<Filter[]>([]);
45
+ pagination=$state<Pagination>({ current: 1,pageSize: 10 });
46
+ currentSorters=$state<Sort[]>([]);
47
+ currentFilters=$state<Filter[]>([]);
48
48
 
49
49
  // ─── Private config ───────────────────────────────────────────
50
50
  private permanentSorters: Sort[];
51
51
  private permanentFilters: Filter[];
52
- private sortersMode: 'off' | 'server';
53
- private filtersMode: 'off' | 'server';
52
+ private sortersMode: 'off'|'server';
53
+ private filtersMode: 'off'|'server';
54
54
  private filterDefaultBehavior: FilterSetMode;
55
55
  private syncWithLocation: boolean;
56
- private paginationMode: string;
56
+ private paginationMode: NonNullable<Pagination['mode']>;
57
57
  private adminContext: AdminContextAccessor;
58
58
 
59
59
  // ─── Query result ─────────────────────────────────────────────
60
60
  private _queryResult;
61
61
 
62
- constructor(options: TableStateOptions<TData, TError> = {}) {
63
- this.adminContext = captureAdminContext();
64
- const parsed = useParsed();
65
- const resource = options.resource ?? parsed.resource ?? '';
62
+ constructor(options: TableStateOptions={}) {
63
+ this.adminContext=captureAdminContext();
66
64
 
67
- this.permanentSorters = options.sorters?.permanent ?? [];
68
- this.permanentFilters = options.filters?.permanent ?? [];
69
- this.sortersMode = options.sorters?.mode ?? 'server';
70
- this.filtersMode = options.filters?.mode ?? 'server';
71
- this.filterDefaultBehavior = options.filters?.defaultBehavior ?? 'replace';
72
- this.syncWithLocation = options.syncWithLocation ?? false;
73
- this.paginationMode = options.pagination?.mode ?? 'server';
65
+ this.permanentSorters=options.sorters?.permanent??[];
66
+ this.permanentFilters=options.filters?.permanent??[];
67
+ this.sortersMode=options.sorters?.mode??'server';
68
+ this.filtersMode=options.filters?.mode??'server';
69
+ this.filterDefaultBehavior=options.filters?.defaultBehavior??'replace';
70
+ this.syncWithLocation=options.syncWithLocation??false;
71
+ this.paginationMode=options.pagination?.mode??'server';
74
72
 
75
73
  // Init from URL or defaults
76
- let initPagination = options.pagination ?? { current: 1, pageSize: 10 };
77
- let initSorters = options.sorters?.initial ?? [];
78
- let initFilters = options.filters?.initial ?? [];
79
-
80
- if (this.syncWithLocation && typeof window !== 'undefined') {
81
- const urlState = readURLState(this.adminContext);
82
- if (urlState.page || urlState.pageSize) {
83
- initPagination = {
84
- current: urlState.page ?? initPagination.current,
85
- pageSize: urlState.pageSize ?? initPagination.pageSize,
86
- };
74
+ let initPagination=options.pagination??{ current: 1,pageSize: 10 };
75
+ let initSorters=options.sorters?.initial??[];
76
+ let initFilters=options.filters?.initial??[];
77
+
78
+ if(this.syncWithLocation&&typeof window!=='undefined') {
79
+ const urlState=readURLState(this.adminContext);
80
+ if(urlState.page||urlState.pageSize) {
81
+ initPagination=definedOptions({
82
+ current: urlState.page??initPagination.current,
83
+ pageSize: urlState.pageSize??initPagination.pageSize,
84
+ });
87
85
  }
88
- if (urlState.sortField) {
89
- initSorters = [{ field: urlState.sortField, order: (urlState.sortOrder as 'asc' | 'desc') ?? 'asc' }];
86
+ if(urlState.sortField) {
87
+ initSorters=[{ field: urlState.sortField,order: urlState.sortOrder??'asc' }];
90
88
  }
91
- if (urlState.filters) {
92
- initFilters = urlState.filters;
89
+ if(urlState.filters) {
90
+ initFilters=urlState.filters;
93
91
  }
94
92
  }
95
93
 
96
- this.pagination = initPagination;
97
- this.currentSorters = initSorters;
98
- this.currentFilters = initFilters;
94
+ this.pagination=initPagination;
95
+ this.currentSorters=initSorters;
96
+ this.currentFilters=initFilters;
99
97
 
100
98
  // Create query
101
- const _this = this; // eslint-disable-line @typescript-eslint/no-this-alias
102
- const { meta, syncWithLocation: _s, dataProviderName, pagination: _p, sorters: _sr, filters: _f, ...restOptions } = options;
103
- this._queryResult = useList<TData, TError>({
104
- resource,
105
- get pagination() { return _this.queryPagination; },
106
- get sorters() { return _this.effectiveSorters; },
107
- get filters() { return _this.effectiveFilters; },
108
- meta,
109
- dataProviderName,
99
+ const { meta,syncWithLocation: _s,dataProviderName,pagination: _p,sorters: _sr,filters: _f,...restOptions }=options;
100
+ this._queryResult=useList(() => ({
101
+ pagination: this.queryPagination,
102
+ sorters: this.effectiveSorters,
103
+ filters: this.effectiveFilters,
104
+ ...definedOptions({
105
+ meta,
106
+ dataProviderName,
107
+ }),
110
108
  ...restOptions,
111
- });
109
+ }));
112
110
 
113
111
  // URL sync effect
114
- if (this.syncWithLocation && typeof window !== 'undefined') {
112
+ if(this.syncWithLocation&&typeof window!=='undefined') {
115
113
  $effect(() => {
116
114
  writeURLState({
117
115
  page: this.pagination.current,
118
116
  pageSize: this.pagination.pageSize,
119
117
  sortField: this.currentSorters[0]?.field,
120
118
  sortOrder: this.currentSorters[0]?.order,
121
- filters: this.currentFilters,
122
- }, this.adminContext);
119
+ ...definedOptions({
120
+ filters: this.currentFilters
121
+ })
122
+ },this.adminContext);
123
123
  });
124
124
  }
125
125
  }
126
126
 
127
127
  // ─── Derived state ────────────────────────────────────────────
128
- get effectiveSorters() { return [...this.currentSorters, ...this.permanentSorters]; }
129
- get effectiveFilters() { return [...this.currentFilters, ...this.permanentFilters]; }
128
+ get effectiveSorters() { return [...this.currentSorters,...this.permanentSorters]; }
129
+ get effectiveFilters() { return [...this.currentFilters,...this.permanentFilters]; }
130
130
 
131
131
  private get queryPagination() {
132
- return this.paginationMode === 'server'
132
+ return this.paginationMode==='server'
133
133
  ? this.pagination
134
- : { current: 1, pageSize: 999999, mode: this.paginationMode as 'client' | 'off' };
134
+ :{ current: 1,pageSize: 999999,mode: this.paginationMode };
135
135
  }
136
136
 
137
137
  /** Underlying TanStack Query result */
138
138
  get query() { return this._queryResult; }
139
139
 
140
140
  /** Table data rows */
141
- get data(): TData[] { return (this.query.data as GetListResult<TData> | undefined)?.data ?? []; }
141
+ get data(): BaseRecord[] { return this.query.data?.data??[]; }
142
142
 
143
143
  /** Total record count */
144
- get total(): number { return (this.query.data as GetListResult<TData> | undefined)?.total ?? 0; }
144
+ get total(): number { return this.query.data?.total??0; }
145
145
 
146
146
  /** Is data loading */
147
147
  get isLoading(): boolean { return this.query.isLoading; }
@@ -150,105 +150,105 @@ export class TableState<TData extends BaseRecord = BaseRecord, TError = HttpErro
150
150
  get isError(): boolean { return this.query.isError; }
151
151
 
152
152
  /** Error object */
153
- get error(): TError | null { return this.query.error; }
153
+ get error(): unknown { return this.query.error; }
154
154
 
155
155
  /** Current page number */
156
- get current(): number { return this.pagination.current ?? 1; }
156
+ get current(): number { return this.pagination.current??1; }
157
157
 
158
158
  /** Page size */
159
- get pageSize(): number { return this.pagination.pageSize ?? 10; }
159
+ get pageSize(): number { return this.pagination.pageSize??10; }
160
160
 
161
161
  /** Total number of pages */
162
- get pageCount(): number { return Math.ceil(this.total / this.pageSize); }
162
+ get pageCount(): number { return Math.ceil(this.total/this.pageSize); }
163
163
 
164
164
  /** Has next page */
165
- get hasNextPage(): boolean { return this.current < this.pageCount; }
165
+ get hasNextPage(): boolean { return this.current<this.pageCount; }
166
166
 
167
167
  /** Has previous page */
168
- get hasPreviousPage(): boolean { return this.current > 1; }
168
+ get hasPreviousPage(): boolean { return this.current>1; }
169
169
 
170
170
  // ─── Mutators ─────────────────────────────────────────────────
171
171
  setPage(page: number) {
172
- this.pagination = { ...this.pagination, current: page };
172
+ this.pagination={ ...this.pagination,current: page };
173
173
  }
174
174
 
175
175
  setPageSize(size: number) {
176
- this.pagination = { ...this.pagination, pageSize: size, current: 1 };
176
+ this.pagination={ ...this.pagination,pageSize: size,current: 1 };
177
177
  }
178
178
 
179
179
  nextPage() {
180
- if (this.hasNextPage) this.setPage(this.current + 1);
180
+ if(this.hasNextPage) this.setPage(this.current+1);
181
181
  }
182
182
 
183
183
  previousPage() {
184
- if (this.hasPreviousPage) this.setPage(this.current - 1);
184
+ if(this.hasPreviousPage) this.setPage(this.current-1);
185
185
  }
186
186
 
187
187
  setSorters(newSorters: Sort[]) {
188
- this.currentSorters = newSorters;
189
- this.pagination = { ...this.pagination, current: 1 };
188
+ this.currentSorters=newSorters;
189
+ this.pagination={ ...this.pagination,current: 1 };
190
190
  }
191
191
 
192
192
  toggleSort(field: string) {
193
- const existing = this.currentSorters.find(s => s.field === field);
194
- if (!existing) {
195
- this.currentSorters = [{ field, order: 'asc' }];
196
- } else if (existing.order === 'asc') {
197
- this.currentSorters = [{ field, order: 'desc' }];
193
+ const existing=this.currentSorters.find(s => s.field===field);
194
+ if(!existing) {
195
+ this.currentSorters=[{ field,order: 'asc' }];
196
+ } else if(existing.order==='asc') {
197
+ this.currentSorters=[{ field,order: 'desc' }];
198
198
  } else {
199
- this.currentSorters = [];
199
+ this.currentSorters=[];
200
200
  }
201
- this.pagination = { ...this.pagination, current: 1 };
201
+ this.pagination={ ...this.pagination,current: 1 };
202
202
  }
203
203
 
204
- setFilters(newFilters: Filter[], mode?: FilterSetMode) {
205
- const behavior = mode ?? this.filterDefaultBehavior;
206
- if (behavior === 'merge') {
207
- const merged = [...this.currentFilters];
208
- for (const nf of newFilters) {
209
- if (!('field' in nf)) { merged.push(nf); continue; }
210
- const idx = merged.findIndex(f => 'field' in f && f.field === nf.field && f.operator === nf.operator);
211
- if (idx >= 0) {
212
- if (nf.value === undefined || nf.value === null || nf.value === '') merged.splice(idx, 1);
213
- else merged[idx] = nf;
214
- } else if (nf.value !== undefined && nf.value !== null && nf.value !== '') {
204
+ setFilters(newFilters: Filter[],mode?: FilterSetMode) {
205
+ const behavior=mode??this.filterDefaultBehavior;
206
+ if(behavior==='merge') {
207
+ const merged=[...this.currentFilters];
208
+ for(const nf of newFilters) {
209
+ if(!('field' in nf)) { merged.push(nf); continue; }
210
+ const idx=merged.findIndex(f => 'field' in f&&f.field===nf.field&&f.operator===nf.operator);
211
+ if(idx>=0) {
212
+ if(nf.value===undefined||nf.value===null||nf.value==='') merged.splice(idx,1);
213
+ else merged[idx]=nf;
214
+ } else if(nf.value!==undefined&&nf.value!==null&&nf.value!=='') {
215
215
  merged.push(nf);
216
216
  }
217
217
  }
218
- this.currentFilters = merged;
218
+ this.currentFilters=merged;
219
219
  } else {
220
- this.currentFilters = newFilters;
220
+ this.currentFilters=newFilters;
221
221
  }
222
- this.pagination = { ...this.pagination, current: 1 };
222
+ this.pagination={ ...this.pagination,current: 1 };
223
223
  }
224
224
 
225
225
  resetFilters() {
226
- this.currentFilters = [];
227
- this.pagination = { ...this.pagination, current: 1 };
226
+ this.currentFilters=[];
227
+ this.pagination={ ...this.pagination,current: 1 };
228
228
  }
229
229
 
230
- get clientData(): TData[] {
231
- if (this.paginationMode === 'server') return [];
232
- const allData = this.data;
233
- const sorted = [...allData];
234
- const activeSorters = this.sortersMode === 'off' ? this.effectiveSorters : [];
235
- if (activeSorters.length > 0) {
236
- sorted.sort((a, b) => {
237
- for (const { field, order } of activeSorters) {
238
- const va = (a as Record<string, unknown>)[field];
239
- const vb = (b as Record<string, unknown>)[field];
240
- if (va == null && vb == null) continue;
241
- if (va == null) return 1;
242
- if (vb == null) return -1;
243
- const cmp = va < vb ? -1 : va > vb ? 1 : 0;
244
- if (cmp !== 0) return order === 'desc' ? -cmp : cmp;
230
+ get clientData(): BaseRecord[] {
231
+ if(this.paginationMode==='server') return [];
232
+ const allData=this.data;
233
+ const sorted=[...allData];
234
+ const activeSorters=this.sortersMode==='off'? this.effectiveSorters:[];
235
+ if(activeSorters.length>0) {
236
+ sorted.sort((a,b) => {
237
+ for(const { field,order } of activeSorters) {
238
+ const va=a[field];
239
+ const vb=b[field];
240
+ if(va==null&&vb==null) continue;
241
+ if(va==null) return 1;
242
+ if(vb==null) return -1;
243
+ const cmp=va<vb? -1:va>vb? 1:0;
244
+ if(cmp!==0) return order==='desc'? -cmp:cmp;
245
245
  }
246
246
  return 0;
247
247
  });
248
248
  }
249
- if (this.paginationMode === 'off') return sorted;
250
- const start = (this.current - 1) * this.pageSize;
251
- const end = start + this.pageSize;
252
- return sorted.slice(start, end);
249
+ if(this.paginationMode==='off') return sorted;
250
+ const start=(this.current-1)*this.pageSize;
251
+ const end=start+this.pageSize;
252
+ return sorted.slice(start,end);
253
253
  }
254
254
  }
@@ -0,0 +1,87 @@
1
+ import { Type, type Static } from '@sinclair/typebox';
2
+ import { checkExact } from './schema-validation';
3
+ import { snapshotPlainData } from './plain-data';
4
+
5
+ const text = Type.String({ minLength: 1 });
6
+ const dateValue = Type.Union([text, Type.Null()]);
7
+ const messageValue = Type.Union([Type.String(), Type.Null()]);
8
+ const record = Type.Record(Type.String(), Type.Unknown());
9
+ const taskSchema = Type.Intersect([
10
+ Type.Object({
11
+ id: text,
12
+ name: Type.Optional(text), title: Type.Optional(text), status: Type.Optional(text),
13
+ progress: Type.Optional(Type.Union([Type.Number({ minimum: 0, maximum: 100 }), Type.Null()])),
14
+ priority: Type.Optional(Type.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })),
15
+ queueName: Type.Optional(text), queue_name: Type.Optional(text),
16
+ message: Type.Optional(messageValue),
17
+ payload: Type.Optional(Type.Unknown()), result: Type.Optional(Type.Unknown()), result_data: Type.Optional(Type.Unknown()),
18
+ createdAt: Type.Optional(dateValue), created_at: Type.Optional(dateValue),
19
+ updatedAt: Type.Optional(dateValue), updated_at: Type.Optional(dateValue),
20
+ startedAt: Type.Optional(dateValue), started_at: Type.Optional(dateValue),
21
+ finishedAt: Type.Optional(dateValue), finished_at: Type.Optional(dateValue),
22
+ cancelledAt: Type.Optional(dateValue), cancelled_at: Type.Optional(dateValue),
23
+ error: Type.Optional(Type.Unknown()),
24
+ errorMessage: Type.Optional(messageValue), error_message: Type.Optional(messageValue),
25
+ }),
26
+ record,
27
+ ]);
28
+ const submitSchema = Type.Object({
29
+ body: Type.Optional(record),
30
+ idempotencyKey: Type.Optional(text),
31
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
32
+ meta: Type.Optional(record),
33
+ }, { additionalProperties: false });
34
+ const listSchema = Type.Object({
35
+ data: Type.Array(taskSchema),
36
+ total: Type.Optional(Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER })),
37
+ }, { additionalProperties: false });
38
+
39
+ export type TaskRecord = Static<typeof taskSchema>;
40
+ export type TaskDateValue = Static<typeof dateValue>;
41
+ export type TaskMessageValue = Static<typeof messageValue>;
42
+ export type SubmitTaskOptions = Static<typeof submitSchema>;
43
+
44
+ const messages = {
45
+ INVALID_TASK_INPUT: 'Invalid task input.',
46
+ INVALID_TASK_RESPONSE: 'Invalid task response.',
47
+ TASK_PROVIDER_FAILED: 'Task request failed.',
48
+ TASK_SUBSCRIPTION_FAILED: 'Task subscription failed.',
49
+ TASK_CALLBACK_FAILED: 'Task callback failed.',
50
+ } as const;
51
+
52
+ export class TaskError extends Error {
53
+ constructor(readonly code: keyof typeof messages, readonly writeMayHaveSucceeded = false) {
54
+ super(messages[code]);
55
+ this.name = 'TaskError';
56
+ }
57
+ }
58
+
59
+ export function decodeTaskRecord(value: unknown, expectedId?: string, write = false): TaskRecord {
60
+ try {
61
+ const candidate = snapshotPlainData(value);
62
+ if (checkExact(taskSchema, candidate) && (expectedId === undefined || candidate.id === expectedId)) return candidate;
63
+ } catch {
64
+ // Do not expose submitted records or reflection errors.
65
+ }
66
+ throw new TaskError('INVALID_TASK_RESPONSE', write);
67
+ }
68
+
69
+ export function decodeTaskList(value: unknown): Static<typeof listSchema> {
70
+ try {
71
+ const candidate = snapshotPlainData(value);
72
+ if (checkExact(listSchema, candidate)) return candidate;
73
+ } catch {
74
+ // Normalize reflection and serialization failures.
75
+ }
76
+ throw new TaskError('INVALID_TASK_RESPONSE');
77
+ }
78
+
79
+ export function decodeTaskSubmitOptions(value: unknown): SubmitTaskOptions {
80
+ try {
81
+ const candidate = snapshotPlainData(value);
82
+ if (checkExact(submitSchema, candidate)) return candidate;
83
+ } catch {
84
+ // Optional fields must be omitted, not serialized as undefined.
85
+ }
86
+ throw new TaskError('INVALID_TASK_INPUT');
87
+ }