Files
bknd/app/src/data/fields/JsonField.ts
2025-10-28 16:00:58 +01:00

117 lines
2.9 KiB
TypeScript

import { omitKeys } 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 jsonFieldConfigSchema = s
.strictObject({
default_value: s.any(),
...omitKeys(baseFieldConfigSchema.properties, ["default_value"]),
})
.partial();
export type JsonFieldConfig = s.Static<typeof jsonFieldConfigSchema>;
export class JsonField<Required extends true | false = false, TypeOverride = object> extends Field<
JsonFieldConfig,
TypeOverride,
Required
> {
override readonly type = "json";
protected getSchema() {
return jsonFieldConfigSchema;
}
/**
* Transform value after retrieving from database
* @param value
*/
override transformRetrieve(value: any): any {
const val = super.transformRetrieve(value);
if (val === null && this.hasDefault()) {
return this.getDefault();
}
if (this.isSerialized(val)) {
return JSON.parse(val);
}
return val;
}
isSerializable(value: any) {
try {
const stringified = JSON.stringify(value);
if (stringified === JSON.stringify(JSON.parse(stringified))) {
return true;
}
} catch (e) {}
return false;
}
isSerialized(value: any) {
try {
if (typeof value === "string") {
return value === JSON.stringify(JSON.parse(value));
}
} catch (e) {}
return false;
}
override isValid(value: any): boolean {
return this.isSerializable(value);
}
override getValue(value: any, context: TRenderContext): any {
switch (context) {
case "table":
if (value === null) return null;
return JSON.stringify(value);
case "submit":
if (!value || (typeof value === "string" && value.length === 0)) {
return null;
} else if (typeof value === "object") {
return value;
}
return JSON.parse(value);
}
return value;
}
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.isSerializable(value)) {
throw new TransformPersistFailedException(
`Field "${this.name}" must be serializable to JSON.`,
);
}
if (this.isSerialized(value)) {
return value;
}
return JSON.stringify(value);
}
override toType(): TFieldTSType {
return {
...super.toType(),
type: "any",
};
}
}