@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,402 +1,357 @@
1
- import { createQuery } from '@tanstack/svelte-query';
1
+ import { definedOptions } from './defined-options';
2
+ import { createSessionQuery } from './session-query.svelte';
2
3
  import { getAdminOptions } from './options.svelte';
3
4
  import { captureAdminContext } from './context.svelte';
5
+ import { captureContractContext,type ContractOptions } from './contract-context.svelte';
4
6
  import { useParsed } from './useParsed.svelte';
5
7
  import {
6
- checkError,
7
8
  createOvertimeTracker,
8
9
  createLiveSubscription,
9
- fireSuccessNotification,
10
- fireErrorNotification,
11
10
  } from './hook-utils.svelte';
12
- import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
11
+ import type { NotificationConfig,OvertimeOptions,LiveSubscriptionParams } from './hook-utils.svelte';
13
12
  import type {
14
- GetListResult, Pagination, Sort, Filter, BaseRecord, HttpError,
15
- GetOneResult, GetManyResult,
13
+ GetListResult,Pagination,Sort,Filter,BaseRecord,
14
+ GetOneResult,GetManyResult,
16
15
  KnownResources,
17
16
  } from './types';
18
- import type { LiveMode, LiveEvent } from './live.svelte';
19
-
20
- /**
21
- * Extend a Tanstack Query result with additional properties.
22
- * The query object is returned as-is, with extra properties accessible via Proxy.
23
- */
24
- function extendQuery<Q extends object, E extends Record<string, unknown>>(
25
- query: Q,
26
- extensions: E,
27
- ): Q & E {
28
- return new Proxy(query, {
29
- get(target, prop) {
30
- if (typeof prop === 'string' && prop in extensions) {
31
- return extensions[prop as keyof E];
32
- }
33
- return target[prop as keyof Q];
34
- },
35
- has(target, prop) {
36
- if (typeof prop === 'string' && prop in extensions) return true;
37
- return prop in target;
38
- },
39
- }) as Q & E;
40
- }
17
+ import type { LiveMode,LiveEvent } from './live.svelte';
18
+ import { decodeBaseRecord, decodeListResult, decodeOneResult, decodeManyResult, rejectProviderResponse, type RecordDecoder } from './record-decoder';
19
+ import { HttpError } from './types';
20
+ import { extendReactiveMembers } from './reactive-projection';
21
+ import { captureQueryProvider, snapshotListParams, snapshotOneParams, snapshotManyParams } from './query-snapshot';
41
22
 
42
23
  // ─── useList ───────────────────────────────────────────────────
43
24
 
44
- export type MaybeGetter<T> = T | (() => T);
45
-
46
- function cloneQueryKeyPart<T>(value: T): T {
47
- if (Array.isArray(value)) {
48
- return value.map(item => cloneQueryKeyPart(item)) as T;
49
- }
50
- if (value && typeof value === 'object') {
51
- const proto = Object.getPrototypeOf(value);
52
- if (proto !== Object.prototype && proto !== null) return value;
53
- return Object.fromEntries(
54
- Object.entries(value as Record<string, unknown>).map(([key, entry]) => [key, cloneQueryKeyPart(entry)])
55
- ) as T;
56
- }
57
- return value;
58
- }
25
+ export type MaybeGetter<T>=T|(() => T);
59
26
 
60
- export interface UseListOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
27
+ export interface UseListOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> extends ContractOptions {
61
28
  resource?: KnownResources;
62
29
  pagination?: Pagination;
63
30
  sorters?: Sort[];
64
31
  filters?: Filter[];
65
- meta?: Record<string, unknown>;
32
+ meta?: Record<string,unknown>;
66
33
  dataProviderName?: string;
67
34
  queryOptions?: { staleTime?: number; enabled?: boolean; gcTime?: number; refetchOnWindowFocus?: boolean };
68
35
  successNotification?: NotificationConfig;
69
36
  errorNotification?: NotificationConfig;
70
37
  liveMode?: LiveMode;
71
38
  onLiveEvent?: (event: LiveEvent) => void;
72
- liveParams?: Record<string, unknown>;
39
+ liveParams?: Record<string,unknown>;
73
40
  overtimeOptions?: OvertimeOptions;
74
41
  }
75
42
 
