[fabric/domain] Simplify model field definitions

This commit is contained in:
Pablo Baleztena 2024-10-23 23:52:12 -03:00
parent 36b5286a09
commit 6329044415
18 changed files with 165 additions and 330 deletions

View File

@ -1,11 +1,12 @@
import { import {
Decimal,
PosixDate, PosixDate,
Result, Result,
TaggedError, TaggedError,
type VariantFromTag, type VariantFromTag,
} from "@fabric/core"; } from "@fabric/core";
import { isUUID, parseAndSanitizeString } from "@fabric/validations"; import { isUUID, parseAndSanitizeString } from "@fabric/validations";
import type { FieldDefinition, FieldToType } from "../index.ts"; import type { FieldDefinition, FieldToType } from "./index.ts";
export type FieldParsers = { export type FieldParsers = {
[K in FieldDefinition["_tag"]]: FieldParser< [K in FieldDefinition["_tag"]]: FieldParser<
@ -32,46 +33,56 @@ export const fieldParsers: FieldParsers = {
: Result.failWith(new InvalidFieldTypeError()) : Result.failWith(new InvalidFieldTypeError())
); );
}, },
TimestampField: (f, v) => { PosixDateField: (f, v) => {
return parseOptionality(f, v).flatMap((parsedValue) => { return parseOptionality(f, v, (v) => {
if (parsedValue === undefined) return Result.ok(undefined); if (v instanceof PosixDate) {
if (!(v instanceof PosixDate)) {
return Result.failWith(new InvalidFieldTypeError());
}
return Result.ok(v); return Result.ok(v);
}
return Result.failWith(new InvalidFieldTypeError());
}); });
}, },
BooleanField: (f, v) => { BooleanField: (f, v) => {
if (!f.isOptional && v === undefined) { return parseOptionality(f, v, (v) => {
return Result.failWith(new MissingRequiredFieldError());
}
if (v === undefined) {
return Result.ok(undefined);
}
if (typeof v === "boolean") { if (typeof v === "boolean") {
return Result.ok(v); return Result.ok(v);
} }
return Result.failWith(new InvalidFieldTypeError()); return Result.failWith(new InvalidFieldTypeError());
});
}, },
IntegerField: function (): Result< IntegerField: (f, v) => {
number | bigint | undefined, return parseOptionality(f, v, (v) => {
FieldParsingError if (
> { (typeof v === "number" &&
throw new Error("Function not implemented."); Number.isInteger(v)) || (typeof v === "bigint")
) {
return Result.ok(v);
}
return Result.failWith(new InvalidFieldTypeError());
});
}, },
FloatField: function () { FloatField: (f, v) => {
throw new Error("Function not implemented."); return parseOptionality(f, v, (v) => {
if (typeof v === "number") {
return Result.ok(v);
}
return Result.failWith(new InvalidFieldTypeError());
});
}, },
DecimalField: function () { DecimalField: (f, v) => {
throw new Error("Function not implemented."); return parseOptionality(f, v, (v) => {
if (v instanceof Decimal) {
return Result.ok(v);
}
return Result.failWith(new InvalidFieldTypeError());
});
}, },
EmbeddedField: function () { EmbeddedField: function () {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
EmailField: function () {
throw new Error("Function not implemented.");
},
}; };
/** /**
@ -125,10 +136,17 @@ function parseStringValue(
*/ */
function parseOptionality<T>( function parseOptionality<T>(
field: FieldDefinition, field: FieldDefinition,
value: T | undefined, value: unknown,
withMapping?: (value: unknown) => Result<T, FieldParsingError>,
): Result<T | undefined, FieldParsingError> { ): Result<T | undefined, FieldParsingError> {
if (!field.isOptional && value === undefined) { if (!field.isOptional && value === undefined) {
return Result.failWith(new MissingRequiredFieldError()); return Result.failWith(new MissingRequiredFieldError());
} }
if (value === undefined) {
return Result.ok(value); return Result.ok(value);
} }
if (!withMapping) {
return Result.ok(value as T);
}
return withMapping(value);
}

View File

