[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 "./models/index.ts";
|
||||||
export * from "./security/index.ts";
|
export * from "./security/index.ts";
|
||||||
export * from "./services/index.ts";
|
export * from "./services/index.ts";
|
||||||
export * from "./types/index.ts";
|
|
||||||
export * from "./use-case/index.ts";
|
export * from "./use-case/index.ts";
|
||||||
export * from "./utils/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 { PosixDate } from "@fabric/core";
|
||||||
import type Decimal from "jsr:@quentinadam/decimal";
|
import type Decimal from "decimal";
|
||||||
import type { UUID } from "../../types/uuid.ts";
|
import type { UUID } from "../../../core/types/uuid.ts";
|
||||||
|
import type { BooleanField } from "./boolean-field.ts";
|
||||||
import type { DecimalField } from "./decimal.ts";
|
import type { DecimalField } from "./decimal.ts";
|
||||||
import type { EmbeddedField } from "./embedded.ts";
|
import type { EmbeddedField } from "./embedded.ts";
|
||||||
import type { FloatField } from "./float.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 DecimalField ? MaybeOptional<TField, Decimal>
|
||||||
: TField extends FloatField ? MaybeOptional<TField, number>
|
: TField extends FloatField ? MaybeOptional<TField, number>
|
||||||
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
|
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
|
||||||
|
: TField extends BooleanField ? MaybeOptional<TField, boolean>
|
||||||
: TField extends EmbeddedField<infer TSubModel>
|
: TField extends EmbeddedField<infer TSubModel>
|
||||||
? MaybeOptional<TField, TSubModel>
|
? MaybeOptional<TField, TSubModel>
|
||||||
: never;
|
: never;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { type BooleanField, createBooleanField } from "./boolean-field.ts";
|
||||||
import { createDecimalField, type DecimalField } from "./decimal.ts";
|
import { createDecimalField, type DecimalField } from "./decimal.ts";
|
||||||
import { createEmbeddedField, type EmbeddedField } from "./embedded.ts";
|
import { createEmbeddedField, type EmbeddedField } from "./embedded.ts";
|
||||||
import { createFloatField, type FloatField } from "./float.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 "./base-field.ts";
|
||||||
export * from "./field-to-type.ts";
|
export * from "./field-to-type.ts";
|
||||||
export * from "./reference-field.ts";
|
export * from "./reference-field.ts";
|
||||||
|
export * from "./uuid-field.ts";
|
||||||
|
|
||||||
export type FieldDefinition =
|
export type FieldDefinition =
|
||||||
| StringField
|
| StringField
|
||||||
@ -21,7 +23,8 @@ export type FieldDefinition =
|
|||||||
| DecimalField
|
| DecimalField
|
||||||
| ReferenceField
|
| ReferenceField
|
||||||
| TimestampField
|
| TimestampField
|
||||||
| EmbeddedField;
|
| EmbeddedField
|
||||||
|
| BooleanField;
|
||||||
|
|
||||||
export namespace Field {
|
export namespace Field {
|
||||||
export const string = createStringField;
|
export const string = createStringField;
|
||||||
@ -32,4 +35,5 @@ export namespace Field {
|
|||||||
export const float = createFloatField;
|
export const float = createFloatField;
|
||||||
export const timestamp = createTimestampField;
|
export const timestamp = createTimestampField;
|
||||||
export const embedded = createEmbeddedField;
|
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 { describe, expectTypeOf, test } from "@fabric/testing";
|
||||||
import type { PosixDate } from "../../core/index.ts";
|
import type { PosixDate } from "../../core/index.ts";
|
||||||
import type { UUID } from "../types/uuid.ts";
|
|
||||||
import { defineAggregateModel } from "./aggregate-model.ts";
|
import { defineAggregateModel } from "./aggregate-model.ts";
|
||||||
import { Field } from "./fields/index.ts";
|
import { Field } from "./fields/index.ts";
|
||||||
import type { ModelToType } from "./model.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