@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,404 +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 ({ signal }) => provider.getList<TData>({
119
- signal,
120
- resource,
121
- pagination,
122
- sorters,
123
- filters,
124
- 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,
125
115
  }),
126
- enabled: queryOptions?.enabled ?? true,
127
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
128
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
129
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
130
116
  };
131
117
  });
132
118
 
133
- 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);
134
120
 
135
121
  createLiveSubscription((): LiveSubscriptionParams => {
136
- const opts = getOptions();
122
+ const opts=getOptions();
137
123
  return {
138
124
  resource: getResource(),
139
- liveProvider: adminContext.liveProvider,
140
- liveMode: opts.liveMode ?? adminOptions.liveMode,
141
- onLiveEvent: (e: LiveEvent) => {
142
- opts.onLiveEvent?.(e);
143
- adminOptions.onLiveEvent?.(e);
144
- },
145
- liveParams: opts.liveParams,
146
- enabled: opts.queryOptions?.enabled ?? true,
147
- 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 }),
148
135
  };
149
136
  });
150
137
 
151
- // Track last notification timestamps to prevent duplicate firings on re-render
152
- let lastSuccessAt = 0;
153
- let lastErrorAt = 0;
154
- $effect(() => {
155
- const opts = getOptions();
156
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
157
- lastSuccessAt = query.dataUpdatedAt;
158
- if (opts.successNotification) {
159
- fireSuccessNotification({
160
- config: opts.successNotification,
161
- defaultMessage: '',
162
- data: query.data,
163
- resource: getResource(),
164
- provider: adminContext.notificationProvider,
165
- });
166
- }
167
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
168
- lastErrorAt = query.errorUpdatedAt;
169
- checkError(query.error, adminContext);
170
- fireErrorNotification({
171
- config: opts.errorNotification,
172
- defaultMessage: 'Fetch failed',
173
- error: query.error,
174
- resource: getResource(),
175
- provider: adminContext.notificationProvider,
176
- });
177
- }
178
- });
179
-
180
- return extendQuery(query, { overtime });
138
+ return extendReactiveMembers(query,{ overtime });
181
139
  }
182
140
 
183
141
  // ─── useOne ────────────────────────────────────────────────────
184
142
 
185
- export interface UseOneOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
143
+ export interface UseOneOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> extends ContractOptions {
186
144
  resource?: KnownResources;
187
- id?: string | number;
188
- meta?: Record<string, unknown>;
145
+ id?: string|number;
146
+ meta?: Record<string,unknown>;
189
147
  dataProviderName?: string;
190
148
  queryOptions?: { staleTime?: number; enabled?: boolean; gcTime?: number; refetchOnWindowFocus?: boolean };
191
149
  successNotification?: NotificationConfig;
192
150
  errorNotification?: NotificationConfig;
193
151
  liveMode?: LiveMode;
194
152
  onLiveEvent?: (event: LiveEvent) => void;
195
- liveParams?: Record<string, unknown>;
153
+ liveParams?: Record<string,unknown>;
196
154
  overtimeOptions?: OvertimeOptions;
197
155
  }
198
156
 
199
- export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
200
- const adminContext = captureAdminContext();
201
- const parsed = useParsed();
202
- const adminOptions = getAdminOptions();
203
-
204
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
205
- const getResource = () => getOptions().resource ?? parsed.resource ?? '';
206
- const getId = () => getOptions().id ?? parsed.id;
207
-
208
- const query = createQuery<GetOneResult<TData>, TError>(() => {
209
- const opts = getOptions();
210
- const resource = getResource();
211
- const id = getId();
212
- const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
213
- 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;
214
185
 
215
186
  return {
216
- queryKey: adminContext.queryKeys(resource, opts.dataProviderName).data.one(resource, id ?? '', {
217
- 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,
218
196
  }),
219
- queryFn: async ({ signal }) => {
220
- if (id == null) throw new Error('useOne requires an id');
221
- const result = await provider.getOne<TData>({
222
- signal,
223
- resource,
224
- id,
225
- meta: opts.meta,
226
- });
227
- 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;
228
203
  },
229
- enabled: (queryOptions?.enabled ?? true) && id != null,
230
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
231
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
232
- 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
+ }),
233
210
  };