@ -0,0 +1,108 @@
// deno-lint-ignore-file no-explicit-any
import type {
Decimal,
Email,
PosixDate,
TaggedVariant,
UUID,
} from "@fabric/core";
import { variantConstructor } from "@fabric/core";
export const Field = {
string: variantConstructor<StringField>("StringField"),
uuid: variantConstructor<UUIDField>("UUIDField"),
integer: variantConstructor<IntegerField>("IntegerField"),
float: variantConstructor<FloatField>("FloatField"),
decimal: variantConstructor<DecimalField>("DecimalField"),
reference: variantConstructor<ReferenceField>("ReferenceField"),
posixDate: variantConstructor<PosixDateField>("PosixDateField"),
embedded: variantConstructor<EmbeddedField>("EmbeddedField"),
boolean: variantConstructor<BooleanField>("BooleanField"),
email: variantConstructor<EmailField>("EmailField"),
} as const;
export type FieldDefinition =
| StringField
| UUIDField
| IntegerField
| FloatField
| DecimalField
| ReferenceField
| PosixDateField
| EmbeddedField
| EmailField
| BooleanField;
/**
* Converts a field definition to its corresponding TypeScript type.
*/
//prettier-ignore
export type FieldToType<TField> = TField extends StringField
? MaybeOptional<TField, string>
: TField extends UUIDField ? MaybeOptional<TField, UUID>
: TField extends IntegerField ? IntegerFieldToType<TField>
: TField extends ReferenceField ? MaybeOptional<TField, UUID>
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
: TField extends FloatField ? MaybeOptional<TField, number>
: TField extends PosixDateField ? MaybeOptional<TField, PosixDate>
: TField extends BooleanField ? MaybeOptional<TField, boolean>
: TField extends EmailField ? MaybeOptional<TField, Email>
: TField extends EmbeddedField<infer TSubModel>
? MaybeOptional<TField, TSubModel>
: never;
//prettier-ignore
type IntegerFieldToType<TField extends IntegerField> =
TField["hasArbitraryPrecision"] extends true ? MaybeOptional<TField, bigint>
: TField["hasArbitraryPrecision"] extends false
? MaybeOptional<TField, number>
: MaybeOptional<TField, number | bigint>;
type MaybeOptional<TField, TType> = TField extends { isOptional: true }
? TType | undefined
: TType;
interface BaseField {
isOptional?: boolean;
isUnique?: boolean;
isIndexed?: boolean;
}
export interface UUIDField extends TaggedVariant<"UUIDField">, BaseField {
isPrimaryKey?: boolean;
}
export interface PosixDateField
extends TaggedVariant<"PosixDateField">, BaseField {}
export interface BooleanField
extends TaggedVariant<"BooleanField">, BaseField {}
export interface StringField extends TaggedVariant<"StringField">, BaseField {
maxLength?: number;
minLength?: number;
}
export interface EmailField extends TaggedVariant<"EmailField">, BaseField {}
export interface IntegerField extends TaggedVariant<"IntegerField">, BaseField {
isUnsigned?: boolean;
hasArbitraryPrecision?: boolean;
}
export interface FloatField extends TaggedVariant<"FloatField">, BaseField {}
export interface ReferenceField
extends TaggedVariant<"ReferenceField">, BaseField {
targetModel: string;
targetKey?: string;
}
export interface DecimalField extends TaggedVariant<"DecimalField">, BaseField {
isUnsigned?: boolean;
precision?: number;
scale?: number;
}
export interface EmbeddedField<T = any>
extends TaggedVariant<"EmbeddedField">, BaseField {}

View File

@ -1,5 +0,0 @@
export interface BaseField {
isOptional?: boolean;
isUnique?: boolean;
isIndexed?: boolean;
}

View File

@ -1,16 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface BooleanFieldOptions extends BaseField {}
export interface BooleanField
extends TaggedVariant<"BooleanField">, BooleanFieldOptions {}
export function createBooleanField<T extends BooleanFieldOptions>(
opts: T = {} as T,
): BooleanField & T {
return {
[VariantTag]: "BooleanField",
...opts,
} as const;
}

View File

@ -1,20 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface DecimalFieldOptions extends BaseField {
isUnsigned?: boolean;
precision?: number;
scale?: number;
}
export interface DecimalField
extends TaggedVariant<"DecimalField">, DecimalFieldOptions {}
export function createDecimalField<T extends DecimalFieldOptions>(
opts: T = {} as T,
): DecimalField & T {
return {
[VariantTag]: "DecimalField",
...opts,
} as const;
}

View File

@ -1,18 +0,0 @@
// deno-lint-ignore-file no-explicit-any
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface EmbeddedFieldOptions<T = any> extends BaseField {}
export interface EmbeddedField<T = any>
extends TaggedVariant<"EmbeddedField">, EmbeddedFieldOptions<T> {}
export function createEmbeddedField<
K = any,
T extends EmbeddedFieldOptions<K> = EmbeddedFieldOptions<K>,
>(opts: T = {} as T): EmbeddedField & T {
return {
[VariantTag]: "EmbeddedField",
...opts,
} as const;
}

