@svadmin/core 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +3 -2
- package/src/access-control-contract.ts +1 -1
- package/src/admin-tool.ts +11 -5
- package/src/approval-contract.ts +1 -1
- package/src/audit-contract.ts +3 -3
- package/src/audit.ts +1 -1
- package/src/auth-mutation-contract.ts +1 -1
- package/src/auth-query-contract.ts +1 -1
- package/src/command-contract.ts +2 -3
- package/src/contract-form.svelte.ts +1 -1
- package/src/create-contract.ts +1 -1
- package/src/create-many-contract.ts +1 -1
- package/src/dashboard-contract.ts +1 -1
- package/src/delete-contract.ts +1 -1
- package/src/delete-many-contract.ts +1 -1
- package/src/export-format.ts +1 -1
- package/src/feature-gate-contract.ts +1 -1
- package/src/helpers-pure.ts +13 -9
- package/src/http.ts +2 -2
- package/src/import-contract.ts +1 -1
- package/src/inferencer.ts +1 -1
- package/src/infinite-query.svelte.ts +1 -1
- package/src/invalidation-contract.ts +1 -1
- package/src/live-sse.ts +1 -1
- package/src/live-subscription.svelte.ts +1 -1
- package/src/live-transport.ts +1 -1
- package/src/permission-catalog.ts +1 -1
- package/src/permission-hints-contract.ts +1 -1
- package/src/query-invalidation.ts +1 -1
- package/src/query-keys.ts +1 -1
- package/src/query-session.svelte.ts +1 -1
- package/src/query-snapshot.ts +17 -15
- package/src/record-decoder.ts +13 -2
- package/src/resource-contract.ts +31 -24
- package/src/resource-schemas.ts +21 -20
- package/src/resource-transport-config.ts +16 -7
- package/src/schema-form.ts +2 -2
- package/src/schema-validation.ts +119 -36
- package/src/spreadsheet-workbook.ts +1 -1
- package/src/strict-command-hooks.svelte.ts +1 -1
- package/src/task-contract.ts +1 -1
- package/src/task-provider.ts +1 -1
- package/src/theme.svelte.ts +13 -5
- package/src/update-contract.ts +1 -1
- package/src/update-many-contract.ts +1 -1
- package/src/useStepsForm.svelte.ts +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Use the existing `useForm.validate` callback with a shared TypeBox schema:
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import { Type, type Static } from '
|
|
6
|
+
import { Type, type Static } from 'typebox';
|
|
7
7
|
import { createSchemaFormValidator, useForm } from '@svadmin/core';
|
|
8
8
|
|
|
9
9
|
const Input = Type.Object({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -133,8 +133,9 @@
|
|
|
133
133
|
"type": "module",
|
|
134
134
|
"types": "src/index.ts",
|
|
135
135
|
"dependencies": {
|
|
136
|
+
"@types/papaparse": "^5.5.2",
|
|
136
137
|
"@internationalized/date": "^3.12.3",
|
|
137
|
-
"
|
|
138
|
+
"typebox": "^1.3.34",
|
|
138
139
|
"papaparse": "^5.5.4"
|
|
139
140
|
},
|
|
140
141
|
"peerDependenciesMeta": {
|
package/src/admin-tool.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { StaticDecode, TObject } from '
|
|
2
|
-
import { Value } from '
|
|
1
|
+
import type { StaticDecode, TObject } from 'typebox';
|
|
2
|
+
import { Value } from 'typebox/value';
|
|
3
3
|
|
|
4
4
|
/** Result returned by a tool execution. */
|
|
5
5
|
export interface ToolResult {
|
|
@@ -61,7 +61,10 @@ export function decodeAdminToolArgs<const Schema extends TObject>(
|
|
|
61
61
|
tool: AdminTool<Schema>,
|
|
62
62
|
input: unknown,
|
|
63
63
|
): StaticDecode<Schema> {
|
|
64
|
-
|
|
64
|
+
const schema = strictAdminToolSchema(tool.parameters);
|
|
65
|
+
// TypeBox 1.x Value.Decode strips undeclared properties instead of rejecting them.
|
|
66
|
+
if (!Value.Check(schema, input)) throw new Error('Admin tool arguments do not match the declared schema');
|
|
67
|
+
return Value.Decode(schema, input);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/** Decodes and executes a tool without allowing callers to bypass runtime input validation. */
|
|
@@ -94,6 +97,9 @@ export function projectAdminToolSchema<Schema extends TObject>(tool: AdminTool<S
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
function strictAdminToolSchema<Schema extends TObject>(schema: Schema): Schema {
|
|
97
|
-
if (schema.additionalProperties === false) return schema;
|
|
98
|
-
|
|
100
|
+
if ((schema as { additionalProperties?: unknown }).additionalProperties === false) return schema;
|
|
101
|
+
// Spreading would drop TypeBox 1.x non-enumerable markers such as ~codec; clone instead.
|
|
102
|
+
const copy = Value.Clone(schema) as TObject & { additionalProperties?: boolean };
|
|
103
|
+
copy.additionalProperties = false;
|
|
104
|
+
return copy as Schema;
|
|
99
105
|
}
|
package/src/approval-contract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static } from '
|
|
1
|
+
import { Type, type Static } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import type { EnterpriseProviderRequestContext } from './enterprise';
|
package/src/audit-contract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static, type TSchema } from '
|
|
1
|
+
import { Type, ObjectOptions, type Static, type TSchema } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
|
|
@@ -27,8 +27,8 @@ const entrySchema = Type.Object({
|
|
|
27
27
|
userAgent: Type.Optional(text),
|
|
28
28
|
error: Type.Optional(Type.Object({ message: Type.String(), code: Type.Optional(text) }, { additionalProperties: false })),
|
|
29
29
|
}, { additionalProperties: false });
|
|
30
|
-
const draftSchema = Type.Omit(entrySchema, ['id', 'timestamp']);
|
|
31
|
-
const createSchema = Type.Omit(entrySchema, ['id']);
|
|
30
|
+
const draftSchema = Type.Omit(entrySchema, ['id', 'timestamp'], ObjectOptions(entrySchema));
|
|
31
|
+
const createSchema = Type.Omit(entrySchema, ['id'], ObjectOptions(entrySchema));
|
|
32
32
|
const querySchema = Type.Object({
|
|
33
33
|
resource: Type.Optional(text), action: Type.Optional(text),
|
|
34
34
|
meta: Type.Optional(record), author: Type.Optional(record),
|
package/src/audit.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { definedOptions } from './defined-options';
|
|
2
|
-
import { Value } from '
|
|
2
|
+
import { Value } from 'typebox/value';
|
|
3
3
|
import {
|
|
4
4
|
AuditError, createAuditEntry, decodeAuditCreate, decodeAuditEntries, decodeAuditEntry, decodeAuditQuery,
|
|
5
5
|
type AuditCreateParams, type AuditDraft, type AuditEntry, type AuditQueryParams,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static } from '
|
|
1
|
+
import { Type, type Static } from 'typebox';
|
|
2
2
|
import type { AuthProvider } from './types';
|
|
3
3
|
import { authErrorSchema, authRedirectSchema, decodeIdentity } from './auth-query-contract';
|
|
4
4
|
import { snapshotPlainData } from './plain-data';
|
package/src/command-contract.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Type, type TObject, type TSchema } from '
|
|
2
|
-
import { TypeGuard } from '@sinclair/typebox/type';
|
|
1
|
+
import { Type, type TObject, type TSchema } from 'typebox';
|
|
3
2
|
import { checkExact } from './schema-validation';
|
|
4
3
|
import { closeContractSchema, type SafeSchema, type SchemaValue } from './resource-contract';
|
|
5
4
|
import { snapshotPlainData } from './plain-data';
|
|
@@ -101,7 +100,7 @@ export function defineCommand(name: string, definition: {
|
|
|
101
100
|
const outputSchema: unknown = fields['output']?.value;
|
|
102
101
|
if (typeof url !== 'string' || !url.trim() ||
|
|
103
102
|
(method !== 'get' && method !== 'post' && method !== 'put' && method !== 'patch' && method !== 'delete') ||
|
|
104
|
-
!
|
|
103
|
+
!Type.IsObject(inputSchema) || !Type.IsSchema(outputSchema)) {
|
|
105
104
|
return invalidContract();
|
|
106
105
|
}
|
|
107
106
|
const input = closeContractSchema(inputSchema);
|
|
@@ -16,7 +16,7 @@ import { invalidateOwnedQueries } from './query-invalidation';
|
|
|
16
16
|
import { appendListQueryFromPath } from './url-sync';
|
|
17
17
|
import { HttpError } from './types';
|
|
18
18
|
import { useTranslation } from './i18n.svelte';
|
|
19
|
-
import { Type } from '
|
|
19
|
+
import { Type } from 'typebox';
|
|
20
20
|
import { checkExact } from './schema-validation';
|
|
21
21
|
import { auditWithProvider } from './audit';
|
|
22
22
|
import { fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
|
package/src/create-contract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { snapshotPlainData } from './plain-data';
|
|
3
3
|
import { checkExact } from './schema-validation';
|
|
4
4
|
import { parseContractId, parseContractRecord, parseContractCreateInput, requireContractCreateSchema,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static } from '
|
|
1
|
+
import { Type, type Static } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import type { EnterpriseProviderRequestContext } from './enterprise';
|
package/src/delete-contract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { snapshotPlainData } from './plain-data';
|
|
3
3
|
import { checkExact } from './schema-validation';
|
|
4
4
|
import { parseContractId, parseContractRecord, parseContractDeleteInput, type ContractSchemas, type ContractId, type ResourceContract } from './resource-contract';
|
package/src/export-format.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure export formatting helpers with no Svelte rune or browser API dependencies except downloadData.
|
|
2
2
|
// These helpers can be tested directly with bun:test.
|
|
3
3
|
|
|
4
|
-
import { Type, type Static } from '
|
|
4
|
+
import { Type, type Static } from 'typebox';
|
|
5
5
|
import { checkExact } from './schema-validation';
|
|
6
6
|
import { snapshotPlainData } from './plain-data';
|
|
7
7
|
|
package/src/helpers-pure.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Extracted to enable unit testing with bun test
|
|
3
3
|
|
|
4
4
|
import type { Filter, Sort, CrudOperator } from './types';
|
|
5
|
+
import { schemaIssues } from './schema-validation';
|
|
5
6
|
|
|
6
7
|
// ─── Table Helpers ────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -168,21 +169,24 @@ export function parseCSV(text: string): string[][] {
|
|
|
168
169
|
// ─── TypeBox Validator Helper ───────────────────────────────────
|
|
169
170
|
|
|
170
171
|
/**
|
|
171
|
-
* TypeBox validator interface matching
|
|
172
|
+
* TypeBox validator interface matching Compile or Value.Errors
|
|
172
173
|
*/
|
|
173
174
|
export type TypeBoxValidatorLike = {
|
|
174
175
|
Check?: (value: unknown) => boolean;
|
|
175
|
-
Errors: (value: unknown) => Iterable<{
|
|
176
|
+
Errors: (value: unknown) => Iterable<{
|
|
177
|
+
path?: string; instancePath?: string; keyword?: string;
|
|
178
|
+
params?: Record<string, unknown>; message: string;
|
|
179
|
+
}>;
|
|
176
180
|
};
|
|
177
181
|
|
|
178
182
|
/**
|
|
179
183
|
* Creates a synchronous form validator function specifically optimized for TypeBox
|
|
180
|
-
* compiled validators (from
|
|
184
|
+
* compiled validators (from Compile) or TypeBox value checks.
|
|
181
185
|
*
|
|
182
186
|
* @example
|
|
183
187
|
* ```ts
|
|
184
|
-
* import { Type } from '
|
|
185
|
-
* import {
|
|
188
|
+
* import { Type } from 'typebox';
|
|
189
|
+
* import { Compile } from 'typebox/compile';
|
|
186
190
|
* import { createTypeBoxValidator, useForm } from '@svadmin/core';
|
|
187
191
|
*
|
|
188
192
|
* const userSchema = Type.Object({
|
|
@@ -190,7 +194,7 @@ export type TypeBoxValidatorLike = {
|
|
|
190
194
|
* email: Type.String({ format: 'email' }),
|
|
191
195
|
* });
|
|
192
196
|
*
|
|
193
|
-
* const compiledUser =
|
|
197
|
+
* const compiledUser = Compile(userSchema);
|
|
194
198
|
*
|
|
195
199
|
* const form = useForm({
|
|
196
200
|
* resource: 'users',
|
|
@@ -205,10 +209,10 @@ export function createTypeBoxValidator(
|
|
|
205
209
|
if (validator.Check?.(values)) return null;
|
|
206
210
|
|
|
207
211
|
const errors: Record<string, string> = {};
|
|
208
|
-
for (const
|
|
209
|
-
const path =
|
|
212
|
+
for (const issue of schemaIssues(validator.Errors(values))) {
|
|
213
|
+
const path = issue.path.replace(/^\//, '').replace(/\//g, '.');
|
|
210
214
|
const key = path || '_root';
|
|
211
|
-
if (!errors[key]) errors[key] =
|
|
215
|
+
if (!errors[key]) errors[key] = issue.message;
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
return Object.keys(errors).length > 0 ? errors : null;
|
package/src/http.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { HttpError,type ValidationErrors } from './types';
|
|
6
|
-
import { Type } from '
|
|
7
|
-
import { Value } from '
|
|
6
|
+
import { Type } from 'typebox';
|
|
7
|
+
import { Value } from 'typebox/value';
|
|
8
8
|
import { definedOptions } from './defined-options';
|
|
9
9
|
|
|
10
10
|
export interface FetchWithInterceptorOptions {
|
package/src/import-contract.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Papa from 'papaparse';
|
|
2
|
-
import { Type, type Static } from '
|
|
2
|
+
import { Type, type Static } from 'typebox';
|
|
3
3
|
import { checkExact } from './schema-validation';
|
|
4
4
|
import { snapshotPlainData } from './plain-data';
|
|
5
5
|
import { HttpError } from './types';
|
package/src/inferencer.ts
CHANGED
|
@@ -407,7 +407,7 @@ export function generateTypeBoxSchemaCode(resource: ResourceDefinition): string
|
|
|
407
407
|
const baseName = resource.name.endsWith('s') ? resource.name.slice(0, -1) : resource.name;
|
|
408
408
|
const typeName = capitalize(baseName);
|
|
409
409
|
|
|
410
|
-
return `import { Type, type Static } from '
|
|
410
|
+
return `import { Type, type Static } from 'typebox';
|
|
411
411
|
|
|
412
412
|
export const ${typeName}Schema = Type.Object({
|
|
413
413
|
${schemaProps}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInfiniteQuery, type InfiniteData, type InfiniteQueryObserverBaseResult, type QueryObserverResult,
|
|
2
2
|
type RefetchOptions, type FetchNextPageOptions, type FetchPreviousPageOptions } from '@tanstack/svelte-query';
|
|
3
3
|
import { tick } from 'svelte';
|
|
4
|
-
import { Type } from '
|
|
4
|
+
import { Type } from 'typebox';
|
|
5
5
|
import { definedOptions } from './defined-options';
|
|
6
6
|
import { getAdminOptions } from './options.svelte';
|
|
7
7
|
import { useParsed } from './useParsed.svelte';
|
package/src/live-sse.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LiveProvider } from './live.svelte';
|
|
2
2
|
import { createLiveSubscribers, decodeLiveMessage, notifyLiveObserver, readLiveOptions, captureLiveObserver } from './live-transport';
|
|
3
|
-
import { Type } from '
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
4
|
import { checkExact } from './schema-validation';
|
|
5
5
|
import { snapshotPlainData } from './plain-data';
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
2
|
import type { QueryClient } from '@tanstack/svelte-query';
|
|
3
|
-
import { Type } from '
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
4
|
import { captureAdminContext } from './context.svelte';
|
|
5
5
|
import { contractKey, type ResourceContract } from './resource-contract';
|
|
6
6
|
import { captureQueryProvider } from './query-snapshot';
|
package/src/live-transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static } from '
|
|
1
|
+
import { Type, type Static } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData, type JsonValue } from './plain-data';
|
|
4
4
|
import { definedOptions } from './defined-options';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static } from '
|
|
1
|
+
import { Type, type Static } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import type { AccessControlOptions, AccessControlProvider, CanParams } from './permissions.svelte';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import { decodePermissionSnapshot, permissionSnapshotSchema, type PermissionSnapshot } from './permission-catalog';
|
|
@@ -4,7 +4,7 @@ import { snapshotPlainData } from './plain-data';
|
|
|
4
4
|
import { snapshotInvalidationParams } from './invalidation-contract';
|
|
5
5
|
import { definedOptions } from './defined-options';
|
|
6
6
|
import { HttpError } from './types';
|
|
7
|
-
import { Type } from '
|
|
7
|
+
import { Type } from 'typebox';
|
|
8
8
|
import { checkExact } from './schema-validation';
|
|
9
9
|
|
|
10
10
|
export interface QueryCacheOwner {
|
package/src/query-keys.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type,type Static } from '
|
|
1
|
+
import { Type,type Static } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { definedOptions } from './defined-options';
|
|
4
4
|
import { snapshotPlainData, type JsonValue } from './plain-data';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hashKey, useQueryClient, type QueryObserverPendingResult, type QueryObserverResult, type RefetchOptions } from '@tanstack/svelte-query';
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import { Type } from '
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
4
|
import type { AdminContextAccessor } from './context.svelte';
|
|
5
5
|
import { captureAuthLiveScope, clearAuthQueries, handleAuthError } from './auth-hooks.svelte';
|
|
6
6
|
import { decodeBaseRecord } from './record-decoder';
|
package/src/query-snapshot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type, type Static, type TSchema } from '
|
|
1
|
+
import { Type, type Static, type TSchema } from 'typebox';
|
|
2
2
|
import type { AdminContextAccessor } from './context.svelte';
|
|
3
3
|
import type { DataProvider, GetListParams, GetOneParams, GetManyParams } from './types';
|
|
4
4
|
import { HttpError } from './types';
|
|
@@ -8,20 +8,22 @@ import { attachAbortSignal, detachAbortSignal, snapshotPlainData } from './plain
|
|
|
8
8
|
import { checkExact } from './schema-validation';
|
|
9
9
|
import { decodeBaseRecord, decodeListResult, decodeOneResult, decodeManyResult, rejectProviderResponse } from './record-decoder';
|
|
10
10
|
|
|
11
|
-
const filters = Type.
|
|
12
|
-
Type.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
const filters = Type.Cyclic({
|
|
12
|
+
Filters: Type.Union([
|
|
13
|
+
Type.Object({
|
|
14
|
+
field: Type.String(),
|
|
15
|
+
operator: Type.Union(([
|
|
16
|
+
'eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'nin', 'contains', 'ncontains',
|
|
17
|
+
'startswith', 'endswith', 'null', 'nnull', 'between', 'nbetween',
|
|
18
|
+
] as const).map(value => Type.Literal(value))),
|
|
19
|
+
value: Type.Unknown(),
|
|
20
|
+
}, { additionalProperties: false }),
|
|
21
|
+
Type.Object({
|
|
22
|
+
operator: Type.Union([Type.Literal('and'), Type.Literal('or')]),
|
|
23
|
+
value: Type.Array(Type.Ref('Filters')),
|
|
24
|
+
}, { additionalProperties: false }),
|
|
25
|
+
]),
|
|
26
|
+
}, 'Filters');
|
|
25
27
|
const base = {
|
|
26
28
|
resource: Type.String({ minLength: 1 }),
|
|
27
29
|
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
package/src/record-decoder.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { createExactSchemaValidator } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import { HttpError, type BaseRecord, type GetListResult, type GetOneResult, type GetManyResult, type CustomResult } from './types';
|
|
5
5
|
|
|
6
6
|
export type RecordDecoder<T extends BaseRecord> = (value: unknown) => T;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** TypeBox 1.x Record validation accepts class instances; keep records limited to plain JSON objects. */
|
|
9
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
11
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
12
|
+
return prototype === Object.prototype || prototype === null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const record = Type.Refine(
|
|
16
|
+
Type.Record(Type.String(), Type.Unknown()),
|
|
17
|
+
isPlainRecord,
|
|
18
|
+
() => 'Expected plain object',
|
|
19
|
+
);
|
|
9
20
|
const recordValidator = createExactSchemaValidator(record);
|
|
10
21
|
const customValidator = createExactSchemaValidator(Type.Object({ data: Type.Unknown() }));
|
|
11
22
|
const oneValidator = createExactSchemaValidator(Type.Object({ data: record }));
|
package/src/resource-contract.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Type, type Static, type TObject, type TSchema } from 'typebox';
|
|
2
|
+
import { Value } from 'typebox/value';
|
|
3
|
+
import { checkExact, createExactSchemaValidator, schemaIssues } from './schema-validation';
|
|
4
4
|
import { DeleteManyPartialError, HttpError, type DataProvider, type KnownResources, type BaseRecord,
|
|
5
5
|
type GetListResult, type GetManyParams, type CreateManyParams, type UpdateManyParams, type DeleteManyParams,
|
|
6
6
|
type GetOneParams, type GetOneResult, type GetManyResult } from './types';
|
|
@@ -70,26 +70,33 @@ interface Definition {
|
|
|
70
70
|
const definitions = new WeakMap<ResourceContract, Definition>();
|
|
71
71
|
let nextKey = 0;
|
|
72
72
|
|
|
73
|
+
/** TypeBox 1.x stores the schema kind under the non-enumerable `~kind` marker instead of a symbol. */
|
|
74
|
+
function schemaKind(schema: TSchema): string | undefined {
|
|
75
|
+
const kind = (schema as { '~kind'?: unknown })['~kind'];
|
|
76
|
+
return typeof kind === 'string' ? kind : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
// A deliberately closed JSON subset keeps runtime validation aligned with Static.
|
|
74
80
|
export function closeContractSchema<S extends TSchema>(schema: S): S {
|
|
75
|
-
const copy =
|
|
81
|
+
const copy = Value.Clone(schema);
|
|
76
82
|
const visit = (node: TSchema): void => {
|
|
77
|
-
if (
|
|
83
|
+
if ('~codec' in node) {
|
|
78
84
|
throw new HttpError('Contract transforms require a separate decoding boundary', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
79
85
|
}
|
|
80
|
-
if (!
|
|
86
|
+
if (!Type.IsSchema(node) || !['Object', 'Array', 'Tuple', 'Union', 'String', 'Number', 'Integer', 'Boolean', 'Null', 'Literal', 'Never'].includes(schemaKind(node) ?? '')) {
|
|
81
87
|
throw new HttpError('Unsupported or unconstrained contract schema', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
82
88
|
}
|
|
83
|
-
if (
|
|
84
|
-
|
|
89
|
+
if (Type.IsObject(node)) {
|
|
90
|
+
const objectNode = node as TSchema & { additionalProperties?: TSchema | boolean };
|
|
91
|
+
if (objectNode.additionalProperties !== undefined && objectNode.additionalProperties !== false) {
|
|
85
92
|
throw new HttpError('Contract objects must be closed', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
86
93
|
}
|
|
87
|
-
|
|
94
|
+
objectNode.additionalProperties = false;
|
|
88
95
|
for (const property of Object.values(node.properties)) visit(property);
|
|
89
96
|
}
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
if (
|
|
97
|
+
if (Type.IsArray(node)) visit(node.items);
|
|
98
|
+
if (Type.IsTuple(node)) for (const item of node.items ?? []) visit(item);
|
|
99
|
+
if (Type.IsUnion(node)) for (const item of node.anyOf) visit(item);
|
|
93
100
|
};
|
|
94
101
|
visit(copy);
|
|
95
102
|
return copy;
|
|
@@ -101,11 +108,11 @@ export function defineResource<const S extends ContractSchemas>(
|
|
|
101
108
|
): ResourceContract<S>;
|
|
102
109
|
export function defineResource(name: KnownResources, schemas: ContractSchemas): ResourceContract {
|
|
103
110
|
const isId = (schema: TSchema): boolean => (
|
|
104
|
-
['String', 'Number', 'Integer'].includes(schema
|
|
105
|
-
|| (
|
|
106
|
-
|| (
|
|
111
|
+
['String', 'Number', 'Integer'].includes(schemaKind(schema) ?? '')
|
|
112
|
+
|| (Type.IsLiteral(schema) && ['string', 'number'].includes(typeof schema.const))
|
|
113
|
+
|| (Type.IsUnion(schema) && schema.anyOf.every(isId))
|
|
107
114
|
);
|
|
108
|
-
if (!name.trim() || schemas.record
|
|
115
|
+
if (!name.trim() || schemaKind(schemas.record) !== 'Object'
|
|
109
116
|
|| !schemas.record.required?.includes('id')
|
|
110
117
|
|| !schemas.record.properties['id']
|
|
111
118
|
|| !isId(schemas.record.properties['id'])) {
|
|
@@ -177,7 +184,7 @@ export interface ContractProjectionOptions {
|
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
function projectBySchema(schema: TSchema, value: unknown): unknown {
|
|
180
|
-
if (
|
|
187
|
+
if (Type.IsObject(schema)) {
|
|
181
188
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
|
|
182
189
|
const source = value as Record<string, unknown>;
|
|
183
190
|
const projected: Record<string, unknown> = {};
|
|
@@ -186,15 +193,15 @@ function projectBySchema(schema: TSchema, value: unknown): unknown {
|
|
|
186
193
|
}
|
|
187
194
|
return projected;
|
|
188
195
|
}
|
|
189
|
-
if (
|
|
196
|
+
if (Type.IsArray(schema)) {
|
|
190
197
|
return Array.isArray(value) ? value.map(item => projectBySchema(schema.items, item)) : value;
|
|
191
198
|
}
|
|
192
|
-
if (
|
|
199
|
+
if (Type.IsTuple(schema)) {
|
|
193
200
|
return Array.isArray(value)
|
|
194
201
|
? value.map((item, index) => schema.items?.[index] ? projectBySchema(schema.items[index], item) : item)
|
|
195
202
|
: value;
|
|
196
203
|
}
|
|
197
|
-
if (
|
|
204
|
+
if (Type.IsUnion(schema)) {
|
|
198
205
|
const branch = schema.anyOf.find(candidate => checkExact(candidate, value));
|
|
199
206
|
return branch ? projectBySchema(branch, value) : value;
|
|
200
207
|
}
|
|
@@ -394,7 +401,7 @@ export function parseContractDeleteInput(contract: ResourceContract, value: unkn
|
|
|
394
401
|
export function contractFormValues(contract: ResourceContract, action: string, value: Record<string, unknown>): Record<string, unknown> {
|
|
395
402
|
const schemas = definitionOf(contract).schemas;
|
|
396
403
|
const schema = action === 'edit' ? schemas.update : schemas.create;
|
|
397
|
-
if (!schema || !
|
|
404
|
+
if (!schema || !Type.IsObject(schema)) {
|
|
398
405
|
throw new HttpError('Forms require an object input schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
399
406
|
}
|
|
400
407
|
const projected = Object.fromEntries(Object.keys(schema.properties)
|
|
@@ -406,7 +413,7 @@ export function contractFormValues(contract: ResourceContract, action: string, v
|
|
|
406
413
|
function formSchema(contract: ResourceContract, action: ContractFormAction): TObject {
|
|
407
414
|
const schemas = definitionOf(contract).schemas;
|
|
408
415
|
const schema = action === 'show' ? schemas.record : action === 'edit' ? schemas.update : schemas.create;
|
|
409
|
-
if (!schema || !
|
|
416
|
+
if (!schema || !Type.IsObject(schema)) {
|
|
410
417
|
throw new HttpError('Forms require an object schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
411
418
|
}
|
|
412
419
|
return schema;
|
|
@@ -445,7 +452,7 @@ export function snapshotContractFormDraft(contract: ResourceContract, action: Co
|
|
|
445
452
|
export function contractFormInvalidFields(contract: ResourceContract, action: ContractFormAction, value: unknown): string[] {
|
|
446
453
|
const schema = formSchema(contract, action);
|
|
447
454
|
const validator = createExactSchemaValidator(schema);
|
|
448
|
-
const paths =
|
|
455
|
+
const paths = schemaIssues(validator.Errors(value)).map(issue => issue.path);
|
|
449
456
|
const fields = Object.keys(schema.properties).filter(field => {
|
|
450
457
|
const path = `/${field.replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
451
458
|
return paths.some(candidate => candidate === path || candidate.startsWith(`${path}/`));
|
|
@@ -460,7 +467,7 @@ export function parseContractFormInput<S extends ContractSchemas, A extends 'cre
|
|
|
460
467
|
export function parseContractFormInput(contract: ResourceContract, action: 'create' | 'edit', value: unknown): BaseRecord {
|
|
461
468
|
const schemas = definitionOf(contract).schemas;
|
|
462
469
|
const schema = action === 'edit' ? schemas.update : schemas.create;
|
|
463
|
-
if (!schema || !
|
|
470
|
+
if (!schema || !Type.IsObject(schema)) {
|
|
464
471
|
throw new HttpError('Forms require an object input schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|
|
465
472
|
}
|
|
466
473
|
check(schema, value, 'values');
|
package/src/resource-schemas.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Type, type Static, type TSchema } from '
|
|
2
|
-
import { checkExact, createExactSchemaValidator } from './schema-validation';
|
|
1
|
+
import { Type, type Static, type TSchema } from 'typebox';
|
|
2
|
+
import { checkExact, createExactSchemaValidator, schemaIssues } from './schema-validation';
|
|
3
3
|
import { attachAbortSignal, detachAbortSignal, snapshotPlainData } from './plain-data';
|
|
4
4
|
import { decodeBaseRecord, decodeOneResult, decodeManyResult, decodeListResult } from './record-decoder';
|
|
5
5
|
import { HttpError } from './types';
|
|
@@ -43,22 +43,24 @@ const common = {
|
|
|
43
43
|
resource: Type.String({ minLength: 1 }),
|
|
44
44
|
meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
45
45
|
};
|
|
46
|
-
const filter = Type.
|
|
47
|
-
Type.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
const filter = Type.Cyclic({
|
|
47
|
+
Filter: Type.Union([
|
|
48
|
+
Type.Object({
|
|
49
|
+
operator: Type.Union([Type.Literal('or'), Type.Literal('and')]),
|
|
50
|
+
value: Type.Array(Type.Ref('Filter')),
|
|
51
|
+
}, { additionalProperties: false }),
|
|
52
|
+
Type.Object({
|
|
53
|
+
field: Type.String(),
|
|
54
|
+
operator: Type.Union([
|
|
55
|
+
Type.Literal('eq'), Type.Literal('ne'), Type.Literal('lt'), Type.Literal('gt'),
|
|
56
|
+
Type.Literal('lte'), Type.Literal('gte'), Type.Literal('contains'), Type.Literal('ncontains'),
|
|
57
|
+
Type.Literal('startswith'), Type.Literal('endswith'), Type.Literal('in'), Type.Literal('nin'),
|
|
58
|
+
Type.Literal('null'), Type.Literal('nnull'), Type.Literal('between'), Type.Literal('nbetween'),
|
|
59
|
+
]),
|
|
60
|
+
value: Type.Unknown(),
|
|
61
|
+
}, { additionalProperties: false }),
|
|
62
|
+
]),
|
|
63
|
+
}, 'Filter');
|
|
62
64
|
const requestSchemas = {
|
|
63
65
|
getList: Type.Object({
|
|
64
66
|
...common,
|
|
@@ -166,8 +168,7 @@ function createSchemaBoundary(schemas: ResourceSchemaMap) {
|
|
|
166
168
|
}
|
|
167
169
|
if (validator.Check(value)) return;
|
|
168
170
|
// Do not include rejected values or raw responses in diagnostic details.
|
|
169
|
-
issues =
|
|
170
|
-
.map(({ path, message }) => ({ path, message }));
|
|
171
|
+
issues = schemaIssues(validator.Errors(value));
|
|
171
172
|
} catch {
|
|
172
173
|
throw new HttpError('Unable to evaluate resource schema', 500, undefined, {
|
|
173
174
|
code: 'RESOURCE_SCHEMA_INVALID',
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { TypeGuard } from '@sinclair/typebox/type';
|
|
1
|
+
import { Type, type TSchema } from 'typebox';
|
|
3
2
|
import { HttpError } from './types';
|
|
4
3
|
import type { DataProvider } from './types';
|
|
5
4
|
import type { ResourceSchemas } from './resource-schemas';
|
|
@@ -15,6 +14,9 @@ function invalidProvider(): never {
|
|
|
15
14
|
});
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
/** Scalar TypeBox 1.x markers that may be carried through a configuration snapshot. */
|
|
18
|
+
const TYPEBOX_MARKERS = new Set(['~kind', '~optional', '~readonly', '~immutable']);
|
|
19
|
+
|
|
18
20
|
/** Configuration can carry TypeBox markers, but no getters, functions or foreign symbols. */
|
|
19
21
|
function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0): unknown {
|
|
20
22
|
if (depth > 128) return invalidSchema();
|
|
@@ -41,11 +43,18 @@ function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0
|
|
|
41
43
|
}
|
|
42
44
|
const copy: Record<PropertyKey, unknown> = {};
|
|
43
45
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
// TypeBox 1.x marks schema metadata with non-enumerable `~`-prefixed string keys.
|
|
47
|
+
if (typeof key === 'symbol') return invalidSchema();
|
|
48
|
+
if (key.startsWith('~')) {
|
|
49
|
+
if (!TYPEBOX_MARKERS.has(key)) return invalidSchema();
|
|
50
|
+
const marker: PropertyDescriptor | undefined = Reflect.get(descriptors, key);
|
|
51
|
+
if (!marker || !('value' in marker) || marker.get || marker.set) return invalidSchema();
|
|
52
|
+
const markerValue: unknown = marker.value;
|
|
53
|
+
if (typeof markerValue !== 'string' && typeof markerValue !== 'boolean') return invalidSchema();
|
|
54
|
+
Object.defineProperty(copy, key, { value: markerValue, enumerable: false, configurable: true, writable: false });
|
|
55
|
+
continue;
|
|
46
56
|
}
|
|
47
57
|
const field = read(key);
|
|
48
|
-
if (typeof key === 'symbol' && typeof field !== 'string') return invalidSchema();
|
|
49
58
|
Object.defineProperty(copy, key, { value: field, enumerable: true, configurable: true, writable: true });
|
|
50
59
|
}
|
|
51
60
|
return copy;
|
|
@@ -56,7 +65,7 @@ function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0
|
|
|
56
65
|
|
|
57
66
|
function schema(value: unknown): TSchema {
|
|
58
67
|
const copy = copySchemaData(value);
|
|
59
|
-
if (!
|
|
68
|
+
if (!Type.IsSchema(copy) || typeof (copy as { '~kind'?: unknown })['~kind'] !== 'string') return invalidSchema();
|
|
60
69
|
return copy;
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -88,7 +97,7 @@ export function captureResourceSchemas(value: unknown): Map<string, ResourceSche
|
|
|
88
97
|
if (!Array.isArray(copied)) return invalidSchema();
|
|
89
98
|
const candidates: unknown[] = copied;
|
|
90
99
|
for (const candidate of candidates) {
|
|
91
|
-
if (!
|
|
100
|
+
if (!Type.IsSchema(candidate)) return invalidSchema();
|
|
92
101
|
references.push(candidate);
|
|
93
102
|
}
|
|
94
103
|
}
|
package/src/schema-form.ts
CHANGED
package/src/schema-validation.ts
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { Type, type Static, type TSchema } from 'typebox';
|
|
2
|
+
import { Value } from 'typebox/value';
|
|
3
|
+
|
|
4
|
+
const CODEC_KEY = '~codec';
|
|
5
|
+
|
|
6
|
+
/** JSON Schema keywords that TypeBox 1.x stores but does not expose on every typed interface. */
|
|
7
|
+
interface SchemaMeta {
|
|
8
|
+
$id?: string;
|
|
9
|
+
$defs?: Record<string, TSchema>;
|
|
10
|
+
properties?: Record<string, TSchema>;
|
|
11
|
+
required?: readonly string[];
|
|
12
|
+
additionalProperties?: TSchema | boolean;
|
|
13
|
+
patternProperties?: Record<string, TSchema>;
|
|
14
|
+
contains?: TSchema;
|
|
15
|
+
unevaluatedProperties?: TSchema | boolean;
|
|
16
|
+
anyOf?: TSchema[];
|
|
17
|
+
allOf?: TSchema[];
|
|
18
|
+
items?: TSchema | TSchema[];
|
|
19
|
+
not?: TSchema;
|
|
20
|
+
}
|
|
4
21
|
|
|
5
22
|
/**
|
|
6
23
|
* Strengthen optional-property presence with ordinary schemas, without changing
|
|
7
24
|
* TypeBox's process-wide policy or maintaining a second value validator.
|
|
8
25
|
*/
|
|
9
|
-
function exactSchema(
|
|
10
|
-
|
|
26
|
+
function exactSchema(input: TSchema): TSchema {
|
|
27
|
+
const schema = input as TSchema & SchemaMeta;
|
|
28
|
+
if (typeof schema.not === 'object' && Type.IsSchema(schema.not)) {
|
|
29
|
+
schema.not = exactSchema(schema.not);
|
|
30
|
+
}
|
|
31
|
+
if (Type.IsObject(input)) {
|
|
32
|
+
const properties = schema.properties ?? {};
|
|
11
33
|
schema.properties = Object.fromEntries(
|
|
12
|
-
Object.entries(
|
|
34
|
+
Object.entries(properties).map(([key, property]) => [key, exactSchema(property)]),
|
|
13
35
|
);
|
|
14
|
-
if (
|
|
36
|
+
if (typeof schema.additionalProperties === 'object' && Type.IsSchema(schema.additionalProperties)) {
|
|
15
37
|
schema.additionalProperties = exactSchema(schema.additionalProperties);
|
|
16
38
|
}
|
|
17
39
|
const optional = Object.entries(schema.properties)
|
|
@@ -20,7 +42,8 @@ function exactSchema(schema: TSchema): TSchema {
|
|
|
20
42
|
|
|
21
43
|
const constraints = optional.map(([key, property]) => Type.Union([
|
|
22
44
|
Type.Required(Type.Object({ [key]: property })),
|
|
23
|
-
Type.Not
|
|
45
|
+
// TypeBox 1.x removed Type.Not; the JSON Schema `not` keyword is expressed through Unsafe.
|
|
46
|
+
Type.Unsafe({ not: Type.Object({ [key]: Type.Unknown() }) }),
|
|
24
47
|
]));
|
|
25
48
|
// The presence constraint owns each optional value's validation. Leaving it
|
|
26
49
|
// in both places would validate deep optional objects exponentially often.
|
|
@@ -28,57 +51,117 @@ function exactSchema(schema: TSchema): TSchema {
|
|
|
28
51
|
// References must resolve to the strengthened object, not its inner copy.
|
|
29
52
|
const id = schema.$id;
|
|
30
53
|
delete schema.$id;
|
|
31
|
-
const
|
|
32
|
-
Reflect.deleteProperty(schema,
|
|
54
|
+
const codec = Object.getOwnPropertyDescriptor(schema, CODEC_KEY);
|
|
55
|
+
Reflect.deleteProperty(schema, CODEC_KEY);
|
|
33
56
|
const result = Type.Intersect([schema, ...constraints], id === undefined ? {} : { $id: id });
|
|
34
|
-
if (
|
|
57
|
+
if (codec !== undefined) Object.defineProperty(result, CODEC_KEY, codec);
|
|
35
58
|
return result;
|
|
36
59
|
}
|
|
37
|
-
if (
|
|
38
|
-
schema.items = exactSchema(schema.items);
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else if (
|
|
43
|
-
schema.
|
|
44
|
-
} else if (
|
|
45
|
-
schema.
|
|
46
|
-
|
|
60
|
+
if (Type.IsArray(input)) {
|
|
61
|
+
schema.items = exactSchema(schema.items as TSchema);
|
|
62
|
+
if (typeof schema.contains === 'object' && Type.IsSchema(schema.contains)) {
|
|
63
|
+
schema.contains = exactSchema(schema.contains);
|
|
64
|
+
}
|
|
65
|
+
} else if (Type.IsTuple(input)) {
|
|
66
|
+
if (Array.isArray(schema.items)) schema.items = schema.items.map(exactSchema);
|
|
67
|
+
} else if (Type.IsUnion(input)) {
|
|
68
|
+
schema.anyOf = (schema.anyOf ?? []).map(exactSchema);
|
|
69
|
+
} else if (Type.IsIntersect(input)) {
|
|
70
|
+
schema.allOf = (schema.allOf ?? []).map(exactSchema);
|
|
71
|
+
if (typeof schema.unevaluatedProperties === 'object' && Type.IsSchema(schema.unevaluatedProperties)) {
|
|
47
72
|
schema.unevaluatedProperties = exactSchema(schema.unevaluatedProperties);
|
|
48
73
|
}
|
|
49
|
-
} else if (
|
|
50
|
-
schema.not = exactSchema(schema.not);
|
|
51
|
-
} else if (TypeGuard.IsRecord(schema)) {
|
|
74
|
+
} else if (Type.IsRecord(input)) {
|
|
52
75
|
schema.patternProperties = Object.fromEntries(
|
|
53
|
-
Object.entries(schema.patternProperties).map(([key, property]) => [key, exactSchema(property)]),
|
|
76
|
+
Object.entries(schema.patternProperties ?? {}).map(([key, property]) => [key, exactSchema(property)]),
|
|
54
77
|
);
|
|
55
|
-
if (
|
|
78
|
+
if (typeof schema.additionalProperties === 'object' && Type.IsSchema(schema.additionalProperties)) {
|
|
56
79
|
schema.additionalProperties = exactSchema(schema.additionalProperties);
|
|
57
80
|
}
|
|
58
|
-
} else if (
|
|
81
|
+
} else if (Type.IsCyclic(input)) {
|
|
59
82
|
schema.$defs = Object.fromEntries(
|
|
60
|
-
Object.entries(schema.$defs).map(([key, definition]) =>
|
|
61
|
-
if (!TypeGuard.IsSchema(definition)) throw new TypeError('Invalid imported schema definition');
|
|
62
|
-
return [key, exactSchema(definition)];
|
|
63
|
-
}),
|
|
83
|
+
Object.entries(schema.$defs ?? {}).map(([key, definition]) => [key, exactSchema(definition)]),
|
|
64
84
|
);
|
|
65
85
|
}
|
|
66
86
|
return schema;
|
|
67
87
|
}
|
|
68
88
|
|
|
89
|
+
/** TypeBox 1.x resolves references through a Context keyed by Ref name instead of a `references` array. */
|
|
90
|
+
function referenceContext(references: readonly TSchema[]): Record<string, TSchema> {
|
|
91
|
+
const context: Record<string, TSchema> = {};
|
|
92
|
+
for (const reference of references) {
|
|
93
|
+
const id = (reference as SchemaMeta).$id;
|
|
94
|
+
if (typeof id === 'string' && id.length > 0) context[id] = reference;
|
|
95
|
+
}
|
|
96
|
+
return context;
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
export function createExactSchemaValidator<S extends TSchema>(schema: S, references: readonly TSchema[] = []) {
|
|
70
|
-
const checked = exactSchema(
|
|
71
|
-
const checkedReferences = references.map(reference => exactSchema(
|
|
100
|
+
const checked = exactSchema(Value.Clone(schema));
|
|
101
|
+
const checkedReferences = references.map(reference => exactSchema(Value.Clone(reference)));
|
|
102
|
+
const context = referenceContext(checkedReferences);
|
|
103
|
+
const hasReferences = Object.keys(context).length > 0;
|
|
72
104
|
return {
|
|
73
105
|
Check(value: unknown): value is Static<S> {
|
|
74
|
-
return
|
|
106
|
+
return hasReferences
|
|
107
|
+
? Value.Check(context, checked, value)
|
|
108
|
+
: Value.Check(checked, value);
|
|
75
109
|
},
|
|
76
110
|
Errors(value: unknown) {
|
|
77
|
-
return
|
|
111
|
+
return hasReferences
|
|
112
|
+
? Value.Errors(context, checked, value)
|
|
113
|
+
: Value.Errors(checked, value);
|
|
78
114
|
},
|
|
79
115
|
};
|
|
80
116
|
}
|
|
81
117
|
|
|
118
|
+
/** One normalized validation issue with a field-scoped JSON Pointer. */
|
|
119
|
+
export interface SchemaIssue {
|
|
120
|
+
path: string;
|
|
121
|
+
message: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SchemaErrorLike {
|
|
125
|
+
instancePath?: string;
|
|
126
|
+
path?: string;
|
|
127
|
+
keyword?: string;
|
|
128
|
+
params?: Record<string, unknown>;
|
|
129
|
+
message: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function escapePointerSegment(segment: string): string {
|
|
133
|
+
return segment.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* TypeBox 1.x reports Ajv-style errors that group missing properties into a single
|
|
138
|
+
* `required` issue. Normalize every error back into one field-scoped issue so callers
|
|
139
|
+
* can keep mapping messages onto individual form fields.
|
|
140
|
+
*/
|
|
141
|
+
export function schemaIssues(errors: Iterable<SchemaErrorLike>): SchemaIssue[] {
|
|
142
|
+
const issues: SchemaIssue[] = [];
|
|
143
|
+
for (const error of errors) {
|
|
144
|
+
const base = error.instancePath ?? error.path ?? '';
|
|
145
|
+
const required = error.params?.['requiredProperties'];
|
|
146
|
+
if (error.keyword === 'required' && Array.isArray(required)) {
|
|
147
|
+
for (const property of required) {
|
|
148
|
+
if (typeof property !== 'string') continue;
|
|
149
|
+
issues.push({
|
|
150
|
+
path: `${base}/${escapePointerSegment(property)}`,
|
|
151
|
+
message: `must have required property '${property}'`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const additional = error.params?.['additionalProperty'];
|
|
157
|
+
issues.push({
|
|
158
|
+
path: typeof additional === 'string' ? `${base}/${escapePointerSegment(additional)}` : base,
|
|
159
|
+
message: error.message,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return issues;
|
|
163
|
+
}
|
|
164
|
+
|
|
82
165
|
const validators = new WeakMap<TSchema, ReturnType<typeof createExactSchemaValidator>>();
|
|
83
166
|
|
|
84
167
|
/** Schemas are snapshotted on first use; later edits cannot weaken a live boundary. */
|
|
@@ -89,4 +172,4 @@ export function checkExact<S extends TSchema>(schema: S, value: unknown): value
|
|
|
89
172
|
validators.set(schema, validator);
|
|
90
173
|
}
|
|
91
174
|
return validator.Check(value);
|
|
92
|
-
}
|
|
175
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TObject, TSchema } from '
|
|
1
|
+
import type { TObject, TSchema } from 'typebox';
|
|
2
2
|
import { hashKey, useQueryClient } from '@tanstack/svelte-query';
|
|
3
3
|
import { captureAdminContext, type AdminContextAccessor } from './context.svelte';
|
|
4
4
|
import { commandDefinition, prepareCommand, parseCommandInput, parseCommandResponse,
|
package/src/task-contract.ts
CHANGED
package/src/task-provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { checkExact } from './schema-validation';
|
|
3
3
|
import { snapshotPlainData } from './plain-data';
|
|
4
4
|
import { TaskError, decodeTaskList, decodeTaskRecord, decodeTaskSubmitOptions, type SubmitTaskOptions } from './task-contract';
|
package/src/theme.svelte.ts
CHANGED
|
@@ -246,6 +246,8 @@ export interface ThemeConfig {
|
|
|
246
246
|
|
|
247
247
|
/** Explicit document-level values contributed by one mounted AdminApp. */
|
|
248
248
|
export interface ThemeOwnerOptions {
|
|
249
|
+
/** 仅在没有显式主题 owner 或 legacy 配置时使用,不接管其他实例的主题。 */
|
|
250
|
+
fallbackThemeConfig?: ThemeConfig;
|
|
249
251
|
themeConfig?: ThemeConfig;
|
|
250
252
|
defaultTheme?: ThemeMode;
|
|
251
253
|
}
|
|
@@ -285,6 +287,9 @@ function cloneThemeConfig(config: ThemeConfig={}): ThemeConfig {
|
|
|
285
287
|
|
|
286
288
|
function cloneOwnerOptions(options: ThemeOwnerOptions): ThemeOwnerOptions {
|
|
287
289
|
return definedOptions({
|
|
290
|
+
fallbackThemeConfig: options.fallbackThemeConfig === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: cloneThemeConfig(options.fallbackThemeConfig),
|
|
288
293
|
defaultTheme: options.defaultTheme,
|
|
289
294
|
themeConfig: options.themeConfig===undefined
|
|
290
295
|
? undefined
|
|
@@ -409,8 +414,10 @@ function applyCssOverrides(overrides: Record<string,string>): void {
|
|
|
409
414
|
function applyEffectiveTheme(): void {
|
|
410
415
|
const owner=getActiveOwner();
|
|
411
416
|
const nextConfig=owner
|
|
412
|
-
? cloneThemeConfig(owner.options.themeConfig)
|
|
413
|
-
:cloneThemeConfig(legacyThemeConfig)
|
|
417
|
+
? cloneThemeConfig(owner.options.themeConfig ?? owner.options.fallbackThemeConfig)
|
|
418
|
+
:cloneThemeConfig(Object.keys(legacyThemeConfig).length
|
|
419
|
+
? legacyThemeConfig
|
|
420
|
+
: themeOwners.at(-1)?.options.fallbackThemeConfig);
|
|
414
421
|
const nextMode=selectedMode??owner?.options.defaultTheme??'system';
|
|
415
422
|
const configuredPreset=!colorSelectionOverridesConfig&&nextConfig.colorPreset
|
|
416
423
|
? resolvePreset(nextConfig.colorPreset)
|
|
@@ -513,8 +520,8 @@ export function registerThemeOwner(options: ThemeOwnerOptions): ThemeOwnerToken|
|
|
|
513
520
|
|
|
514
521
|
if(isExplicitOwner(entry.options)) {
|
|
515
522
|
colorSelectionOverridesConfig=false;
|
|
516
|
-
applyEffectiveTheme();
|
|
517
523
|
}
|
|
524
|
+
if(isExplicitOwner(entry.options)||entry.options.fallbackThemeConfig) applyEffectiveTheme();
|
|
518
525
|
|
|
519
526
|
return token;
|
|
520
527
|
}
|
|
@@ -529,11 +536,12 @@ export function updateThemeOwner(
|
|
|
529
536
|
if(!owner) return;
|
|
530
537
|
|
|
531
538
|
const wasExplicit=isExplicitOwner(owner.options);
|
|
539
|
+
const hadFallback=owner.options.fallbackThemeConfig!==undefined;
|
|
532
540
|
owner.options=cloneOwnerOptions(options);
|
|
533
541
|
if(wasExplicit||isExplicitOwner(owner.options)) {
|
|
534
542
|
colorSelectionOverridesConfig=false;
|
|
535
|
-
applyEffectiveTheme();
|
|
536
543
|
}
|
|
544
|
+
if(wasExplicit||isExplicitOwner(owner.options)||hadFallback||owner.options.fallbackThemeConfig) applyEffectiveTheme();
|
|
537
545
|
}
|
|
538
546
|
|
|
539
547
|
/** Remove an owner safely regardless of its position in the stack. */
|
|
@@ -545,8 +553,8 @@ export function unregisterThemeOwner(token: ThemeOwnerToken|undefined): void {
|
|
|
545
553
|
const [removed]=themeOwners.splice(index,1);
|
|
546
554
|
if(removed&&isExplicitOwner(removed.options)) {
|
|
547
555
|
colorSelectionOverridesConfig=false;
|
|
548
|
-
applyEffectiveTheme();
|
|
549
556
|
}
|
|
557
|
+
if(removed&&(isExplicitOwner(removed.options)||removed.options.fallbackThemeConfig)) applyEffectiveTheme();
|
|
550
558
|
}
|
|
551
559
|
|
|
552
560
|
export function resetTheme(): void {
|
package/src/update-contract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from '
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
2
|
import { useContractForm, type ContractFormOptions } from './contract-form.svelte';
|
|
3
3
|
import { getContractFormFields, type ContractSchemas, type ContractFormValues } from './resource-contract';
|
|
4
4
|
import { snapshotPlainData } from './plain-data';
|