import { omitKeys } from "bknd/utils"; import type { EntityManager } from "data/entities"; import { TransformPersistFailedException } from "../errors"; import { baseFieldConfigSchema, Field, type TActionContext, type TRenderContext } from "./Field"; import type { TFieldTSType } from "data/entities/EntityTypescript"; import { s } from "bknd/utils"; export const enumFieldConfigSchema = s .strictObject({ default_value: s.string(), options: s.anyOf([ s.object({ type: s.literal("strings"), values: s.array(s.string()), }), s.object({ type: s.literal("objects"), values: s.array( s.object({ label: s.string(), value: s.string(), }), ), }), ]), ...omitKeys(baseFieldConfigSchema.properties, ["default_value"]), }) .partial(); export type EnumFieldConfig = s.Static; export class EnumField extends Field< EnumFieldConfig, TypeOverride, Required > { override readonly type = "enum"; constructor(name: string, config: Partial) { super(name, config); if (this.config.default_value && !this.isValidValue(this.config.default_value)) { throw new Error(`Default value "${this.config.default_value}" is not a valid option`); } } protected getSchema() { return enumFieldConfigSchema; } getOptions(): { label: string; value: string }[] { const options = this.config?.options ?? { type: "strings", values: [] }; if (options.type === "strings") { return options.values?.map((option) => ({ label: option, value: option })); } return options?.values; } isValidValue(value: string): boolean { const valid_values = this.getOptions().map((option) => option.value); return valid_values.includes(value); } override getValue(value: any, context: TRenderContext) { if (!this.isValidValue(value)) { return this.hasDefault() ? this.getDefault() : null; } switch (context) { case "table": return this.getOptions().find((option) => option.value === value)?.label ?? value; } return value; } /** * Transform value after retrieving from database * @param value */ override transformRetrieve(value: string | null): string | null { const val = super.transformRetrieve(value); if (val === null && this.hasDefault()) { return this.getDefault(); } if (!this.isValidValue(val)) { return this.hasDefault() ? this.getDefault() : null; } 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.isValidValue(value)) { throw new TransformPersistFailedException( `Field "${this.name}" must be one of the following values: ${this.getOptions() .map((o) => o.value) .join(", ")}`, ); } return value; } override toJsonSchema() { const options = this.config?.options ?? { type: "strings", values: [] }; const values = options.values?.map((option) => (typeof option === "string" ? option : option.value)) ?? []; return this.toSchemaWrapIfRequired( s.string({ enum: values, default: this.getDefault(), }), ); } override toType(): TFieldTSType { const union = this.getOptions().map(({ value }) => typeof value === "string" ? `"${value}"` : value, ); return { ...super.toType(), type: union.length > 0 ? union.join(" | ") : "string", }; } }