View File

@ -1,40 +0,0 @@
import type { PosixDate } from "@fabric/core";
import type Decimal from "decimal";
import type { UUID } from "../../../core/types/uuid.ts";
import type { BooleanField } from "./boolean-field.ts";
import type { DecimalField } from "./decimal.ts";
import type { EmbeddedField } from "./embedded.ts";
import type { FloatField } from "./float.ts";
import type { IntegerField } from "./integer.ts";
import type { ReferenceField } from "./reference-field.ts";
import type { StringField } from "./string-field.ts";
import type { TimestampField } from "./timestamp.ts";
import type { UUIDField } from "./uuid-field.ts";
/**
* Converts a field definition to its corresponding TypeScript type.
*/
//prettier-ignore
export type FieldToType<TField> = TField extends StringField
? MaybeOptional<TField, string>
: TField extends UUIDField ? MaybeOptional<TField, UUID>
: TField extends IntegerField ? IntegerFieldToType<TField>
: TField extends ReferenceField ? MaybeOptional<TField, UUID>
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
: TField extends FloatField ? MaybeOptional<TField, number>
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
: TField extends BooleanField ? MaybeOptional<TField, boolean>
: TField extends EmbeddedField<infer TSubModel>
? MaybeOptional<TField, TSubModel>
: never;
//prettier-ignore
type IntegerFieldToType<TField extends IntegerField> =
TField["hasArbitraryPrecision"] extends true ? MaybeOptional<TField, bigint>
: TField["hasArbitraryPrecision"] extends false
? MaybeOptional<TField, number>
: MaybeOptional<TField, number | bigint>;
type MaybeOptional<TField, TType> = TField extends { isOptional: true }
? TType | null
: TType;

View File

@ -1,16 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface FloatFieldOptions extends BaseField {}
export interface FloatField
extends TaggedVariant<"FloatField">, FloatFieldOptions {}
export function createFloatField<T extends FloatFieldOptions>(
opts: T = {} as T,
): FloatField & T {
return {
[VariantTag]: "FloatField",
...opts,
} as const;
}

View File

@ -1,39 +0,0 @@
import { type BooleanField, createBooleanField } from "./boolean-field.ts";
import { createDecimalField, type DecimalField } from "./decimal.ts";
import { createEmbeddedField, type EmbeddedField } from "./embedded.ts";
import { createFloatField, type FloatField } from "./float.ts";
import { createIntegerField, type IntegerField } from "./integer.ts";
import {
createReferenceField,
type ReferenceField,
} from "./reference-field.ts";
import { createStringField, type StringField } from "./string-field.ts";
import { createTimestampField, type TimestampField } from "./timestamp.ts";
import { createUUIDField, type UUIDField } from "./uuid-field.ts";
export * from "./base-field.ts";
export * from "./field-to-type.ts";
export * from "./reference-field.ts";
export * from "./uuid-field.ts";
export type FieldDefinition =
| StringField
| UUIDField
| IntegerField
| FloatField
| DecimalField
| ReferenceField
| TimestampField
| EmbeddedField
| BooleanField;
export namespace Field {
export const string = createStringField;
export const uuid = createUUIDField;
export const integer = createIntegerField;
export const reference = createReferenceField;
export const decimal = createDecimalField;
export const float = createFloatField;
export const timestamp = createTimestampField;
export const embedded = createEmbeddedField;
export const boolean = createBooleanField;
}

View File

@ -1,19 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface IntegerFieldOptions extends BaseField {
isUnsigned?: boolean;
hasArbitraryPrecision?: boolean;
}
export interface IntegerField
extends TaggedVariant<"IntegerField">, IntegerFieldOptions {}
export function createIntegerField<T extends IntegerFieldOptions>(
opts: T = {} as T,
): IntegerField & T {
return {
[VariantTag]: "IntegerField",
...opts,
} as const;
}

View File

@ -1,19 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface StringFieldOptions extends BaseField {
maxLength?: number;
minLength?: number;
}
export interface StringField
extends TaggedVariant<"StringField">, StringFieldOptions {}
export function createStringField<T extends StringFieldOptions>(
opts: T = {} as T,
): StringField & T {
return {
[VariantTag]: "StringField",
...opts,
} as const;
}

