@svadmin/core 0.49.1 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/package.json +21 -4
- package/src/access-control-contract.ts +250 -0
- package/src/adapters/casbin.ts +20 -14
- package/src/adapters/casl.ts +22 -19
- package/src/admin-tool.ts +7 -7
- package/src/audit-contract.ts +105 -0
- package/src/audit.ts +88 -128
- package/src/auth-hooks.svelte.ts +593 -261
- package/src/auth-mutation-contract.ts +143 -0
- package/src/auth-query-contract.ts +97 -0
- package/src/captured-mutation.svelte.ts +45 -0
- package/src/chatProvider.svelte.ts +1 -1
- package/src/command-contract.ts +195 -0
- package/src/context.svelte.ts +217 -205
- package/src/contract-context.svelte.ts +37 -0
- package/src/contract-form.svelte.ts +566 -0
- package/src/create-contract.ts +56 -0
- package/src/create-hooks.svelte.ts +347 -0
- package/src/create-many-contract.ts +90 -0
- package/src/create-many-hooks.svelte.ts +396 -0
- package/src/data-transfer.svelte.ts +1 -215
- package/src/defined-options.ts +50 -0
- package/src/delete-contract.ts +50 -0
- package/src/delete-hooks.svelte.ts +403 -0
- package/src/delete-many-contract.ts +60 -0
- package/src/delete-many-hooks.svelte.ts +402 -0
- package/src/enterprise.ts +4 -4
- package/src/export-format.ts +16 -14
- package/src/export-hooks.svelte.ts +191 -0
- package/src/feature-gate-contract.ts +53 -0
- package/src/hook-utils.svelte.ts +53 -105
- package/src/hooks.svelte.ts +19 -774
- package/src/http.ts +88 -87
- package/src/i18n.svelte.ts +56 -2
- package/src/import-contract.ts +106 -0
- package/src/import-hooks.svelte.ts +310 -0
- package/src/index.ts +77 -28
- package/src/inferencer-graphql.ts +7 -6
- package/src/infinite-query.svelte.ts +228 -0
- package/src/invalidation-contract.ts +26 -0
- package/src/live-sse.ts +91 -127
- package/src/live-subscription.svelte.ts +130 -0
- package/src/live-transport.ts +127 -0
- package/src/live-websocket.ts +85 -118
- package/src/live.svelte.ts +66 -68
- package/src/notification.svelte.ts +17 -15
- package/src/permission-catalog.ts +121 -0
- package/src/permission-hints-contract.ts +48 -0
- package/src/permissions.svelte.ts +112 -59
- package/src/plain-data.ts +61 -0
- package/src/provider-bundle.ts +85 -79
- package/src/provider-response.ts +35 -0
- package/src/query-hooks.svelte.ts +241 -286
- package/src/query-invalidation.ts +172 -0
- package/src/query-keys.ts +221 -110
- package/src/query-session.svelte.ts +216 -0
- package/src/query-snapshot.ts +144 -0
- package/src/reactive-projection.ts +53 -0
- package/src/record-decoder.ts +63 -0
- package/src/resource-binding.svelte.ts +19 -0
- package/src/resource-contract.ts +595 -0
- package/src/resource-mutation-types.ts +30 -0
- package/src/resource-schemas.ts +371 -0
- package/src/resource-transport-config.ts +156 -0
- package/src/route-parsing.ts +11 -11
- package/src/router-provider.ts +64 -59
- package/src/router.ts +8 -3
- package/src/routing-hooks.svelte.ts +90 -62
- package/src/schema-validation.ts +92 -0
- package/src/schema.ts +13 -0
- package/src/select-options.ts +95 -0
- package/src/select-query.svelte.ts +191 -0
- package/src/session-query.svelte.ts +88 -0
- package/src/strict-command-hooks.svelte.ts +333 -0
- package/src/strict-hooks.svelte.ts +198 -0
- package/src/table-hooks.svelte.ts +180 -174
- package/src/table-state.svelte.ts +117 -117
- package/src/task-contract.ts +87 -0
- package/src/task-hooks.svelte.ts +193 -149
- package/src/task-provider.ts +115 -0
- package/src/task-subscription.ts +132 -0
- package/src/theme.svelte.ts +155 -154
- package/src/toast.svelte.ts +38 -37
- package/src/types.ts +55 -103
- package/src/unsafe.ts +7 -0
- package/src/update-contract.ts +65 -0
- package/src/update-hooks.svelte.ts +347 -0
- package/src/update-many-contract.ts +79 -0
- package/src/update-many-hooks.svelte.ts +373 -0
- package/src/url-sync.ts +11 -10
- package/src/useCan.ts +96 -31
- package/src/useParsed.svelte.ts +18 -14
- package/src/useStepsForm.svelte.ts +123 -63
- package/src/utility-hooks.svelte.ts +101 -158
- package/src/form-hooks.svelte.ts +0 -889
- package/src/mutation-hooks.svelte.ts +0 -561
|
@@ -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,
|
|
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,
|
|
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
|
|
12
|
+
export type FilterSetMode='merge'|'replace';
|
|
13
13
|
|
|
14
|
-
export interface TableStateOptions<TData extends BaseRecord
|
|
15
|
-
extends Omit<UseListOptions<TData,
|
|
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'
|
|
21
|
+
mode?: 'off'|'server';
|
|
22
22
|
};
|
|
23
23
|
filters?: {
|
|
24
24
|
initial?: Filter[];
|
|
25
25
|
permanent?: Filter[];
|
|
26
|
-
mode?: 'off'
|
|
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
|
|
43
|
+
export class TableState {
|
|
44
44
|
// ─── Reactive state ───────────────────────────────────────────
|
|
45
|
-
pagination
|
|
46
|
-
currentSorters
|
|
47
|
-
currentFilters
|
|
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'
|
|
53
|
-
private filtersMode: 'off'
|
|
52
|
+
private sortersMode: 'off'|'server';
|
|
53
|
+
private filtersMode: 'off'|'server';
|
|
54
54
|
private filterDefaultBehavior: FilterSetMode;
|
|
55
55
|
private syncWithLocation: boolean;
|
|
56
|
-
private paginationMode:
|
|
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
|
|
63
|
-
this.adminContext
|
|
64
|
-
const parsed = useParsed();
|
|
65
|
-
const resource = options.resource ?? parsed.resource ?? '';
|
|
62
|
+
constructor(options: TableStateOptions={}) {
|
|
63
|
+
this.adminContext=captureAdminContext();
|
|
66
64
|
|
|
67
|
-
this.permanentSorters
|
|
68
|
-
this.permanentFilters
|
|
69
|
-
this.sortersMode
|
|
70
|
-
this.filtersMode
|
|
71
|
-
this.filterDefaultBehavior
|
|
72
|
-
this.syncWithLocation
|
|
73
|
-
this.paginationMode
|
|
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
|
|
77
|
-
let initSorters
|
|
78
|
-
let initFilters
|
|
79
|
-
|
|
80
|
-
if
|
|
81
|
-
const urlState
|
|
82
|
-
if
|
|
83
|
-
initPagination
|
|
84
|
-
current: urlState.page
|
|
85
|
-
pageSize: urlState.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
|
|
89
|
-
initSorters
|
|
86
|
+
if(urlState.sortField) {
|
|
87
|
+
initSorters=[{ field: urlState.sortField,order: urlState.sortOrder??'asc' }];
|
|
90
88
|
}
|
|
91
|
-
if
|
|
92
|
-
initFilters
|
|
89
|
+
if(urlState.filters) {
|
|
90
|
+
initFilters=urlState.filters;
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
this.pagination
|
|
97
|
-
this.currentSorters
|
|
98
|
-
this.currentFilters
|
|
94
|
+
this.pagination=initPagination;
|
|
95
|
+
this.currentSorters=initSorters;
|
|
96
|
+
this.currentFilters=initFilters;
|
|
99
97
|
|
|
100
98
|
// Create query
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
129
|
-
get effectiveFilters() { return [...this.currentFilters
|
|
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
|
|
132
|
+
return this.paginationMode==='server'
|
|
133
133
|
? this.pagination
|
|
134
|
-
:
|
|
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():
|
|
141
|
+
get data(): BaseRecord[] { return this.query.data?.data??[]; }
|
|
142
142
|
|
|
143
143
|
/** Total record count */
|
|
144
|
-
get total(): number { return
|
|
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():
|
|
153
|
+
get error(): unknown { return this.query.error; }
|
|
154
154
|
|
|
155
155
|
/** Current page number */
|
|
156
|
-
get current(): number { return this.pagination.current
|
|
156
|
+
get current(): number { return this.pagination.current??1; }
|
|
157
157
|
|
|
158
158
|
/** Page size */
|
|
159
|
-
get pageSize(): number { return this.pagination.pageSize
|
|
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
|
|
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
|
|
165
|
+
get hasNextPage(): boolean { return this.current<this.pageCount; }
|
|
166
166
|
|
|
167
167
|
/** Has previous page */
|
|
168
|
-
get hasPreviousPage(): boolean { return this.current
|
|
168
|
+
get hasPreviousPage(): boolean { return this.current>1; }
|
|
169
169
|
|
|
170
170
|
// ─── Mutators ─────────────────────────────────────────────────
|
|
171
171
|
setPage(page: number) {
|
|
172
|
-
this.pagination
|
|
172
|
+
this.pagination={ ...this.pagination,current: page };
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
setPageSize(size: number) {
|
|
176
|
-
this.pagination
|
|
176
|
+
this.pagination={ ...this.pagination,pageSize: size,current: 1 };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
nextPage() {
|
|
180
|
-
if
|
|
180
|
+
if(this.hasNextPage) this.setPage(this.current+1);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
previousPage() {
|
|
184
|
-
if
|
|
184
|
+
if(this.hasPreviousPage) this.setPage(this.current-1);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
setSorters(newSorters: Sort[]) {
|
|
188
|
-
this.currentSorters
|
|
189
|
-
this.pagination
|
|
188
|
+
this.currentSorters=newSorters;
|
|
189
|
+
this.pagination={ ...this.pagination,current: 1 };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
toggleSort(field: string) {
|
|
193
|
-
const existing
|
|
194
|
-
if
|
|
195
|
-
this.currentSorters
|
|
196
|
-
} else if
|
|
197
|
-
this.currentSorters
|
|
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
|
|
201
|
+
this.pagination={ ...this.pagination,current: 1 };
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
setFilters(newFilters: Filter[],
|
|
205
|
-
const behavior
|
|
206
|
-
if
|
|
207
|
-
const merged
|
|
208
|
-
for
|
|
209
|
-
if
|
|
210
|
-
|
|
211
|
-
if
|
|
212
|
-
if
|
|
213
|
-
else merged[idx]
|
|
214
|
-
} else if
|
|
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
|
|
218
|
+
this.currentFilters=merged;
|
|
219
219
|
} else {
|
|
220
|
-
this.currentFilters
|
|
220
|
+
this.currentFilters=newFilters;
|
|
221
221
|
}
|
|
222
|
-
this.pagination
|
|
222
|
+
this.pagination={ ...this.pagination,current: 1 };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
resetFilters() {
|
|
226
|
-
this.currentFilters
|
|
227
|
-
this.pagination
|
|
226
|
+
this.currentFilters=[];
|
|
227
|
+
this.pagination={ ...this.pagination,current: 1 };
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
get clientData():
|
|
231
|
-
if
|
|
232
|
-
const allData
|
|
233
|
-
const sorted
|
|
234
|
-
const activeSorters
|
|
235
|
-
if
|
|
236
|
-
sorted.sort((a,
|
|
237
|
-
for
|
|
238
|
-
const va
|
|
239
|
-
const vb
|
|
240
|
-
if
|
|
241
|
-
if
|
|
242
|
-
if
|
|
243
|
-
const cmp
|
|
244
|
-
if
|
|
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
|
|
250
|
-
const start
|
|
251
|
-
const end
|
|
252
|
-
return sorted.slice(start,
|
|
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
|
+
}
|