[fabric/domain] Add base model validations
This commit is contained in:
parent
87ce76e13f
commit
3b0533b0a9
@ -4,6 +4,6 @@ export * from "./files/index.ts";
|
||||
export * from "./models/index.ts";
|
||||
export * from "./security/index.ts";
|
||||
export * from "./services/index.ts";
|
||||
export * from "./types/index.ts";
|
||||
export * from "./use-case/index.ts";
|
||||
export * from "./utils/index.ts";
|
||||
export * from "./validations/index.ts";
|
||||
|
||||
16
packages/fabric/domain/models/fields/boolean-field.ts
Normal file
16
packages/fabric/domain/models/fields/boolean-field.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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,6 +1,7 @@
|
||||
import type { PosixDate } from "@fabric/core";
|
||||
import type Decimal from "jsr:@quentinadam/decimal";
|
||||
import type { UUID } from "../../types/uuid.ts";
|
||||
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";
|
||||
@ -22,6 +23,7 @@ export type FieldToType<TField> = TField extends StringField
|
||||
: 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;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
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";
|
||||
@ -12,6 +13,7 @@ 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
|
||||
@ -21,7 +23,8 @@ export type FieldDefinition =
|
||||
| DecimalField
|
||||
| ReferenceField
|
||||
| TimestampField
|
||||
| EmbeddedField;
|
||||
| EmbeddedField
|
||||
| BooleanField;
|
||||
|
||||
export namespace Field {
|
||||
export const string = createStringField;
|
||||
@ -32,4 +35,5 @@ export namespace Field {
|
||||
export const float = createFloatField;
|
||||
export const timestamp = createTimestampField;
|
||||
export const embedded = createEmbeddedField;
|
||||
export const boolean = createBooleanField;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { UUID } from "@fabric/core";
|
||||
import { describe, expectTypeOf, test } from "@fabric/testing";
|
||||
import type { PosixDate } from "../../core/index.ts";
|
||||
import type { UUID } from "../types/uuid.ts";
|
||||
import { defineAggregateModel } from "./aggregate-model.ts";
|
||||
import { Field } from "./fields/index.ts";
|
||||
import type { ModelToType } from "./model.ts";
|
||||
|
||||
134
packages/fabric/domain/validations/field-parsers.ts
Normal file
134
packages/fabric/domain/validations/field-parsers.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import {
|
||||
PosixDate,
|
||||
Result,
|
||||
TaggedError,
|
||||
type VariantFromTag,
|
||||
} from "@fabric/core";
|
||||
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
||||
import type { FieldDefinition, FieldToType } from "../models/index.ts";
|
||||
|
||||
export type FieldParsers = {
|
||||
[K in FieldDefinition["_tag"]]: FieldParser<
|
||||
VariantFromTag<FieldDefinition, K>
|
||||
>;
|
||||
};
|
||||
export const fieldParsers: FieldParsers = {
|
||||
StringField: (f, v) => {
|
||||
return parseStringValue(f, v);
|
||||
},
|
||||
UUIDField: (f, v) => {
|
||||
return parseStringValue(f, v)
|
||||
.flatMap((parsedString) =>
|
||||
isUUID(parsedString)
|
||||
? Result.ok(parsedString)
|
||||
: Result.failWith(new InvalidFieldTypeError())
|
||||
);
|
||||
},
|
||||
ReferenceField: (f, v) => {
|
||||
return parseStringValue(f, v)
|
||||
.flatMap((parsedString) =>
|
||||
isUUID(parsedString)
|
||||
? Result.ok(parsedString)
|
||||
: 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());
|
||||
}
|
||||
|
||||
return Result.ok(v);
|
||||
});
|
||||
},
|
||||
BooleanField: (f, v) => {
|
||||
if (!f.isOptional && v === undefined) {
|
||||
return Result.failWith(new MissingRequiredFieldError());
|
||||
}
|
||||
if (v === undefined) {
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
if (typeof v === "boolean") {
|
||||
return Result.ok(v);
|
||||
}
|
||||
|
||||
return Result.failWith(new InvalidFieldTypeError());
|
||||
},
|
||||
IntegerField: function (): Result<
|
||||
number | bigint | undefined,
|
||||
FieldParsingError
|
||||
> {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
FloatField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
DecimalField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
EmbeddedField: function () {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that takes a field definition and a value and returns a result
|
||||
*/
|
||||
export type FieldParser<TField extends FieldDefinition> = (
|
||||
field: TField,
|
||||
value: unknown,
|
||||
) => Result<FieldToType<TField> | undefined, FieldParsingError>;
|
||||
|
||||
/**
|
||||
* Field parsing errors
|
||||
*/
|
||||
export type FieldParsingError =
|
||||
| InvalidFieldTypeError
|
||||
| MissingRequiredFieldError;
|
||||
|
||||
/**
|
||||
* An error that occurs when a field is invalid
|
||||
*/
|
||||
export class InvalidFieldTypeError extends TaggedError<"InvalidField"> {
|
||||
constructor() {
|
||||
super("InvalidField");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error that occurs when a required field is missing
|
||||
*/
|
||||
export class MissingRequiredFieldError extends TaggedError<"RequiredField"> {
|
||||
constructor() {
|
||||
super("RequiredField");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string value including optionality
|
||||
*/
|
||||
function parseStringValue(
|
||||
field: FieldDefinition,
|
||||
value: unknown,
|
||||
): Result<string | undefined, FieldParsingError> {
|
||||
const parsedValue = parseAndSanitizeString(value);
|
||||
return parseOptionality(field, parsedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the optionality of a field.
|
||||
* In other words, if a field is required and the value is undefined, it will return a MissingRequiredFieldError.
|
||||
* If the field is optional and the value is undefined, it will return the value as undefined.
|
||||
*/
|
||||
function parseOptionality<T>(
|
||||
field: FieldDefinition,
|
||||
value: T | undefined,
|
||||
): Result<T | undefined, FieldParsingError> {
|
||||
if (!field.isOptional && value === undefined) {
|
||||
return Result.failWith(new MissingRequiredFieldError());
|
||||
}
|
||||
return Result.ok(value);
|
||||
}
|
||||
1
packages/fabric/domain/validations/index.ts
Normal file
1
packages/fabric/domain/validations/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./parse-from-model.ts";
|
||||
43
packages/fabric/domain/validations/parse-from-model.ts
Normal file
43
packages/fabric/domain/validations/parse-from-model.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
|
||||
import { isRecordEmpty, Result, TaggedError } from "@fabric/core";
|
||||
import type { FieldDefinition, Model, ModelToType } from "../models/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