mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
perf: optimize plugin schema validation
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
b0424fd44d888d28f7f4ab0f653e5ae37f6ae61aad298b759ea0531edccb4405 plugin-sdk-api-baseline.json
|
||||
82a080f2ec0455f1496391dc35534545b07181655ef5d3845e8c86eda7979501 plugin-sdk-api-baseline.jsonl
|
||||
dfdecb3918124ec7926ffe17220e498ffeef2fc7a7edfea528cc5a7f284cb8ef plugin-sdk-api-baseline.json
|
||||
079c31016f34256af290f80f3e16d6f8154eb13513d36547ba41d3241d60e0e4 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -399,6 +399,20 @@ const accountSchema = z.object({
|
||||
const configSchema = buildChannelConfigSchema(accountSchema);
|
||||
```
|
||||
|
||||
If you already author the contract as JSON Schema or TypeBox, use the direct helper so OpenClaw can skip Zod-to-JSON-Schema conversion on metadata paths:
|
||||
|
||||
```typescript
|
||||
import { Type } from "typebox";
|
||||
import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
|
||||
const configSchema = buildJsonChannelConfigSchema(
|
||||
Type.Object({
|
||||
token: Type.Optional(Type.String()),
|
||||
allowFrom: Type.Optional(Type.Array(Type.String())),
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
For third-party plugins, the cold-path contract is still the plugin manifest: mirror the generated JSON Schema into `openclaw.plugin.json#channelConfigs` so config schema, setup, and UI surfaces can inspect `channels.<id>` without loading runtime code.
|
||||
|
||||
## Setup wizards
|
||||
|
||||
@@ -22,7 +22,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| Subpath | Key exports |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `plugin-sdk/plugin-entry` | `definePluginEntry` |
|
||||
| `plugin-sdk/core` | `defineChannelPluginEntry`, `createChatChannelPlugin`, `createChannelPluginBase`, `defineSetupPluginEntry`, `buildChannelConfigSchema` |
|
||||
| `plugin-sdk/core` | `defineChannelPluginEntry`, `createChatChannelPlugin`, `createChannelPluginBase`, `defineSetupPluginEntry`, `buildChannelConfigSchema`, `buildJsonChannelConfigSchema` |
|
||||
| `plugin-sdk/config-schema` | `OpenClawSchema` |
|
||||
| `plugin-sdk/provider-entry` | `defineSingleProviderPluginEntry` |
|
||||
| `plugin-sdk/testing` | Broad compatibility barrel for legacy plugin tests; prefer focused test subpaths for new extension tests |
|
||||
@@ -58,7 +58,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/channel-pairing` | `createChannelPairingController` |
|
||||
| `plugin-sdk/channel-reply-pipeline` | `createChannelReplyPipeline`, `resolveChannelSourceReplyDeliveryMode` |
|
||||
| `plugin-sdk/channel-config-helpers` | `createHybridChannelConfigAdapter`, `resolveChannelDmAccess`, `resolveChannelDmAllowFrom`, `resolveChannelDmPolicy`, `normalizeChannelDmPolicy`, `normalizeLegacyDmAliases` |
|
||||
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives and generic builder |
|
||||
| `plugin-sdk/channel-config-schema` | Shared channel config schema primitives plus Zod and direct JSON/TypeBox builders |
|
||||
| `plugin-sdk/bundled-channel-config-schema` | Bundled OpenClaw channel config schemas for maintained bundled plugins only |
|
||||
| `plugin-sdk/channel-config-schema-legacy` | Deprecated compatibility alias for bundled-channel config schemas |
|
||||
| `plugin-sdk/telegram-command-config` | Telegram custom-command normalization/validation helpers with bundled-contract fallback |
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { buildChannelConfigSchema, emptyChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
buildJsonChannelConfigSchema,
|
||||
emptyChannelConfigSchema,
|
||||
} from "./config-schema.js";
|
||||
|
||||
describe("buildChannelConfigSchema", () => {
|
||||
it("builds json schema when toJSONSchema is available", () => {
|
||||
@@ -47,6 +51,37 @@ describe("buildChannelConfigSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildJsonChannelConfigSchema", () => {
|
||||
it("validates direct JSON schemas without zod conversion", () => {
|
||||
const result = buildJsonChannelConfigSchema(
|
||||
{
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: "boolean", default: true },
|
||||
},
|
||||
},
|
||||
{ cacheKey: "config-schema.test.json-channel" },
|
||||
);
|
||||
|
||||
expect(result.schema).toEqual({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: "boolean", default: true },
|
||||
},
|
||||
});
|
||||
expect(result.runtime?.safeParse({})).toEqual({
|
||||
success: true,
|
||||
data: { enabled: true },
|
||||
});
|
||||
expect(result.runtime?.safeParse({ enabled: "yes" })).toEqual({
|
||||
success: false,
|
||||
issues: [{ path: ["enabled"], message: "must be boolean" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("emptyChannelConfigSchema", () => {
|
||||
it("accepts undefined and empty objects only", () => {
|
||||
const result = emptyChannelConfigSchema();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z, type ZodRawShape, type ZodTypeAny } from "zod";
|
||||
import { DmPolicySchema } from "../../config/zod-schema.core.js";
|
||||
import { validateJsonSchemaValue } from "../../plugins/schema-validator.js";
|
||||
import type { JsonSchemaObject } from "../../shared/json-schema.types.js";
|
||||
import type {
|
||||
ChannelConfigRuntimeIssue,
|
||||
@@ -41,6 +42,12 @@ type BuildChannelConfigSchemaOptions = {
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
};
|
||||
|
||||
type BuildJsonChannelConfigSchemaOptions = {
|
||||
cacheKey?: string;
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
runtime?: ChannelConfigSchema["runtime"];
|
||||
};
|
||||
|
||||
function cloneRuntimeIssue(issue: unknown): ChannelConfigRuntimeIssue {
|
||||
const record = issue && typeof issue === "object" ? (issue as Record<string, unknown>) : {};
|
||||
const path = Array.isArray(record.path)
|
||||
@@ -72,6 +79,53 @@ function safeParseRuntimeSchema(
|
||||
};
|
||||
}
|
||||
|
||||
function toIssuePath(path: string): Array<string | number> {
|
||||
if (!path || path === "<root>") {
|
||||
return [];
|
||||
}
|
||||
return path.split(".").map((segment) => {
|
||||
const index = Number(segment);
|
||||
return Number.isInteger(index) && String(index) === segment ? index : segment;
|
||||
});
|
||||
}
|
||||
|
||||
function safeParseJsonSchema(
|
||||
schema: JsonSchemaObject,
|
||||
cacheKey: string,
|
||||
value: unknown,
|
||||
): ChannelConfigRuntimeParseResult {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema,
|
||||
cacheKey,
|
||||
value,
|
||||
applyDefaults: true,
|
||||
});
|
||||
if (result.ok) {
|
||||
return { success: true, data: result.value };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
issues: result.errors.map((issue) => ({
|
||||
path: toIssuePath(issue.path),
|
||||
message: issue.message,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildJsonChannelConfigSchema(
|
||||
schema: JsonSchemaObject,
|
||||
options?: BuildJsonChannelConfigSchemaOptions,
|
||||
): ChannelConfigSchema {
|
||||
return {
|
||||
schema,
|
||||
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
|
||||
runtime: options?.runtime ?? {
|
||||
safeParse: (value) =>
|
||||
safeParseJsonSchema(schema, options?.cacheKey ?? "channel-config-schema:json", value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildChannelConfigSchema(
|
||||
schema: ZodTypeAny,
|
||||
options?: BuildChannelConfigSchemaOptions,
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
AllowFromListSchema,
|
||||
buildChannelConfigSchema,
|
||||
buildCatchallMultiAccountChannelSchema,
|
||||
buildJsonChannelConfigSchema,
|
||||
buildNestedDmConfigSchema,
|
||||
} from "../channels/plugins/config-schema.js";
|
||||
export {
|
||||
|
||||
@@ -181,7 +181,11 @@ export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
|
||||
export { definePluginEntry } from "./plugin-entry.js";
|
||||
export { buildPluginConfigSchema, emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export {
|
||||
buildJsonPluginConfigSchema,
|
||||
buildPluginConfigSchema,
|
||||
emptyPluginConfigSchema,
|
||||
} from "../plugins/config-schema.js";
|
||||
export { KeyedAsyncQueue, enqueueKeyedTask } from "./keyed-async-queue.js";
|
||||
export { createDedupeCache, resolveGlobalDedupeCache } from "../infra/dedupe.js";
|
||||
export { generateSecureToken, generateSecureUuid } from "../infra/secure-random.js";
|
||||
@@ -192,6 +196,7 @@ export {
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
export {
|
||||
buildChannelConfigSchema,
|
||||
buildJsonChannelConfigSchema,
|
||||
emptyChannelConfigSchema,
|
||||
} from "../channels/plugins/config-schema.js";
|
||||
export {
|
||||
|
||||
@@ -223,7 +223,11 @@ export type {
|
||||
export type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
export type { OpenClawConfig };
|
||||
|
||||
export { buildPluginConfigSchema, emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||
export {
|
||||
buildJsonPluginConfigSchema,
|
||||
buildPluginConfigSchema,
|
||||
emptyPluginConfigSchema,
|
||||
} from "../plugins/config-schema.js";
|
||||
|
||||
/** Options for a plugin entry that registers providers, tools, commands, or services. */
|
||||
type DefinePluginEntryOptions = {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
buildJsonChannelConfigSchema,
|
||||
} from "../channels/plugins/config-schema.js";
|
||||
import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js";
|
||||
import type { JsonSchemaObject } from "../shared/json-schema.types.js";
|
||||
import {
|
||||
@@ -46,6 +49,24 @@ function isBuiltChannelConfigSchema(value: unknown): value is ChannelConfigSurfa
|
||||
return Boolean(candidate.schema && typeof candidate.schema === "object");
|
||||
}
|
||||
|
||||
function isJsonSchemaConfigSurface(value: unknown): value is JsonSchemaObject {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as Record<string, unknown>;
|
||||
if (typeof candidate.safeParse === "function" || typeof candidate.toJSONSchema === "function") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof candidate.type === "string" ||
|
||||
Array.isArray(candidate.anyOf) ||
|
||||
Array.isArray(candidate.oneOf) ||
|
||||
Array.isArray(candidate.allOf) ||
|
||||
Array.isArray(candidate.enum) ||
|
||||
Object.prototype.hasOwnProperty.call(candidate, "const")
|
||||
);
|
||||
}
|
||||
|
||||
function resolveConfigSchemaExport(imported: Record<string, unknown>): ChannelConfigSurface | null {
|
||||
for (const [name, value] of Object.entries(imported)) {
|
||||
if (name.endsWith("ChannelConfigSchema") && isBuiltChannelConfigSchema(value)) {
|
||||
@@ -60,6 +81,9 @@ function resolveConfigSchemaExport(imported: Record<string, unknown>): ChannelCo
|
||||
if (isBuiltChannelConfigSchema(value)) {
|
||||
return value;
|
||||
}
|
||||
if (isJsonSchemaConfigSurface(value)) {
|
||||
return buildJsonChannelConfigSchema(value);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return buildChannelConfigSchema(value as never);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { buildPluginConfigSchema, emptyPluginConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
buildJsonPluginConfigSchema,
|
||||
buildPluginConfigSchema,
|
||||
emptyPluginConfigSchema,
|
||||
} from "./config-schema.js";
|
||||
|
||||
function expectSafeParseCases(
|
||||
safeParse: ((value: unknown) => unknown) | undefined,
|
||||
@@ -83,6 +87,37 @@ describe("buildPluginConfigSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildJsonPluginConfigSchema", () => {
|
||||
it("validates direct JSON schemas without zod conversion", () => {
|
||||
const result = buildJsonPluginConfigSchema(
|
||||
{
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: "boolean", default: true },
|
||||
},
|
||||
},
|
||||
{ cacheKey: "config-schema.test.json-plugin" },
|
||||
);
|
||||
|
||||
expect(result.jsonSchema).toEqual({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: "boolean", default: true },
|
||||
},
|
||||
});
|
||||
expect(result.safeParse?.({})).toEqual({
|
||||
success: true,
|
||||
data: { enabled: true },
|
||||
});
|
||||
expect(result.safeParse?.({ enabled: "yes" })).toEqual({
|
||||
success: false,
|
||||
error: { issues: [{ path: ["enabled"], message: "must be boolean" }] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("emptyPluginConfigSchema", () => {
|
||||
it("accepts undefined and empty objects only", () => {
|
||||
const schema = emptyPluginConfigSchema();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z, type ZodTypeAny } from "zod";
|
||||
import type { JsonSchemaObject } from "../shared/json-schema.types.js";
|
||||
import type { PluginConfigUiHint } from "./manifest-types.js";
|
||||
import { validateJsonSchemaValue } from "./schema-validator.js";
|
||||
import type { OpenClawPluginConfigSchema } from "./types.js";
|
||||
|
||||
type Issue = { path: Array<string | number>; message: string };
|
||||
@@ -18,6 +19,12 @@ type BuildPluginConfigSchemaOptions = {
|
||||
safeParse?: OpenClawPluginConfigSchema["safeParse"];
|
||||
};
|
||||
|
||||
type BuildJsonPluginConfigSchemaOptions = {
|
||||
cacheKey?: string;
|
||||
uiHints?: Record<string, PluginConfigUiHint>;
|
||||
safeParse?: OpenClawPluginConfigSchema["safeParse"];
|
||||
};
|
||||
|
||||
function error(message: string): SafeParseResult {
|
||||
return { success: false, error: { issues: [{ path: [], message }] } };
|
||||
}
|
||||
@@ -77,6 +84,56 @@ function normalizeJsonSchema(schema: unknown): unknown {
|
||||
return record;
|
||||
}
|
||||
|
||||
function toIssuePath(path: string): Array<string | number> {
|
||||
if (!path || path === "<root>") {
|
||||
return [];
|
||||
}
|
||||
return path.split(".").map((segment) => {
|
||||
const index = Number(segment);
|
||||
return Number.isInteger(index) && String(index) === segment ? index : segment;
|
||||
});
|
||||
}
|
||||
|
||||
function safeParseJsonSchema(
|
||||
schema: JsonSchemaObject,
|
||||
cacheKey: string,
|
||||
value: unknown,
|
||||
): SafeParseResult {
|
||||
const result = validateJsonSchemaValue({
|
||||
schema,
|
||||
cacheKey,
|
||||
value,
|
||||
applyDefaults: true,
|
||||
});
|
||||
if (result.ok) {
|
||||
return { success: true, data: result.value };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.errors.map((issue) => ({
|
||||
path: toIssuePath(issue.path),
|
||||
message: issue.message,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildJsonPluginConfigSchema(
|
||||
schema: JsonSchemaObject,
|
||||
options?: BuildJsonPluginConfigSchemaOptions,
|
||||
): OpenClawPluginConfigSchema {
|
||||
const safeParse =
|
||||
options?.safeParse ??
|
||||
((value: unknown) =>
|
||||
safeParseJsonSchema(schema, options?.cacheKey ?? "plugin-config-schema:json", value));
|
||||
return {
|
||||
safeParse,
|
||||
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
|
||||
jsonSchema: normalizeJsonSchema(schema) as JsonSchemaObject,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPluginConfigSchema(
|
||||
schema: ZodTypeAny,
|
||||
options?: BuildPluginConfigSchemaOptions,
|
||||
|
||||
@@ -65,6 +65,29 @@ function expectUriValidationCase(params: {
|
||||
|
||||
describe("schema validator", () => {
|
||||
it("can apply JSON Schema defaults while validating", () => {
|
||||
const value = {};
|
||||
const result = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.defaults.clone",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
type: "string",
|
||||
default: "auto",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
value,
|
||||
applyDefaults: true,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value).toEqual({ mode: "auto" });
|
||||
expect(result.value).not.toBe(value);
|
||||
}
|
||||
expect(value).toEqual({});
|
||||
|
||||
expectSuccessfulValidationValue({
|
||||
input: {
|
||||
cacheKey: "schema-validator.test.defaults",
|
||||
@@ -85,6 +108,44 @@ describe("schema validator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not clone values when default application has no defaults to inject", () => {
|
||||
const value = { mode: "manual" };
|
||||
const result = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.defaults.no-clone",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
value,
|
||||
applyDefaults: true,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.value).toBe(value);
|
||||
}
|
||||
});
|
||||
|
||||
it("recompiles when a stable cache key receives a different schema shape", () => {
|
||||
const cacheKey = "schema-validator.test.cache-key-drift";
|
||||
expectValidationSuccess({
|
||||
cacheKey,
|
||||
schema: { type: "string" },
|
||||
value: "ok",
|
||||
});
|
||||
|
||||
const result = expectValidationFailure({
|
||||
cacheKey,
|
||||
schema: { type: "number" },
|
||||
value: "not-a-number",
|
||||
});
|
||||
expectValidationIssue(result, "<root>");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: "includes allowed values in enum validation errors",
|
||||
|
||||
@@ -49,12 +49,32 @@ function getAjv(mode: "default" | "defaults"): AjvLike {
|
||||
}
|
||||
|
||||
type CachedValidator = {
|
||||
hasDefaults: boolean;
|
||||
validate: ValidateFunction;
|
||||
schema: JsonSchemaObject;
|
||||
schemaFingerprint: string;
|
||||
};
|
||||
|
||||
const schemaCache = new PluginLruCache<CachedValidator>(512);
|
||||
|
||||
function fingerprintSchema(schema: JsonSchemaObject): string {
|
||||
return JSON.stringify(schema);
|
||||
}
|
||||
|
||||
function schemaHasDefaults(schema: unknown): boolean {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(schema)) {
|
||||
return schema.some((item) => schemaHasDefaults(item));
|
||||
}
|
||||
const record = schema as Record<string, unknown>;
|
||||
if (Object.prototype.hasOwnProperty.call(record, "default")) {
|
||||
return true;
|
||||
}
|
||||
return Object.values(record).some((value) => schemaHasDefaults(value));
|
||||
}
|
||||
|
||||
function cloneValidationValue<T>(value: T): T {
|
||||
if (value === undefined || value === null) {
|
||||
return value;
|
||||
@@ -167,13 +187,26 @@ export function validateJsonSchemaValue(params: {
|
||||
}): { ok: true; value: unknown } | { ok: false; errors: JsonSchemaValidationError[] } {
|
||||
const cacheKey = params.applyDefaults ? `${params.cacheKey}::defaults` : params.cacheKey;
|
||||
let cached = schemaCache.get(cacheKey);
|
||||
if (!cached || cached.schema !== params.schema) {
|
||||
const schemaFingerprint =
|
||||
!cached || cached.schema !== params.schema ? fingerprintSchema(params.schema) : undefined;
|
||||
if (
|
||||
!cached ||
|
||||
(cached.schema !== params.schema && cached.schemaFingerprint !== schemaFingerprint)
|
||||
) {
|
||||
const validate = getAjv(params.applyDefaults ? "defaults" : "default").compile(params.schema);
|
||||
cached = { validate, schema: params.schema };
|
||||
cached = {
|
||||
hasDefaults: params.applyDefaults ? schemaHasDefaults(params.schema) : false,
|
||||
validate,
|
||||
schema: params.schema,
|
||||
schemaFingerprint: schemaFingerprint ?? fingerprintSchema(params.schema),
|
||||
};
|
||||
schemaCache.set(cacheKey, cached);
|
||||
} else if (cached.schema !== params.schema) {
|
||||
cached.schema = params.schema;
|
||||
}
|
||||
|
||||
const value = params.applyDefaults ? cloneValidationValue(params.value) : params.value;
|
||||
const value =
|
||||
params.applyDefaults && cached.hasDefaults ? cloneValidationValue(params.value) : params.value;
|
||||
const ok = cached.validate(value);
|
||||
if (ok) {
|
||||
return { ok: true, value };
|
||||
|
||||
Reference in New Issue
Block a user