234
211
  });
235
212
 
236
- 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);
237
214
 
238
215
  createLiveSubscription((): LiveSubscriptionParams => {
239
- const opts = getOptions();
240
- const resource = getResource();
216
+ const opts=getOptions();
217
+ const resource=getResource();
241
218
  return {
242
219
  resource,
243
- liveProvider: adminContext.liveProvider,
244
- liveMode: opts.liveMode ?? adminOptions.liveMode,
245
- onLiveEvent: (e: LiveEvent) => {
246
- opts.onLiveEvent?.(e);
247
- adminOptions.onLiveEvent?.(e);
248
- },
249
- liveParams: opts.liveParams,
250
- enabled: (opts.queryOptions?.enabled ?? true) && getId() != null,
251
- 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 }),
252
230
  };
253
231
  });
254
232
 
255
- let lastSuccessAt = 0;
256
- let lastErrorAt = 0;
257
- $effect(() => {
258
- const opts = getOptions();
259
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
260
- lastSuccessAt = query.dataUpdatedAt;
261
- if (opts.successNotification) {
262
- fireSuccessNotification({
263
- config: opts.successNotification,
264
- defaultMessage: '',
265
- data: query.data,
266
- resource: getResource(),
267
- provider: adminContext.notificationProvider,
268
- });
269
- }
270
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
271
- lastErrorAt = query.errorUpdatedAt;
272
- checkError(query.error, adminContext);
273
- fireErrorNotification({
274
- config: opts.errorNotification,
275
- defaultMessage: 'Fetch failed',
276
- error: query.error,
277
- resource: getResource(),
278
- provider: adminContext.notificationProvider,
279
- });
280
- }
281
- });
282
-
283
- return extendQuery(query, { overtime });
233
+ return extendReactiveMembers(query,{ overtime });
284
234
  }
285
235
 
286
236
  // ─── useShow ──────────────────────────────────────────────────
287
- export function useShow<TData extends BaseRecord = BaseRecord, TError = HttpError>(
288
- optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}
237
+ export function createShowQuery<TData extends BaseRecord>(
238
+ optionsOrGetter: MaybeGetter<UseOneOptions>, getDecoder: () => RecordDecoder<TData>,
289
239
  ) {
290
- const parsed = useParsed();
291
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
292
-
293
- let _showId = $state<string | number | undefined>(undefined);
294
-
240
+ const parsed=useParsed();
241
+ const getOptions=() => typeof optionsOrGetter==='function'? optionsOrGetter():optionsOrGetter;
242
+
243
+ let _showId=$state<string|number|undefined>(undefined);
244
+
295
245
  // showId prioritizes explicit setShowId, then options.id, then URL id.
296
- const showId = $derived(_showId ?? getOptions().id ?? parsed.id);
246
+ const showId=$derived(_showId??getOptions().id??parsed.id);
297
247
 
298
- function setShowId(id: string | number) { _showId = id; }
248
+ function setShowId(id: string|number) { _showId=id; }
299
249
 
300
250
  // We wrap it in a getter so useOne evaluates it dynamically.
301
- const result = useOne<TData, TError>(() => ({
251
+ const result=createOneQuery(() => ({
302
252
  ...getOptions(),
303
- id: showId,
304
- }));
253
+ ...definedOptions({ id: showId }),
254
+ }), getDecoder);
305
255
 
306
- return extendQuery(result, { get showId() { return showId; }, setShowId });
256
+ return extendReactiveMembers(result,{ get showId() { return showId; },setShowId });
307
257
  }
308
258
 
309
259
  // ─── useMany ───────────────────────────────────────────────────
310
260
 
