@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
@@ -0,0 +1,216 @@
1
+ import { hashKey, useQueryClient, type QueryObserverPendingResult, type QueryObserverResult, type RefetchOptions } from '@tanstack/svelte-query';
2
+ import { untrack } from 'svelte';
3
+ import { Type } from '@sinclair/typebox';
4
+ import type { AdminContextAccessor } from './context.svelte';
5
+ import { captureAuthLiveScope, clearAuthQueries, handleAuthError } from './auth-hooks.svelte';
6
+ import { decodeBaseRecord } from './record-decoder';
7
+ import { HttpError } from './types';
8
+ import type { QueryKey } from './query-keys';
9
+ import { definedOptions } from './defined-options';
10
+ import type { NotificationConfig } from './hook-utils.svelte';
11
+ import { notifyWithProvider } from './notification.svelte';
12
+ import { snapshotPlainData } from './plain-data';
13
+ import { checkExact } from './schema-validation';
14
+
15
+ const notificationSchema = Type.Object({
16
+ message: Type.String(), description: Type.Optional(Type.String()), key: Type.Optional(Type.String()),
17
+ type: Type.Optional(Type.Union([Type.Literal('success'), Type.Literal('error')])),
18
+ }, { additionalProperties: false });
19
+
20
+ export interface QuerySessionOptions {
21
+ queryKey: QueryKey;
22
+ resource: string;
23
+ successNotification: NotificationConfig;
24
+ errorNotification: NotificationConfig;
25
+ /** Extra observer lifetime constraints; must not include auth retirement during delegated logout. */
26
+ isTargetCurrent?: () => boolean;
27
+ /** Allow a shared query function to delegate work to another live observer. */
28
+ allowRetiredQueryFn?: boolean;
29
+ }
30
+
31
+ export function supersededQuerySession(): HttpError {
32
+ return new HttpError('Query session is no longer current', 409, undefined, { code: 'QUERY_SESSION_SUPERSEDED' });
33
+ }
34
+
35
+ /** Retain only protocol status and known local codes, never provider diagnostics. */
36
+ export function querySessionError(error: unknown): HttpError {
37
+ let status = 502;
38
+ let code = 'QUERY_FAILED';
39
+ try {
40
+ if (typeof error === 'object' && error !== null) {
41
+ const statusValue: unknown = Object.getOwnPropertyDescriptor(error, 'statusCode')?.value;
42
+ const codeValue: unknown = Object.getOwnPropertyDescriptor(error, 'code')?.value;
43
+ if (typeof statusValue === 'number' && Number.isInteger(statusValue) && statusValue >= 400 && statusValue <= 599) status = statusValue;
44
+ if (codeValue === 'INVALID_PROVIDER_RESPONSE' || codeValue === 'INVALID_RESOURCE_INPUT' ||
45
+ codeValue === 'DATA_PROVIDER_REQUIRED' || codeValue === 'QUERY_SESSION_SUPERSEDED' ||
46
+ codeValue === 'INVALID_SELECT_OPTION' || codeValue === 'INVALID_COMMAND_INPUT' ||
47
+ codeValue === 'INVALID_COMMAND_RESPONSE' || codeValue === 'INVALID_COMMAND_CONTRACT' ||
48
+ codeValue === 'INVALID_COMMAND_METHOD' || codeValue === 'COMMAND_NOT_SUPPORTED' ||
49
+ codeValue === 'COMMAND_FAILED' || codeValue === 'INVALID_ACCESS_CONTROL_INPUT' ||
50
+ codeValue === 'INVALID_ACCESS_CONTROL_RESPONSE' || codeValue === 'INVALID_ACCESS_CONTROL_PROVIDER') code = codeValue;
51
+ }
52
+ } catch {
53
+ // Hostile reflection is not an authentication instruction.
54
+ }
55
+ return new HttpError('Resource query failed', status, undefined, { code });
56
+ }
57
+
58
+ interface QueryObservation {
59
+ isSuccess: boolean;
60
+ isError: boolean;
61
+ data: unknown;
62
+ error: unknown;
63
+ dataUpdatedAt: number;
64
+ errorUpdatedAt: number;
65
+ }
66
+
67
+ /** Shared ownership for ordinary, projected and paginated readers. */
68
+ export function createQuerySession<O extends QuerySessionOptions>(context: AdminContextAccessor, getOptions: () => O) {
69
+ const client = useQueryClient();
70
+ let disposed = false;
71
+ const options = $derived.by(() => {
72
+ const input = getOptions();
73
+ const provider = context.authProvider;
74
+ const auth = captureAuthLiveScope(provider);
75
+ const descriptor = input.queryKey[0];
76
+ const queryKey: QueryKey = [{
77
+ ...descriptor, params: { ...decodeBaseRecord(descriptor.params), authSession: auth.cacheKey },
78
+ }];
79
+ return {
80
+ ...input, queryKey, auth, provider, target: hashKey(input.queryKey),
81
+ tenant: context.tenantCacheKey?.__svadminTenant,
82
+ router: context.routerProvider, notification: context.notificationProvider,
83
+ };
84
+ });
85
+ type Origin = typeof options;
86
+ const sessionCurrent = (origin: Origin) => origin.provider === context.authProvider && origin.auth.isCurrent();
87
+ const targetCurrent = (origin: Origin) => !disposed && origin.target === options.target &&
88
+ origin.provider === context.authProvider && origin.tenant === context.tenantCacheKey?.__svadminTenant &&
89
+ origin.router === context.routerProvider && (origin.isTargetCurrent === undefined || origin.isTargetCurrent() === true);
90
+ const current = (origin: Origin) => targetCurrent(origin) && sessionCurrent(origin);
91
+
92
+ function notify(origin: Origin, type: 'success' | 'error', value: unknown): void {
93
+ try {
94
+ const config = type === 'success' ? origin.successNotification : origin.errorNotification;
95
+ if (config === false || !current(origin)) return;
96
+ if (config !== undefined && typeof config !== 'function' && typeof config !== 'string') return;
97
+ const fallback = type === 'success' ? '' : 'Fetch failed: Resource query failed';
98
+ if (typeof config !== 'function' && !config && !fallback) return;
99
+ const candidate = snapshotPlainData(typeof config === 'function'
100
+ ? config(value, undefined, origin.resource) : { message: config || fallback });
101
+ if (!checkExact(notificationSchema, candidate) || !current(origin) || origin.notification !== context.notificationProvider) return;
102
+ notifyWithProvider({
103
+ type, message: candidate.message, ...definedOptions({ description: candidate.description, key: candidate.key }),
104
+ }, origin.notification);
105
+ } catch {
106
+ // Observer failures and malformed receipts cannot change query state.
107
+ }
108
+ }
109
+
110
+ function settleDetached(origin: Origin, result: { data?: unknown; error?: unknown; success: boolean }): void {
111
+ if (disposed) return;
112
+ const query = client.getQueryCache().find({ queryKey: origin.queryKey, exact: true });
113
+ if (!query || query.getObserversCount() > 0) return;
114
+ const now = Date.now();
115
+ if (result.success) {
116
+ query.setState({
117
+ data: result.data,
118
+ dataUpdatedAt: now,
119
+ dataUpdateCount: query.state.dataUpdateCount + 1,
120
+ error: null,
121
+ fetchFailureCount: 0,
122
+ fetchFailureReason: null,
123
+ fetchStatus: 'idle',
124
+ status: 'success',
125
+ });
126
+ return;
127
+ }
128
+ const error = querySessionError(result.error);
129
+ query.setState({
130
+ error,
131
+ errorUpdatedAt: now,
132
+ errorUpdateCount: query.state.errorUpdateCount + 1,
133
+ fetchFailureCount: query.state.fetchFailureCount + 1,
134
+ fetchFailureReason: error,
135
+ fetchStatus: 'idle',
136
+ status: 'error',
137
+ });
138
+ }
139
+
140
+ async function run<T>(origin: Origin, request: () => Promise<T>): Promise<T> {
141
+ const allowRetired = origin.allowRetiredQueryFn === true;
142
+ if ((!allowRetired && disposed) || !sessionCurrent(origin)) throw supersededQuerySession();
143
+ try {
144
+ const result = await request();
145
+ if ((!allowRetired && disposed) || !sessionCurrent(origin)) {
146
+ if (!disposed) settleDetached(origin, { success: false, error: supersededQuerySession() });
147
+ throw supersededQuerySession();
148
+ }
149
+ if (!current(origin)) settleDetached(origin, { success: true, data: result });
150
+ return result;
151
+ } catch (error) {
152
+ if ((!allowRetired && disposed) || !sessionCurrent(origin)) {
153
+ if (!disposed) settleDetached(origin, { success: false, error: supersededQuerySession() });
154
+ throw supersededQuerySession();
155
+ }
156
+ if (!current(origin)) settleDetached(origin, { success: false, error });
157
+ throw querySessionError(error);
158
+ }
159
+ }
160
+
161
+ function observe(getResult: (origin: Origin) => QueryObservation): void {
162
+ let lastSuccess: { key: string; at: number } | undefined;
163
+ let lastError: { key: string; at: number } | undefined;
164
+ $effect(() => {
165
+ const origin = options;
166
+ const result = getResult(origin);
167
+ const key = hashKey(origin.queryKey);
168
+ if (!current(origin)) return;
169
+ if (result.isSuccess && (lastSuccess?.key !== key || result.dataUpdatedAt > lastSuccess.at)) {
170
+ lastSuccess = { key, at: result.dataUpdatedAt };
171
+ untrack(() => notify(origin, 'success', result.data));
172
+ } else if (result.isError && (lastError?.key !== key || result.errorUpdatedAt > lastError.at)) {
173
+ lastError = { key, at: result.errorUpdatedAt };
174
+ untrack(() => {
175
+ void handleAuthError(querySessionError(result.error), context, () => targetCurrent(origin),
176
+ () => clearAuthQueries(client, origin.provider), origin.auth).catch(() => {});
177
+ notify(origin, 'error', querySessionError(result.error));
178
+ });
179
+ }
180
+ });
181
+ }
182
+ $effect(() => () => {
183
+ disposed = true;
184
+ queueMicrotask(() => {
185
+ const query = client.getQueryCache().find({ queryKey: options.queryKey, exact: true });
186
+ if (query?.getObserversCount() === 0) void query.cancel();
187
+ });
188
+ });
189
+ return { get options() { return options; }, client, current, sessionCurrent, run, observe };
190
+ }
191
+
192
+ export function pendingQueryResult<T, F extends (settings?: RefetchOptions) => Promise<unknown> = QueryObserverResult<T, unknown>['refetch']>(
193
+ refetch: F,
194
+ ): Omit<QueryObserverPendingResult<T, unknown>, 'refetch'> & { refetch: F } {
195
+ return {
196
+ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0,
197
+ failureCount: 0, failureReason: null, errorUpdateCount: 0,
198
+ isError: false, isFetched: false, isFetchedAfterMount: false,
199
+ isFetching: false, isLoading: false, isPending: true, isLoadingError: false,
200
+ isInitialLoading: false, isPaused: false, isPlaceholderData: false,
201
+ isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false,
202
+ isEnabled: false, status: 'pending', fetchStatus: 'idle', refetch,
203
+ };
204
+ }
205
+
206
+ /** Delegate reflection and property access to a fully discriminated, read-only result. */
207
+ export function createQueryResultView<R extends object>(getResult: () => R): R {
208
+ return new Proxy(getResult(), {
209
+ get: (_target, key): unknown => Reflect.get(getResult(), key),
210
+ has: (_target, key) => Reflect.has(getResult(), key),
211
+ ownKeys: () => Reflect.ownKeys(getResult()),
212
+ getOwnPropertyDescriptor: (_target, key) => Reflect.has(getResult(), key)
213
+ ? { configurable: true, enumerable: true, get: () => Reflect.get(getResult(), key) } : undefined,
214
+ set: () => false, defineProperty: () => false, deleteProperty: () => false,
215
+ });
216
+ }
@@ -0,0 +1,144 @@
1
+ import { Type, type Static, type TSchema } from '@sinclair/typebox';
2
+ import type { AdminContextAccessor } from './context.svelte';
3
+ import type { DataProvider, GetListParams, GetOneParams, GetManyParams } from './types';
4
+ import { HttpError } from './types';
5
+ import { contractProvider, type ResourceContract } from './resource-contract';
6
+ import { definedOptions } from './defined-options';
7
+ import { attachAbortSignal, detachAbortSignal, snapshotPlainData } from './plain-data';
8
+ import { checkExact } from './schema-validation';
9
+ import { decodeBaseRecord, decodeListResult, decodeOneResult, decodeManyResult, rejectProviderResponse } from './record-decoder';
10
+
11
+ const filters = Type.Recursive(self => Type.Union([
12
+ Type.Object({
13
+ field: Type.String(),
14
+ operator: Type.Union(([
15
+ 'eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'nin', 'contains', 'ncontains',
16
+ 'startswith', 'endswith', 'null', 'nnull', 'between', 'nbetween',
17
+ ] as const).map(value => Type.Literal(value))),
18
+ value: Type.Unknown(),
19
+ }, { additionalProperties: false }),
20
+ Type.Object({
21
+ operator: Type.Union([Type.Literal('and'), Type.Literal('or')]),
22
+ value: Type.Array(self),
23
+ }, { additionalProperties: false }),
24
+ ]));
25
+ const base = {
26
+ resource: Type.String({ minLength: 1 }),
27
+ meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
28
+ };
29
+ const listSchema = Type.Object({
30
+ ...base,
31
+ pagination: Type.Optional(Type.Object({
32
+ current: Type.Optional(Type.Integer({ minimum: 1 })),
33
+ pageSize: Type.Optional(Type.Integer({ minimum: 1 })),
34
+ mode: Type.Optional(Type.Union([Type.Literal('server'), Type.Literal('client'), Type.Literal('off')])),
35
+ }, { additionalProperties: false })),
36
+ sorters: Type.Optional(Type.Array(Type.Object({
37
+ field: Type.String(), order: Type.Union([Type.Literal('asc'), Type.Literal('desc')]),
38
+ }, { additionalProperties: false }))),
39
+ filters: Type.Optional(Type.Array(filters)),
40
+ }, { additionalProperties: false });
41
+ const id = Type.Union([Type.String(), Type.Number()]);
42
+ const oneSchema = Type.Object({ ...base, id }, { additionalProperties: false });
43
+ const manySchema = Type.Object({ ...base, ids: Type.Array(id) }, { additionalProperties: false });
44
+
45
+ function invalid(): never {
46
+ throw new HttpError('Invalid resource query', 422, undefined, { code: 'INVALID_RESOURCE_INPUT' });
47
+ }
48
+
49
+ function snapshotQueryParams<S extends TSchema>(schema: S, value: unknown): Static<S> {
50
+ try {
51
+ const candidate = snapshotPlainData(value);
52
+ if (checkExact(schema, candidate)) return candidate;
53
+ } catch {
54
+ // Query input errors must not disclose values or execute serialization hooks.
55
+ }
56
+ return invalid();
57
+ }
58
+
59
+ function snapshotParamsWithSignal<S extends TSchema>(schema: S, value: unknown): Static<S> {
60
+ const { signal, rest } = detachAbortSignal(value);
61
+ const params = snapshotQueryParams(schema, rest);
62
+ if (typeof params !== 'object' || params === null) return invalid();
63
+ return attachAbortSignal(params, signal);
64
+ }
65
+
66
+ export function snapshotListParams(value: unknown): GetListParams {
67
+ return snapshotParamsWithSignal(listSchema, value);
68
+ }
69
+
70
+ export function snapshotOneParams(value: unknown): GetOneParams {
71
+ return snapshotParamsWithSignal(oneSchema, value);
72
+ }
73
+
74
+ export function snapshotManyParams(value: unknown): GetManyParams {
75
+ return snapshotParamsWithSignal(manySchema, value);
76
+ }
77
+
78
+ const sourceIds = new WeakMap<DataProvider, string>();
79
+ const runtime = `${Date.now()}:${Math.random().toString(36).slice(2)}`;
80
+ let nextSource = 0;
81
+
82
+ /** Capture a contract query's transport and metadata before any deferred query work. */
83
+ export function captureQueryProvider(
84
+ context: AdminContextAccessor,
85
+ options: { resource: string; dataProviderName?: string; contract?: ResourceContract; meta?: Record<string, unknown>; fresh?: boolean },
86
+ ) {
87
+ const resource = context.resources.find(item => item.identifier === options.resource || item.name === options.resource);
88
+ const name = options.dataProviderName ?? resource?.provider?.dataProviderName ?? resource?.meta?.dataProviderName ?? 'default';
89
+ const raw = context.providers?.[name];
90
+ if (!raw) throw new HttpError('Data provider is unavailable', 400, undefined, { code: 'DATA_PROVIDER_REQUIRED' });
91
+ let source = sourceIds.get(raw);
92
+ if (source === undefined) {
93
+ source = `${runtime}:${++nextSource}`;
94
+ sourceIds.set(raw, source);
95
+ }
96
+ const explicit = snapshotListParams(definedOptions({ resource: options.resource, meta: options.meta }));
97
+ const metadata = context.getProviderMeta(options.resource, explicit.meta);
98
+ const params = snapshotListParams(definedOptions({ resource: options.resource, meta: metadata }));
99
+ if (options.contract) {
100
+ return { provider: contractProvider(raw, options.contract, { cache: options.fresh !== true }), source, meta: params.meta };
101
+ }
102
+ const provider: DataProvider = {
103
+ ...raw,
104
+ getApiUrl: () => raw.getApiUrl(),
105
+ create: input => raw.create(input),
106
+ update: input => raw.update(input),
107
+ deleteOne: input => raw.deleteOne(input),
108
+ getList: async input => {
109
+ const result = await raw.getList(input);
110
+ try {
111
+ return decodeListResult(snapshotPlainData(result), decodeBaseRecord);
112
+ } catch {
113
+ return rejectProviderResponse();
114
+ }
115
+ },
116
+ getOne: async input => {
117
+ const result = await raw.getOne(input);
118
+ try {
119
+ return decodeOneResult(snapshotPlainData(result), decodeBaseRecord);
120
+ } catch {
121
+ return rejectProviderResponse();
122
+ }
123
+ },
124
+ getMany: async input => {
125
+ if (!raw.getMany) {
126
+ const results = await Promise.all(input.ids.map(id => provider.getOne(snapshotOneParams({
127
+ resource: input.resource, id, ...definedOptions({ meta: input.meta }),
128
+ }))));
129
+ return { data: results.map(result => result.data) };
130
+ }
131
+ const result = await raw.getMany(input);
132
+ try {
133
+ return decodeManyResult(snapshotPlainData(result), decodeBaseRecord);
134
+ } catch {
135
+ return rejectProviderResponse();
136
+ }
137
+ },
138
+ };
139
+ return {
140
+ provider,
141
+ source,
142
+ meta: params.meta,
143
+ };
144
+ }
@@ -0,0 +1,53 @@
1
+ /** Project members without snapshotting reactive getters or changing the source object. */
2
+ function project(source: object, replacement: object, omitted: readonly PropertyKey[]): object {
3
+ const hidden = new Set(omitted);
4
+ return new Proxy({}, {
5
+ get(_target, key): unknown {
6
+ if (Reflect.has(replacement, key)) return Reflect.get(replacement, key, replacement);
7
+ if (hidden.has(key)) return undefined;
8
+ return Reflect.get(source, key, source);
9
+ },
10
+ set(_target, key, value: unknown): boolean {
11
+ if (Reflect.has(replacement, key)) return Reflect.set(replacement, key, value, replacement);
12
+ return !hidden.has(key) && Reflect.set(source, key, value, source);
13
+ },
14
+ has(_target, key): boolean {
15
+ return Reflect.has(replacement, key) || !hidden.has(key) && Reflect.has(source, key);
16
+ },
17
+ ownKeys(): (string | symbol)[] {
18
+ return [...new Set([...Reflect.ownKeys(source).filter(key => !hidden.has(key)), ...Reflect.ownKeys(replacement)])];
19
+ },
20
+ getOwnPropertyDescriptor(_target, key): PropertyDescriptor | undefined {
21
+ const descriptor = Reflect.getOwnPropertyDescriptor(replacement, key)
22
+ ?? (hidden.has(key) ? undefined : Reflect.getOwnPropertyDescriptor(source, key));
23
+ return descriptor ? { ...descriptor, configurable: true } : undefined;
24
+ },
25
+ });
26
+ }
27
+
28
+ // Each retained member delegates to the source; replacement members delegate to their own object.
29
+ export function replaceReactiveMembers<T extends object, R extends object>(source: T, replacement: R): Omit<T, keyof R> & R;
30
+ export function replaceReactiveMembers(source: object, replacement: object): object {
31
+ return project(source, replacement, []);
32
+ }
33
+
34
+ type ReplaceUnion<T, R> = T extends object ? Omit<T, keyof R> & R : never;
35
+
36
+ /** Preserve each result-state branch when projecting a discriminated runtime result. */
37
+ export function replaceReactiveUnionMembers<T extends object, R extends object>(source: T, replacement: R): ReplaceUnion<T, R>;
38
+ export function replaceReactiveUnionMembers(source: object, replacement: object): object {
39
+ return project(source, replacement, []);
40
+ }
41
+
42
+ /** Add only new members so source discriminants and their correlated values remain intact. */
43
+ export function extendReactiveMembers<T extends object, R extends object>(
44
+ source: T, extension: R & Record<Extract<keyof R, keyof NoInfer<T>>, never>,
45
+ ): T & R;
46
+ export function extendReactiveMembers(source: object, extension: object): object {
47
+ return project(source, extension, []);
48
+ }
49
+
50
+ export function omitReactiveMembers<T extends object, K extends keyof T>(source: T, omitted: readonly K[]): Omit<T, K>;
51
+ export function omitReactiveMembers(source: object, omitted: readonly PropertyKey[]): object {
52
+ return project(source, {}, omitted);
53
+ }
@@ -0,0 +1,63 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { createExactSchemaValidator } from './schema-validation';
3
+ import { snapshotPlainData } from './plain-data';
4
+ import { HttpError, type BaseRecord, type GetListResult, type GetOneResult, type GetManyResult, type CustomResult } from './types';
5
+
6
+ export type RecordDecoder<T extends BaseRecord> = (value: unknown) => T;
7
+
8
+ const record = Type.Record(Type.String(), Type.Unknown());
9
+ const recordValidator = createExactSchemaValidator(record);
10
+ const customValidator = createExactSchemaValidator(Type.Object({ data: Type.Unknown() }));
11
+ const oneValidator = createExactSchemaValidator(Type.Object({ data: record }));
12
+ const manyValidator = createExactSchemaValidator(Type.Object({ data: Type.Array(record) }));
13
+ const listValidator = createExactSchemaValidator(Type.Object({
14
+ data: Type.Array(record),
15
+ total: Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
16
+ }));
17
+
18
+ function invalidResponse(writeMayHaveSucceeded = false): never {
19
+ throw new HttpError('Invalid provider response', 502, undefined, {
20
+ code: 'INVALID_PROVIDER_RESPONSE',
21
+ details: { phase: 'response', writeMayHaveSucceeded },
22
+ });
23
+ }
24
+ export { invalidResponse as rejectProviderResponse };
25
+
26
+ export const decodeBaseRecord: RecordDecoder<BaseRecord> = value =>
27
+ recordValidator.Check(value) ? value : invalidResponse();
28
+
29
+ function decodeRecord<T extends BaseRecord>(value: unknown, decode: RecordDecoder<T>, writeMayHaveSucceeded = false): T {
30
+ try {
31
+ return decode(value);
32
+ } catch {
33
+ return invalidResponse(writeMayHaveSucceeded);
34
+ }
35
+ }
36
+
37
+ function snapshotReceipt(value: unknown, writeMayHaveSucceeded = false): unknown {
38
+ try { return snapshotPlainData(value); }
39
+ catch { return invalidResponse(writeMayHaveSucceeded); }
40
+ }
41
+
42
+ export function decodeOneResult<T extends BaseRecord>(value: unknown, decode: RecordDecoder<T>, writeMayHaveSucceeded = false): GetOneResult<T> {
43
+ const snapshot = snapshotReceipt(value, writeMayHaveSucceeded);
44
+ if (!oneValidator.Check(snapshot)) return invalidResponse(writeMayHaveSucceeded);
45
+ return { data: decodeRecord(snapshot.data, decode, writeMayHaveSucceeded) };
46
+ }
47
+
48
+ export function decodeManyResult<T extends BaseRecord>(value: unknown, decode: RecordDecoder<T>, writeMayHaveSucceeded = false): GetManyResult<T> {
49
+ const snapshot = snapshotReceipt(value, writeMayHaveSucceeded);
50
+ if (!manyValidator.Check(snapshot)) return invalidResponse(writeMayHaveSucceeded);
51
+ return { data: snapshot.data.map(item => decodeRecord(item, decode, writeMayHaveSucceeded)) };
52
+ }
53
+
54
+ export function decodeListResult<T extends BaseRecord>(value: unknown, decode: RecordDecoder<T>): GetListResult<T> {
55
+ const snapshot = snapshotReceipt(value);
56
+ if (!listValidator.Check(snapshot)) return invalidResponse();
57
+ return { ...snapshot, data: snapshot.data.map(item => decodeRecord(item, decode)) };
58
+ }
59
+
60
+ export function decodeCustomResult(value: unknown, writeMayHaveSucceeded = false): CustomResult {
61
+ if (!customValidator.Check(value)) return invalidResponse(writeMayHaveSucceeded);
62
+ return { data: value.data };
63
+ }
@@ -0,0 +1,19 @@
1
+ import { captureAdminContext } from './context.svelte';
2
+ import { resolveResourceContract } from './resource-contract';
3
+ import { definedReactiveOptions } from './defined-options';
4
+
5
+ /** Bind a metadata-driven view to its declared runtime contract and provider route. */
6
+ export function useResourceContract(resourceName: () => string) {
7
+ const context = captureAdminContext();
8
+ const definition = () => context.getResource(resourceName());
9
+ return definedReactiveOptions({
10
+ get resource() { return resolveResourceContract(definition()); },
11
+ get dataProviderName() {
12
+ const resource = definition();
13
+ const name = resource.provider?.dataProviderName ?? resource.meta?.dataProviderName ?? 'default';
14
+ context.getDataProvider(name);
15
+ return name;
16
+ },
17
+ get meta() { return context.getProviderMeta(resourceName()); },
18
+ });
19
+ }