View File

@ -1,16 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface TimestampFieldOptions extends BaseField {}
export interface TimestampField
extends TaggedVariant<"TimestampField">, TimestampFieldOptions {}
export function createTimestampField<T extends TimestampFieldOptions>(
opts: T = {} as T,
): TimestampField & T {
return {
[VariantTag]: "TimestampField",
...opts,
} as const;
}

View File

@ -1,18 +0,0 @@
import { type TaggedVariant, VariantTag } from "@fabric/core";
import type { BaseField } from "./base-field.ts";
export interface UUIDFieldOptions extends BaseField {
isPrimaryKey?: boolean;
}
export interface UUIDField
extends TaggedVariant<"UUIDField">, UUIDFieldOptions {}
export function createUUIDField<T extends UUIDFieldOptions>(
opts: T = {} as T,
): UUIDField & T {
return {
[VariantTag]: "UUIDField",
...opts,
} as const;
}

View File

@ -1,6 +1,7 @@
export * from "./fields/index.ts"; export * from "./field-parsers.ts";
export * from "./fields.ts";
export * from "./model-schema.ts"; export * from "./model-schema.ts";
export * from "./model.ts"; export * from "./model.ts";
export * from "./reference-field.ts";
export * from "./state-store.ts"; export * from "./state-store.ts";
export * from "./store-query/index.ts"; export * from "./store-query/index.ts";
export * from "./validations/index.ts";

View File

@ -1,7 +1,7 @@
import { isError } from "@fabric/core"; import { isError } from "@fabric/core";
import { describe, expect, test } from "@fabric/testing"; import { describe, expect, test } from "@fabric/testing";
import { Model } from "../model.ts"; import { Field } from "../index.ts";
import { Field } from "./index.ts"; import { Model } from "./model.ts";
import { import {
InvalidReferenceFieldError, InvalidReferenceFieldError,
validateReferenceField, validateReferenceField,

View File

@ -1,28 +1,6 @@
import { import { Result, TaggedError } from "@fabric/core";
Result, import type { ReferenceField } from "./fields.ts";
TaggedError, import type { ModelSchema } from "./model-schema.ts";
type TaggedVariant,
VariantTag,
} from "@fabric/core";
import type { ModelSchema } from "../model-schema.ts";
import type { BaseField } from "./base-field.ts";
export interface ReferenceFieldOptions extends BaseField {
targetModel: string;
targetKey?: string;
}
export interface ReferenceField
extends TaggedVariant<"ReferenceField">, ReferenceFieldOptions {}
export function createReferenceField<T extends ReferenceFieldOptions>(
opts: T = {} as T,
): ReferenceField & T {
return {
[VariantTag]: "ReferenceField",
...opts,
} as const;
}
export function getTargetKey(field: ReferenceField): string { export function getTargetKey(field: ReferenceField): string {
return field.targetKey || "id"; return field.targetKey || "id";

View File

@ -1 +0,0 @@
export * from "./parse-from-model.ts";

View File

@ -1,43 +0,0 @@
// deno-lint-ignore-file no-explicit-any
import { isRecordEmpty, Result, TaggedError } from "@fabric/core";
import type { FieldDefinition, Model, ModelToType } from "../index.ts";
import { fieldParsers, type FieldParsingError } from "./field-parsers.ts";
export function parseFromModel<
TModel extends Model,
>(
model: TModel,
value: unknown,
): Result<ModelToType<TModel>, SchemaParsingError<TModel>> {
const parsingErrors = {} as Record<keyof TModel, FieldParsingError>;
const parsedValue = {} as ModelToType<TModel>;
for (const key in model) {
const field = model[key] as FieldDefinition;
const fieldResult = fieldParsers[field._tag](field as any, value);
if (fieldResult.isOk()) {
parsedValue[key as keyof ModelToType<TModel>] = fieldResult
.value();
} else {
parsingErrors[key] = fieldResult.unwrapErrorOrThrow();
}
}
if (!isRecordEmpty(parsingErrors)) {
return Result.failWith(new SchemaParsingError(parsingErrors, parsedValue));
} else {
return Result.succeedWith(parsedValue);
}
}
export class SchemaParsingError<TModel extends Model>
extends TaggedError<"SchemaParsingFailed"> {
constructor(
public readonly errors: Record<keyof TModel, FieldParsingError>,
public readonly value?: Partial<ModelToType<TModel>>,
) {
super("SchemaParsingFailed");
}
}