mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
ensure `getJsonSchema` handles both object and non-object outputs to prevent errors during validation initialization. this improves robustness when handling edge cases in schema configurations.
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema";
|
|
import { objectToJsLiteral } from "bknd/utils";
|
|
import type { EntityManager } from "data/entities";
|
|
import { TransformPersistFailedException } from "../errors";
|
|
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
|
|
import type { TFieldTSType } from "data/entities/EntityTypescript";
|
|
import { s } from "bknd/utils";
|
|
|
|
export const jsonSchemaFieldConfigSchema = s
|
|
.strictObject({
|
|
schema: s.any({ type: "object" }),
|
|
ui_schema: s.any({ type: "object" }),
|
|
default_from_schema: s.boolean(),
|
|
...baseFieldConfigSchema.properties,
|
|
})
|
|
.partial();
|
|
|
|
export type JsonSchemaFieldConfig = s.Static<typeof jsonSchemaFieldConfigSchema>;
|
|
|
|
export class JsonSchemaField<
|
|
Required extends true | false = false,
|
|
TypeOverride = object,
|
|
> extends Field<JsonSchemaFieldConfig, TypeOverride, Required> {
|
|
override readonly type = "jsonschema";
|
|
private validator: Validator;
|
|
|
|
constructor(name: string, config: Partial<JsonSchemaFieldConfig>) {
|
|
super(name, config);
|
|
|
|
// make sure to hand over clean json
|
|
const schema = this.getJsonSchema();
|
|
this.validator = new Validator(
|
|
typeof schema === "object" ? JSON.parse(JSON.stringify(schema)) : {},
|
|
);
|
|
}
|
|
|
|
protected getSchema() {
|
|
return jsonSchemaFieldConfigSchema;
|
|
}
|
|
|
|
getJsonSchema(): JsonSchema {
|
|
return this.config?.schema as JsonSchema;
|
|
}
|
|
|
|
getJsonUiSchema() {
|
|
return this.config.ui_schema ?? {};
|
|
}
|
|
|
|
override isValid(value: any, context: TActionContext = "update"): boolean {
|
|
const parentValid = super.isValid(value, context);
|
|
|
|
if (parentValid) {
|
|
// already checked in parent
|
|
if (!this.isRequired() && (!value || typeof value !== "object")) {
|
|
return true;
|
|
}
|
|
|
|
const result = this.validator.validate(value);
|
|
return result.valid;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
override getValue(value: any, context: TRenderContext): any {
|
|
switch (context) {
|
|
case "form":
|
|
if (value === null) return "";
|
|
return value;
|
|
case "table":
|
|
if (value === null) return null;
|
|
return JSON.stringify(value);
|
|
case "submit":
|
|
break;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
override transformRetrieve(value: any): any {
|
|
const val = super.transformRetrieve(value);
|
|
|
|
if (val === null) {
|
|
if (this.config.default_from_schema) {
|
|
try {
|
|
return s.fromSchema(this.getJsonSchema()).template();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
} else if (this.hasDefault()) {
|
|
return this.getDefault();
|
|
}
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
override async transformPersist(
|
|
_value: any,
|
|
em: EntityManager<any>,
|
|
context: TActionContext,
|
|
): Promise<string | undefined> {
|
|
const value = await super.transformPersist(_value, em, context);
|
|
if (this.nullish(value)) return value;
|
|
|
|
if (!this.isValid(value)) {
|
|
throw new TransformPersistFailedException(this.name, value);
|
|
}
|
|
|
|
if (!value || typeof value !== "object") return this.getDefault();
|
|
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
override toJsonSchema() {
|
|
const schema = this.getJsonSchema() ?? { type: "object" };
|
|
return this.toSchemaWrapIfRequired(
|
|
s.fromSchema({
|
|
default: this.getDefault(),
|
|
...schema,
|
|
}),
|
|
);
|
|
}
|
|
|
|
override toType(): TFieldTSType {
|
|
return {
|
|
...super.toType(),
|
|
import: [{ package: "json-schema-to-ts", name: "FromSchema" }],
|
|
type: `FromSchema<${objectToJsLiteral(this.getJsonSchema(), 2, 1)}>`,
|
|
};
|
|
}
|
|
}
|