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; export class JsonSchemaField< Required extends true | false = false, TypeOverride = object, > extends Field { override readonly type = "jsonschema"; private validator: Validator; constructor(name: string, config: Partial) { 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, context: TActionContext, ): Promise { 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)}>`, }; } }