Files
bknd/app/src/data/fields/EnumField.ts
2025-03-07 15:02:19 +01:00

150 lines
4.3 KiB
TypeScript

import { Const, type Static, StringEnum, StringRecord, Type } from "core/utils";
import type { EntityManager } from "data";
import { TransformPersistFailedException } from "../errors";
import { Field, type TActionContext, type TRenderContext, baseFieldConfigSchema } from "./Field";
export const enumFieldConfigSchema = Type.Composite(
[
Type.Object({
default_value: Type.Optional(Type.String()),
options: Type.Optional(
Type.Union([
Type.Object(
{
type: Const("strings"),
values: Type.Array(Type.String()),
},
{ title: "Strings" },
),
Type.Object(
{
type: Const("objects"),
values: Type.Array(
Type.Object({
label: Type.String(),
value: Type.String(),
}),
),
},
{
title: "Objects",
additionalProperties: false,
},
),
]),
),
}),
baseFieldConfigSchema,
],
{
additionalProperties: false,
},
);
export type EnumFieldConfig = Static<typeof enumFieldConfigSchema>;
export class EnumField<Required extends true | false = false, TypeOverride = string> extends Field<
EnumFieldConfig,
TypeOverride,
Required
> {
override readonly type = "enum";
constructor(name: string, config: Partial<EnumFieldConfig>) {
super(name, config);
/*if (this.config.options.values.length === 0) {
throw new Error(`Enum field "${this.name}" requires at least one option`);
}*/
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.values?.length === 0) {
throw new Error(`Enum field "${this.name}" requires at least one option`);
}*/
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<any>,
context: TActionContext,
): Promise<string | undefined> {
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(
StringEnum(values, {
default: this.getDefault(),
}),
);
}
}