311
- export interface UseManyOptions<_TData extends BaseRecord = BaseRecord, _TError = HttpError> {
261
+ export interface UseManyOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> extends ContractOptions {
312
262
  resource: KnownResources;
313
- ids: (string | number)[];
314
- meta?: Record<string, unknown>;
263
+ ids: (string|number)[];
264
+ meta?: Record<string,unknown>;
315
265
  dataProviderName?: string;
316
266
  queryOptions?: { staleTime?: number; enabled?: boolean; gcTime?: number; refetchOnWindowFocus?: boolean };
317
267
  successNotification?: NotificationConfig;
318
268
  errorNotification?: NotificationConfig;
319
269
  liveMode?: LiveMode;
320
270
  onLiveEvent?: (event: LiveEvent) => void;
321
- liveParams?: Record<string, unknown>;
271
+ liveParams?: Record<string,unknown>;
322
272
  overtimeOptions?: OvertimeOptions;
323
273
  }
324
274
 
325
- export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
326
- const adminContext = captureAdminContext();
327
- const adminOptions = getAdminOptions();
328
- const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
275
+ export function useMany(optionsOrGetter: MaybeGetter<UseManyOptions>) {
276
+ return createManyQuery(optionsOrGetter, () => decodeBaseRecord);
277
+ }
329
278
 
330
- const query = createQuery<GetManyResult<TData>, TError>(() => {
331
- const opts = getOptions();
332
- const { resource, ids, meta, dataProviderName, queryOptions } = opts;
333
- 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
+ };
334
305
 
335
306
  return {
336
- queryKey: adminContext.queryKeys(resource, dataProviderName).data.many(resource, {
337
- ids: cloneQueryKeyPart(ids),
338
- 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,
339
313
  }),
340
- queryFn: async ({ signal }) => {
341
- if (!ids.length) return { data: [] };
342
- if (provider.getMany) {
343
- return provider.getMany<TData>({ resource, ids, meta, signal });
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 }) })));
344
318
  }
345
- const results = await Promise.all(ids.map(id => provider.getOne<TData>({ resource, id, meta, signal })));
346
- 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) });
347
325
  },
348
- enabled: (queryOptions?.enabled ?? true) && ids.length > 0,
349
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
350
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
351
- 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
+ }),
352
332
  };
353
333
  });
354
334
 
355
- 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);
356
336
 
357
337
  createLiveSubscription((): LiveSubscriptionParams => {
358
- const opts = getOptions();
338
+ const opts=getOptions();
359
339
  return {
360
340
  resource: opts.resource,
361
- liveProvider: adminContext.liveProvider,
362
- liveMode: opts.liveMode ?? adminOptions.liveMode,
363
- onLiveEvent: (e: LiveEvent) => {
364
- opts.onLiveEvent?.(e);
365
- adminOptions.onLiveEvent?.(e);
366
- },
367
- liveParams: opts.liveParams,
368
- enabled: (opts.queryOptions?.enabled ?? true) && opts.ids.length > 0,
369
- 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 }),
370
351
  };
371
352
  });
372
353
 
373
- let lastSuccessAt = 0;
374
- let lastErrorAt = 0;
375
- $effect(() => {
376
- const opts = getOptions();
377
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
378
- lastSuccessAt = query.dataUpdatedAt;
379
- if (opts.successNotification) {
380
- fireSuccessNotification({
381
- config: opts.successNotification,
382
- defaultMessage: '',
383
- data: query.data,
384
- resource: opts.resource,
385
- provider: adminContext.notificationProvider,
386
- });
387
- }
388
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
389
- lastErrorAt = query.errorUpdatedAt;
390
- checkError(query.error, adminContext);
391
- fireErrorNotification({
392
- config: opts.errorNotification,
393
- defaultMessage: 'Fetch failed',
394
- error: query.error,
395
- resource: opts.resource,
396
- provider: adminContext.notificationProvider,
397
- });
398
- }
399
- });
400
-
401
- return extendQuery(query, { overtime });
354
+ return extendReactiveMembers(query,{ overtime });
402
355
  }
403
356
 
404
357
  export function useApiUrl(dataProviderName?: string): string {