76
- export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseListOptions<TData, TError>> = {}) {
77
- const adminContext = captureAdminContext();
78
- const parsed = useParsed();
79
- const adminOptions = getAdminOptions();
80
-
81
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
82
- const getResource = () => getOptions().resource ?? parsed.resource ?? '';
83
-
84
- const query = createQuery<GetListResult<TData>, TError>(() => {
85
- const opts = getOptions();
86
- const resource = getResource();
87
- const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
88
- const queryOptions = opts.queryOptions;
89
- const pagination = cloneQueryKeyPart(opts.pagination);
90
- const sorters = cloneQueryKeyPart(opts.sorters);
91
- const explicitFilters = cloneQueryKeyPart(opts.filters);
92
- const meta = cloneQueryKeyPart(opts.meta);
93
-
94
- // 自动注入嵌套资源 parent filter
95
- // 仅消费路径解析出的 parentParams(如 /teams/123/users 的 teamId),
96
- // 不再从 query params 中误取,避免 ?tenantId=1 等被静默转成 filter。
43
+ export function useList(optionsOrGetter: MaybeGetter<UseListOptions>={}) {
44
+ return createListQuery(optionsOrGetter, () => decodeBaseRecord);
45
+ }
46
+
47
+ export function createListQuery<TData extends BaseRecord>(
48
+ optionsOrGetter: MaybeGetter<UseListOptions>, getDecoder: () => RecordDecoder<TData>,
49
+ ) {
50
+ const adminContext=captureContractContext(optionsOrGetter);
51
+ const parsed=useParsed();
52
+ const adminOptions=getAdminOptions();
53
+
54
+ const getOptions=() => typeof optionsOrGetter==='function'? optionsOrGetter():optionsOrGetter;
55
+ const getResource=() => getOptions().resource??parsed.resource??'';
56
+
57
+ const query=createSessionQuery<GetListResult<TData>>(adminContext,() => {
58
+ const decode=getDecoder();
59
+ const opts=getOptions();
60
+ const checkId=opts.contract!==undefined;
61
+ const resource=opts.resource??parsed.resource??'';
62
+ const captured=captureQueryProvider(adminContext,{
63
+ resource,...definedOptions({ dataProviderName: opts.dataProviderName,contract: opts.contract,meta: opts.meta }),
64
+ });
65
+ const queryOptions=opts.queryOptions;
66
+
67
+ // Inject a parent filter for nested resources.
68
+ // Consume only parentParams parsed from the path, such as teamId in /teams/123/users,
69
+ // instead of query params such as ?tenantId=1 being silently converted into filters.
97
70
  //
98
- // 安全边界:仅在 useList 未显式指定 resource(回退到 parsed.resource),
99
- // 或显式 resource 与当前路由资源一致时注入。
100
- const parentFilters: typeof explicitFilters = [];
101
- const injectParent = !opts.resource || opts.resource === parsed.resource;
102
- if (injectParent && parsed.parentParams) {
103
- for (const [paramKey, paramVal] of Object.entries(parsed.parentParams)) {
104
- if (paramKey.endsWith('Id') && paramVal) {
105
- parentFilters.push({ field: paramKey, operator: 'eq' as const, value: paramVal });
71
+ // Security boundary: inject only when useList falls back to parsed.resource,
72
+ // or when the explicit resource matches the current route resource.
73
+ const parentFilters: Filter[]=[];
74
+ const injectParent=!opts.resource||opts.resource===parsed.resource;
75
+ if(injectParent&&parsed.parentParams) {
76
+ for(const [paramKey,paramVal] of Object.entries(parsed.parentParams)) {
77
+ if(paramKey.endsWith('Id')&&paramVal) {
78
+ parentFilters.push({ field: paramKey,operator: 'eq',value: paramVal });
106
79
  }
107
80
  }
108
81
  }
109
- const filters = [...parentFilters, ...(explicitFilters ?? [])];
82
+ const params=snapshotListParams({
83
+ resource,
84
+ ...definedOptions({
85
+ pagination: opts.pagination,sorters: opts.sorters,
86
+ filters: [...parentFilters,...(opts.filters??[])],meta: captured.meta,
87
+ }),
88
+ });
89
+ const decodeResult=(value: unknown): GetListResult<TData> => {
90
+ const result=decodeListResult(value,decode);
91
+ if(checkId && new Set(result.data.map(row => row['id'])).size!==result.data.length) return rejectProviderResponse();
92
+ return result;
93
+ };
110
94
 
111
95
  return {
112
- queryKey: adminContext.queryKeys(resource, opts.dataProviderName).data.list(resource, {
113
- pagination,
114
- sorters,
115
- filters,
116
- meta,
96
+ resource, successNotification: opts.successNotification, errorNotification: opts.errorNotification,
97
+ decode: decodeResult,
98
+ queryKey: adminContext.queryKeys(resource,opts.dataProviderName).data.list(resource,{
99
+ ...definedOptions({
100
+ pagination: params.pagination,sorters: params.sorters,filters: params.filters,meta: params.meta,
101
+ }),
102
+ source: captured.source,
117
103
  }),
118
- queryFn: async () => provider.getList<TData>({
119
- resource,
120
- pagination,
121
- sorters,
122
- filters,
123
- meta,
104
+ queryFn: async (signal) => {
105
+ const current = captureQueryProvider(adminContext, {
106
+ resource, ...definedOptions({ dataProviderName: opts.dataProviderName, contract: opts.contract, meta: opts.meta, fresh: true }),
107
+ });
108
+ return decodeResult(await current.provider.getList(snapshotListParams({ ...params, ...definedOptions({ signal }) })));
109
+ },
110
+ enabled: queryOptions?.enabled??true,
111
+ ...definedOptions({
112
+ staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
113
+ gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
114
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
124
115
  }),
125
- enabled: queryOptions?.enabled ?? true,
126
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
127
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
128
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
129
116
  };
130
117
  });
131
118
 
132
- const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
119
+ const overtime=createOvertimeTracker(() => query.isLoading,typeof optionsOrGetter==='function'? optionsOrGetter().overtimeOptions:optionsOrGetter.overtimeOptions??adminOptions.overtime);
133
120
 
134
121
  createLiveSubscription((): LiveSubscriptionParams => {
135
- const opts = getOptions();
122
+ const opts=getOptions();
136
123
  return {
137
124
  resource: getResource(),
138
- liveProvider: adminContext.liveProvider,
139
- liveMode: opts.liveMode ?? adminOptions.liveMode,
140
- onLiveEvent: (e: LiveEvent) => {
141
- opts.onLiveEvent?.(e);
142
- adminOptions.onLiveEvent?.(e);
143
- },
144
- liveParams: opts.liveParams,
145
- enabled: opts.queryOptions?.enabled ?? true,
146
- dataProviderName: opts.dataProviderName,
125
+ ...definedOptions({
126
+ liveProvider: adminContext.liveProvider,
127
+ liveMode: opts.liveMode??adminOptions.liveMode,
128
+ contract: opts.contract,
129
+ onLiveEvent: opts.onLiveEvent,
130
+ onGlobalLiveEvent: adminOptions.onLiveEvent,
131
+ }),
132
+ ...definedOptions({ liveParams: opts.liveParams }),
133
+ enabled: opts.queryOptions?.enabled??true,
134
+ ...definedOptions({ dataProviderName: opts.dataProviderName }),
147
135
  };
148
136
  });
149
137
 
150
- // Track last notification timestamps to prevent duplicate firings on re-render
151
- let lastSuccessAt = 0;
152
- let lastErrorAt = 0;
153
- $effect(() => {
154
- const opts = getOptions();
155
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
156
- lastSuccessAt = query.dataUpdatedAt;
157
- if (opts.successNotification) {
158
- fireSuccessNotification({
159
- config: opts.successNotification,
160
- defaultMessage: '',
161
- data: query.data,
162
- resource: getResource(),
163
- provider: adminContext.notificationProvider,
164
- });
165
- }
166
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
167
- lastErrorAt = query.errorUpdatedAt;
168
- checkError(query.error, adminContext);
169
- fireErrorNotification({
170
- config: opts.errorNotification,
171
- defaultMessage: 'Fetch failed',
172
- error: query.error,
173
- resource: getResource(),
174
- provider: adminContext.notificationProvider,
175
- });
176
- }
177
- });
178
-
179
- return extendQuery(query, { overtime });
138
+ return extendReactiveMembers(query,{ overtime });
180
139
  }
181
140
 
182
141
  // ─── useOne ────────────────────────────────────────────────────
183
142
 
184
- export interface UseOneOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
143
+ export interface UseOneOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> extends ContractOptions {
185
144
  resource?: KnownResources;
186
- id?: string | number;
187
- meta?: Record<string, unknown>;
145
+ id?: string|number;
146
+ meta?: Record<string,unknown>;
188
147
  dataProviderName?: string;
189
148
  queryOptions?: { staleTime?: number; enabled?: boolean; gcTime?: number; refetchOnWindowFocus?: boolean };
190
149
  successNotification?: NotificationConfig;
191
150
  errorNotification?: NotificationConfig;
192
151
  liveMode?: LiveMode;
193
152
  onLiveEvent?: (event: LiveEvent) => void;
194
- liveParams?: Record<string, unknown>;
153
+ liveParams?: Record<string,unknown>;
195
154
  overtimeOptions?: OvertimeOptions;
196
155
  }
197
156
 
198
- export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
199
- const adminContext = captureAdminContext();
200
- const parsed = useParsed();
201
- const adminOptions = getAdminOptions();
202
-
203
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
204
- const getResource = () => getOptions().resource ?? parsed.resource ?? '';
205
- const getId = () => getOptions().id ?? parsed.id;
206
-
207
- const query = createQuery<GetOneResult<TData>, TError>(() => {
208
- const opts = getOptions();
209
- const resource = getResource();
210
- const id = getId();
211
- const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
212
- const queryOptions = opts.queryOptions;
157
+ export function useOne(optionsOrGetter: MaybeGetter<UseOneOptions>={}) {
158
+ return createOneQuery(optionsOrGetter, () => decodeBaseRecord);
159
+ }
160
+
161
+ export function createOneQuery<TData extends BaseRecord>(
162
+ optionsOrGetter: MaybeGetter<UseOneOptions>, getDecoder: () => RecordDecoder<TData>,
163
+ ) {
164
+ const adminContext=captureContractContext(optionsOrGetter);
165
+ const parsed=useParsed();
166
+ const adminOptions=getAdminOptions();
167
+
168
+ const getOptions=() => typeof optionsOrGetter==='function'? optionsOrGetter():optionsOrGetter;
169
+ const getResource=() => getOptions().resource??parsed.resource??'';
170
+ const getId=() => getOptions().id??parsed.id;
171
+
172
+ const query=createSessionQuery<GetOneResult<TData>>(adminContext,() => {
173
+ const decode=getDecoder();
174
+ const opts=getOptions();
175
+ const resource=opts.resource??parsed.resource??'';
176
+ const id=opts.id??parsed.id;
177
+ const checkId=opts.contract!==undefined;
178
+ const captured=captureQueryProvider(adminContext,{
179
+ resource,...definedOptions({ dataProviderName: opts.dataProviderName,contract: opts.contract,meta: opts.meta }),
180
+ });
181
+ const params=id==null? undefined:snapshotOneParams({
182
+ resource,id,...definedOptions({ meta: captured.meta }),
183
+ });
184
+ const queryOptions=opts.queryOptions;
213
185
 
214
186
  return {
215
- queryKey: adminContext.queryKeys(resource, opts.dataProviderName).data.one(resource, id ?? '', {
216
- meta: cloneQueryKeyPart(opts.meta),
187
+ resource, successNotification: opts.successNotification, errorNotification: opts.errorNotification,
188
+ decode: value => {
189
+ const decoded=decodeOneResult(value,decode);
190
+ if(checkId && params && decoded.data['id']!==params.id) return rejectProviderResponse();
191
+ return decoded;
192
+ },
193
+ queryKey: adminContext.queryKeys(resource,opts.dataProviderName).data.one(resource,id??'',{
194
+ ...definedOptions({ meta: captured.meta }),
195
+ source: captured.source,
217
196
  }),
218
- queryFn: async () => {
219
- if (id == null) throw new Error('useOne requires an id');
220
- const result = await provider.getOne<TData>({
221
- resource,
222
- id,
223
- meta: opts.meta,
224
- });
225
- return result;
197
+ queryFn: async (signal) => {
198
+ if(!params) throw new HttpError('Reading a record requires an id',422,undefined,{ code: 'INVALID_RESOURCE_INPUT' });
199
+ const result=await captured.provider.getOne(snapshotOneParams({ ...params, ...definedOptions({ signal }) }));
200
+ const decoded=decodeOneResult(result, decode);
201
+ if(checkId && decoded.data['id']!==params.id) return rejectProviderResponse();
202
+ return decoded;
226
203
  },
227
- enabled: (queryOptions?.enabled ?? true) && id != null,
228
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
229
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
230
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
204
+ enabled: (queryOptions?.enabled??true)&&id!=null,
205
+ ...definedOptions({
206
+ staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
207
+ gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
208
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
209
+ }),
231
210
  };
232
211
  });
233
212
 
234
- const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
213
+ const overtime=createOvertimeTracker(() => query.isLoading,typeof optionsOrGetter==='function'? optionsOrGetter().overtimeOptions:optionsOrGetter.overtimeOptions??adminOptions.overtime);
235
214
 
236
215
  createLiveSubscription((): LiveSubscriptionParams => {
237
- const opts = getOptions();
238
- const resource = getResource();
216
+ const opts=getOptions();
217
+ const resource=getResource();
239
218
  return {
240
219
  resource,
241
- liveProvider: adminContext.liveProvider,
242
- liveMode: opts.liveMode ?? adminOptions.liveMode,
243
- onLiveEvent: (e: LiveEvent) => {
244
- opts.onLiveEvent?.(e);
245
- adminOptions.onLiveEvent?.(e);
246
- },
247
- liveParams: opts.liveParams,
248
- enabled: (opts.queryOptions?.enabled ?? true) && getId() != null,
249
- dataProviderName: opts.dataProviderName,
220
+ ...definedOptions({
221
+ liveProvider: adminContext.liveProvider,
222
+ liveMode: opts.liveMode??adminOptions.liveMode,
223
+ contract: opts.contract,
224
+ onLiveEvent: opts.onLiveEvent,
225
+ onGlobalLiveEvent: adminOptions.onLiveEvent,
226
+ }),
227
+ ...definedOptions({ liveParams: opts.liveParams }),
228
+ enabled: (opts.queryOptions?.enabled??true)&&getId()!=null,
229
+ ...definedOptions({ dataProviderName: opts.dataProviderName }),
250
230
  };
251
231
  });
252
232
 
253
- let lastSuccessAt = 0;
254
- let lastErrorAt = 0;
255
- $effect(() => {
256
- const opts = getOptions();
257
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
258
- lastSuccessAt = query.dataUpdatedAt;
259
- if (opts.successNotification) {
260
- fireSuccessNotification({
261
- config: opts.successNotification,
262
- defaultMessage: '',
263
- data: query.data,
264
- resource: getResource(),
265
- provider: adminContext.notificationProvider,
266
- });
267
- }
268
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
269
- lastErrorAt = query.errorUpdatedAt;
270
- checkError(query.error, adminContext);
271
- fireErrorNotification({
272
- config: opts.errorNotification,
273
- defaultMessage: 'Fetch failed',
274
- error: query.error,
275
- resource: getResource(),
276
- provider: adminContext.notificationProvider,
277
- });
278
- }
279
- });
280
-
281
- return extendQuery(query, { overtime });
233
+ return extendReactiveMembers(query,{ overtime });
282
234
  }
283
235
 
284
236
  // ─── useShow ──────────────────────────────────────────────────
285
- export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
286
- optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}
237
+ export function createShowQuery<TData extends BaseRecord>(
238
+ optionsOrGetter: MaybeGetter<UseOneOptions>, getDecoder: () => RecordDecoder<TData>,
287
239
  ) {
288
- const parsed = useParsed();
289
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
290
-
291
- let _showId = $state<string | number | undefined>(undefined);
292
-
240
+ const parsed=useParsed();
241
+ const getOptions=() => typeof optionsOrGetter==='function'? optionsOrGetter():optionsOrGetter;
242
+
243
+ let _showId=$state<string|number|undefined>(undefined);
244
+
293
245
  // showId prioritizes explicit setShowId, then options.id, then URL id.
294
- const showId = $derived(_showId ?? getOptions().id ?? parsed.id);
246
+ const showId=$derived(_showId??getOptions().id??parsed.id);
295
247
 
296
- function setShowId(id: string | number) { _showId = id; }
248
+ function setShowId(id: string|number) { _showId=id; }
297
249
 
298
250
  // We wrap it in a getter so useOne evaluates it dynamically.
299
- const result = useOne<TData, TError>(() => ({
251
+ const result=createOneQuery(() => ({
300
252
  ...getOptions(),
301
- id: showId,
302
- }));
253
+ ...definedOptions({ id: showId }),
254
+ }), getDecoder);
303
255
 
304
- return extendQuery(result, { get showId() { return showId; }, setShowId });
256
+ return extendReactiveMembers(result,{ get showId() { return showId; },setShowId });
305
257
  }
306
258
 
307
259
  // ─── useMany ───────────────────────────────────────────────────
308
260
 
309
- export interface UseManyOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
261
+ export interface UseManyOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> extends ContractOptions {
310
262
  resource: KnownResources;
311
- ids: (string | number)[];
312
- meta?: Record<string, unknown>;
263
+ ids: (string|number)[];
264
+ meta?: Record<string,unknown>;
313
265
  dataProviderName?: string;
314
266
  queryOptions?: { staleTime?: number; enabled?: boolean; gcTime?: number; refetchOnWindowFocus?: boolean };
315
267
  successNotification?: NotificationConfig;
316
268
  errorNotification?: NotificationConfig;
317
269
  liveMode?: LiveMode;
318
270
  onLiveEvent?: (event: LiveEvent) => void;
319
- liveParams?: Record<string, unknown>;
271
+ liveParams?: Record<string,unknown>;
320
272
  overtimeOptions?: OvertimeOptions;
321
273
  }
322
274
 
323
- export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
324
- const adminContext = captureAdminContext();
325
- const adminOptions = getAdminOptions();
326
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
275
+ export function useMany(optionsOrGetter: MaybeGetter<UseManyOptions>) {
276
+ return createManyQuery(optionsOrGetter, () => decodeBaseRecord);
277
+ }
327
278
 
328
- const query = createQuery<GetManyResult<TData>, TError>(() => {
329
- const opts = getOptions();
330
- const { resource, ids, meta, dataProviderName, queryOptions } = opts;
331
- const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
279
+ export function createManyQuery<TData extends BaseRecord>(
280
+ optionsOrGetter: MaybeGetter<UseManyOptions>, getDecoder: () => RecordDecoder<TData>,
281
+ ) {
282
+ const adminContext=captureContractContext(optionsOrGetter);
283
+ const adminOptions=getAdminOptions();
284
+ const getOptions=() => typeof optionsOrGetter==='function'? optionsOrGetter():optionsOrGetter;
285
+
286
+ const query=createSessionQuery<GetManyResult<TData>>(adminContext,() => {
287
+ const decode=getDecoder();
288
+ const opts=getOptions();
289
+ const checkId=opts.contract!==undefined;
290
+ const { resource,ids,meta,dataProviderName,queryOptions }=opts;
291
+ const captured=captureQueryProvider(adminContext,{
292
+ resource,...definedOptions({ dataProviderName,contract: opts.contract,meta }),
293
+ });
294
+ const params=snapshotManyParams({
295
+ resource,ids,...definedOptions({ meta: captured.meta }),
296
+ });
297
+ const expected=new Set<unknown>(params.ids);
298
+ const decodeResult=(value: unknown): GetManyResult<TData> => {
299
+ const result=decodeManyResult(value,decode);
300
+ if(checkId && (expected.size!==params.ids.length || result.data.length!==expected.size ||
301
+ new Set(result.data.map(row => row['id'])).size!==result.data.length ||
302
+ result.data.some(row => !expected.has(row['id'])))) return rejectProviderResponse();
303
+ return result;
304
+ };
332
305
 
333
306
  return {
334
- queryKey: adminContext.queryKeys(resource, dataProviderName).data.many(resource, {
335
- ids: cloneQueryKeyPart(ids),
336
- meta: cloneQueryKeyPart(meta),
307
+ resource, successNotification: opts.successNotification, errorNotification: opts.errorNotification,
308
+ decode: decodeResult,
309
+ queryKey: adminContext.queryKeys(resource,dataProviderName).data.many(resource,{
310
+ ids: params.ids,
311
+ ...definedOptions({ meta: params.meta }),
312
+ source: captured.source,
337
313
  }),
338
- queryFn: async () => {
339
- if (!ids.length) return { data: [] };
340
- if (provider.getMany) {
341
- return provider.getMany<TData>({ resource, ids, meta });
314
+ queryFn: async (signal) => {
315
+ if(!params.ids.length) return { data: [] };
316
+ if(captured.provider.getMany) {
317
+ return decodeResult(await captured.provider.getMany(snapshotManyParams({ ...params, ...definedOptions({ signal }) })));
342
318
  }
343
- const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta })));
344
- return { data: results.map(r => r.data) };
319
+ const results=await Promise.all(params.ids.map(async id => decodeOneResult(await captured.provider.getOne(snapshotOneParams({
320
+ resource,...definedOptions({
321
+ id,meta: params.meta,signal
322
+ })
323
+ })),decode)));
324
+ return decodeResult({ data: results.map(r => r.data) });
345
325
  },
346
- enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
347
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
348
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
349
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
326
+ enabled: (queryOptions?.enabled??true)&&params.ids.length>0,
327
+ ...definedOptions({
328
+ staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
329
+ gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
330
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
331
+ }),
350
332
  };
351
333
  });
352
334
 
353
- const overtime = createOvertimeTracker(() => query.isLoading, typeof optionsOrGetter === 'function' ? optionsOrGetter().overtimeOptions : optionsOrGetter.overtimeOptions ?? adminOptions.overtime);
335
+ const overtime=createOvertimeTracker(() => query.isLoading,typeof optionsOrGetter==='function'? optionsOrGetter().overtimeOptions:optionsOrGetter.overtimeOptions??adminOptions.overtime);
354
336
 
355
337
  createLiveSubscription((): LiveSubscriptionParams => {
356
- const opts = getOptions();
338
+ const opts=getOptions();
357
339
  return {
358
340
  resource: opts.resource,
359
- liveProvider: adminContext.liveProvider,
360
- liveMode: opts.liveMode ?? adminOptions.liveMode,
361
- onLiveEvent: (e: LiveEvent) => {
362
- opts.onLiveEvent?.(e);
363
- adminOptions.onLiveEvent?.(e);
364
- },
365
- liveParams: opts.liveParams,
366
- enabled: (opts.queryOptions?.enabled ?? true) && opts.ids.length > 0,
367
- dataProviderName: opts.dataProviderName,
341
+ ...definedOptions({
342
+ liveProvider: adminContext.liveProvider,
343
+ liveMode: opts.liveMode??adminOptions.liveMode,
344
+ contract: opts.contract,
345
+ onLiveEvent: opts.onLiveEvent,
346
+ onGlobalLiveEvent: adminOptions.onLiveEvent,
347
+ }),
348
+ ...definedOptions({ liveParams: opts.liveParams }),
349
+ enabled: (opts.queryOptions?.enabled??true)&&opts.ids.length>0,
350
+ ...definedOptions({ dataProviderName: opts.dataProviderName }),
368
351
  };
369
352
  });
370
353
 
371
- let lastSuccessAt = 0;
372
- let lastErrorAt = 0;
373
- $effect(() => {
374
- const opts = getOptions();
375
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
376
- lastSuccessAt = query.dataUpdatedAt;
377
- if (opts.successNotification) {
378
- fireSuccessNotification({
379
- config: opts.successNotification,
380
- defaultMessage: '',
381
- data: query.data,
382
- resource: opts.resource,
383
- provider: adminContext.notificationProvider,
384
- });
385
- }
386
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
387
- lastErrorAt = query.errorUpdatedAt;
388
- checkError(query.error, adminContext);
389
- fireErrorNotification({
390
- config: opts.errorNotification,
391
- defaultMessage: 'Fetch failed',
392
- error: query.error,
393
- resource: opts.resource,
394
- provider: adminContext.notificationProvider,
395
- });
396
- }
397
- });
398
-
399
- return extendQuery(query, { overtime });
354
+ return extendReactiveMembers(query,{ overtime });
400
355
  }
401
356
 
402
357
  export function useApiUrl(dataProviderName?: string): string {