[fabric/domain] Simplify model field definitions
This commit is contained in:
parent
36b5286a09
commit
6329044415
@ -1,11 +1,12 @@
|
||||
import {
|
||||
Decimal,
|
||||
PosixDate,
|
||||
Result,
|
||||
TaggedError,
|
||||
type VariantFromTag,
|
||||
} from "@fabric/core";
|
||||
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
||||
import type { FieldDefinition, FieldToType } from "../index.ts";
|
||||
import type { FieldDefinition, FieldToType } from "./index.ts";
|
||||
|
||||
export type FieldParsers = {
|
||||
[K in FieldDefinition["_tag"]]: FieldParser<
|
||||
@ -32,46 +33,56 @@ export const fieldParsers: FieldParsers = {
|
||||
: Result.failWith(new InvalidFieldTypeError())
|
||||
);
|
||||
},
|
||||
TimestampField: (f, v) => {
|
||||
return parseOptionality(f, v).flatMap((parsedValue) => {
|
||||
if (parsedValue === undefined) return Result.ok(undefined);
|
||||
|
||||
if (!(v instanceof PosixDate)) {
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
PosixDateField: (f, v) => {
|
||||
return parseOptionality(f, v, (v) => {
|
||||
if (v instanceof PosixDate) {
|
||||
return Result.ok(v);
|
||||
}
|
||||
|
||||
return Result.ok(v);
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
});
|
||||
},
|
||||
BooleanField: (f, v) => {
|
||||
if (!f.isOptional && v === undefined) {
|
||||
return Result.failWith(new MissingRequiredFieldError());
|
||||
}
|
||||
if (v === undefined) {
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
return parseOptionality(f, v, (v) => {
|
||||
if (typeof v === "boolean") {
|
||||
return Result.ok(v);
|
||||
}
|
||||
|
||||
if (typeof v === "boolean") {
|
||||
return Result.ok(v);
|
||||
}
|
||||
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
});
|
||||
},
|
||||
IntegerField: function (): Result<
|
||||
number | bigint | undefined,
|
||||
FieldParsingError
|
||||
> {
|
||||
throw new Error("Function not implemented.");
|
||||
IntegerField: (f, v) => {
|
||||
return parseOptionality(f, v, (v) => {
|
||||
if (
|
||||
(typeof v === "number" &&
|
||||
Number.isInteger(v)) || (typeof v === "bigint")
|
||||
) {
|
||||
return Result.ok(v);
|
||||
}
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
});
|
||||
},
|
||||
FloatField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
FloatField: (f, v) => {
|
||||
return parseOptionality(f, v, (v) => {
|
||||
if (typeof v === "number") {
|
||||
return Result.ok(v);
|
||||
}
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
});
|
||||
},
|
||||
DecimalField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
DecimalField: (f, v) => {
|
||||
return parseOptionality(f, v, (v) => {
|
||||
if (v instanceof Decimal) {
|
||||
return Result.ok(v);
|
||||
}
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
});
|
||||
},
|
||||
EmbeddedField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
EmailField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@ -125,10 +136,17 @@ function parseStringValue(
|
||||
*/
|
||||
function parseOptionality<T>(
|
||||
field: FieldDefinition,
|
||||
value: T | undefined,
|
||||
value: unknown,
|
||||
withMapping?: (value: unknown) => Result<T, FieldParsingError>,
|
||||
): Result<T | undefined, FieldParsingError> {
|
||||
if (!field.isOptional && value === undefined) {
|
||||
return Result.failWith(new MissingRequiredFieldError());
|
||||
}
|
||||
return Result.ok(value);
|
||||
if (value === undefined) {
|
||||
return Result.ok(value);
|
||||
}
|
||||
if (!withMapping) {
|
||||
return Result.ok(value as T);
|
||||
}
|
||||
return withMapping(value);
|
||||
}
|
||||
108
packages/fabric/domain/models/fields.ts
Normal file
108
packages/fabric/domain/models/fields.ts
Normal 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 {}
|
||||
@ -1,5 +0,0 @@
|
||||
export interface BaseField {
|
||||
isOptional?: boolean;
|
||||
isUnique?: boolean;
|
||||
isIndexed?: boolean;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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.ts";
|
||||
export * from "./reference-field.ts";
|
||||
export * from "./state-store.ts";
|
||||
export * from "./store-query/index.ts";
|
||||
export * from "./validations/index.ts";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { isError } from "@fabric/core";
|
||||
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 {
|
||||
InvalidReferenceFieldError,
|
||||
validateReferenceField,
|
||||
@ -1,28 +1,6 @@
|
||||
import {
|
||||
Result,
|
||||
TaggedError,
|
||||
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;
|
||||
}
|
||||
import { Result, TaggedError } from "@fabric/core";
|
||||
import type { ReferenceField } from "./fields.ts";
|
||||
import type { ModelSchema } from "./model-schema.ts";
|
||||
|
||||
export function getTargetKey(field: ReferenceField): string {
|
||||
return field.targetKey || "id";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./parse-from-model.ts";
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user