Compare commits
9 Commits
ab41ff028d
...
b77ba6dc83
| Author | SHA1 | Date | |
|---|---|---|---|
| b77ba6dc83 | |||
| 623e67afeb | |||
| a053ca225b | |||
| f30535055f | |||
| 6329044415 | |||
| 36b5286a09 | |||
| de49970c0c | |||
| f189f8994f | |||
| dd95d58e3a |
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "@fabric/core",
|
||||
"version": "0.1.0",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from "decimal";
|
||||
export * from "./array/index.ts";
|
||||
export * from "./error/index.ts";
|
||||
export * from "./record/index.ts";
|
||||
@ -7,3 +8,4 @@ export * from "./time/index.ts";
|
||||
export * from "./types/index.ts";
|
||||
export * from "./utils/index.ts";
|
||||
export * from "./variant/index.ts";
|
||||
export { Decimal };
|
||||
|
||||
@ -35,7 +35,9 @@ export class AsyncResult<
|
||||
);
|
||||
}
|
||||
|
||||
static ok<T>(value: T): AsyncResult<T, never> {
|
||||
static ok(): AsyncResult<void, never>;
|
||||
static ok<T>(value: T): AsyncResult<T, never>;
|
||||
static ok(value?: any) {
|
||||
return new AsyncResult(Promise.resolve(Result.ok(value)));
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ export type Nothing = null;
|
||||
export const Nothing = null;
|
||||
|
||||
/**
|
||||
* Un Optional es un tipo que puede ser un valor o no ser nada.
|
||||
* An `Optional` type is a type that represents a value that may or may not be present.
|
||||
*/
|
||||
export type Optional<T> = T | Nothing;
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./ensure.ts";
|
||||
export * from "./json-utils.ts";
|
||||
|
||||
43
packages/fabric/core/utils/json-utils.ts
Normal file
43
packages/fabric/core/utils/json-utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { isRecord, PosixDate } from "@fabric/core";
|
||||
import Decimal from "decimal";
|
||||
|
||||
export namespace JSONUtils {
|
||||
export function reviver(key: string, value: unknown) {
|
||||
if (isRecord(value)) {
|
||||
if (value._type === "bigint" && typeof value.value == "string") {
|
||||
return BigInt(value.value);
|
||||
}
|
||||
|
||||
if (value._type === "decimal" && typeof value.value == "string") {
|
||||
return Decimal.from(value.value);
|
||||
}
|
||||
|
||||
if (PosixDate.isPosixDateJSON(value)) {
|
||||
return PosixDate.fromJson(value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parse<T>(json: string): T {
|
||||
return JSON.parse(json, reviver);
|
||||
}
|
||||
|
||||
export function stringify<T>(value: T): string {
|
||||
return JSON.stringify(value, (key, value) => {
|
||||
if (typeof value === "bigint") {
|
||||
return {
|
||||
_type: "bigint",
|
||||
value: value.toString(),
|
||||
};
|
||||
}
|
||||
if (value instanceof Decimal) {
|
||||
return {
|
||||
_type: "decimal",
|
||||
value: value.toString(),
|
||||
};
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ export function variantConstructor<
|
||||
>(
|
||||
tag: T[VariantTag],
|
||||
) {
|
||||
return <TOpts extends Omit<T, VariantTag>>(options: TOpts) => {
|
||||
return <TOpts extends Omit<T, VariantTag>>(options: TOpts = {} as TOpts) => {
|
||||
return {
|
||||
_tag: tag,
|
||||
...options,
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
},
|
||||
"imports": {
|
||||
"@fabric/core": "jsr:@fabric/core",
|
||||
"@fabric/validations": "jsr:@fabric/validations",
|
||||
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
||||
"@fabric/validations": "jsr:@fabric/validations"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,8 +1,6 @@
|
||||
import type { UUID } from "@fabric/core";
|
||||
import { describe, expectTypeOf, test } from "@fabric/testing";
|
||||
import type { PosixDate } from "../../core/index.ts";
|
||||
import { Field } from "./fields/index.ts";
|
||||
import { Model, type ModelToType } from "./model.ts";
|
||||
import { describe, expect, test } from "@fabric/testing";
|
||||
import { Field } from "./fields.ts";
|
||||
import { Model, type ModelToType, SchemaParsingError } from "./model.ts";
|
||||
|
||||
describe("CreateModel", () => {
|
||||
test("should create a model and it's interface type", () => {
|
||||
@ -13,14 +11,79 @@ describe("CreateModel", () => {
|
||||
});
|
||||
type User = ModelToType<typeof User>;
|
||||
|
||||
expectTypeOf<User>().toEqualTypeOf<{
|
||||
id: UUID;
|
||||
streamId: UUID;
|
||||
streamVersion: bigint;
|
||||
name: string;
|
||||
password: string;
|
||||
phone: string | null;
|
||||
deletedAt: PosixDate | null;
|
||||
}>();
|
||||
// expectTypeOf<User>().toEqualTypeOf<{
|
||||
// id: UUID;
|
||||
// streamId: UUID;
|
||||
// streamVersion: bigint;
|
||||
// name: string;
|
||||
// password: string;
|
||||
// phone?: string | undefined;
|
||||
// deletedAt?: PosixDate | undefined;
|
||||
// }>();
|
||||
|
||||
// deno-lint-ignore no-unused-vars
|
||||
const p: User = {
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
streamId: "123e4567-e89b-12d3-a456-426614174001",
|
||||
streamVersion: 1n,
|
||||
name: "John Doe",
|
||||
password: "password123",
|
||||
};
|
||||
});
|
||||
|
||||
test("should parse valid data correctly", () => {
|
||||
const User = Model.aggregateFrom("User", {
|
||||
name: Field.string(),
|
||||
password: Field.string(),
|
||||
phone: Field.string({ isOptional: true }),
|
||||
});
|
||||
|
||||
const result = User.parse({
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
streamId: "123e4567-e89b-12d3-a456-426614174001",
|
||||
streamVersion: 1n,
|
||||
name: "John Doe",
|
||||
password: "password123",
|
||||
phone: "123-456-7890",
|
||||
});
|
||||
|
||||
expect(result.unwrapOrThrow()).toEqual({
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
streamId: "123e4567-e89b-12d3-a456-426614174001",
|
||||
streamVersion: 1n,
|
||||
name: "John Doe",
|
||||
password: "password123",
|
||||
phone: "123-456-7890",
|
||||
});
|
||||
});
|
||||
|
||||
test("should fail to parse invalid data", () => {
|
||||
const User = Model.aggregateFrom("User", {
|
||||
name: Field.string(),
|
||||
password: Field.string(),
|
||||
phone: Field.string({ isOptional: true }),
|
||||
});
|
||||
|
||||
const result = User.parse({
|
||||
id: "invalid-uuid",
|
||||
streamId: "invalid-uuid",
|
||||
streamVersion: "not-a-bigint",
|
||||
name: 123,
|
||||
password: true,
|
||||
phone: 456,
|
||||
deletedAt: "not-a-date",
|
||||
});
|
||||
|
||||
expect(result.isError()).toBe(true);
|
||||
if (result.isError()) {
|
||||
const error = result.unwrapErrorOrThrow();
|
||||
expect(error).toBeInstanceOf(SchemaParsingError);
|
||||
expect(error.errors).toHaveProperty("id");
|
||||
expect(error.errors).toHaveProperty("streamId");
|
||||
expect(error.errors).toHaveProperty("streamVersion");
|
||||
expect(error.errors).toHaveProperty("name");
|
||||
expect(error.errors).toHaveProperty("password");
|
||||
expect(error.errors).toHaveProperty("deletedAt");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Keyof } from "@fabric/core";
|
||||
import type { FieldToType } from "./fields/field-to-type.ts";
|
||||
import { Field, type FieldDefinition } from "./fields/index.ts";
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { isRecordEmpty, type Keyof, Result, TaggedError } from "@fabric/core";
|
||||
import { fieldParsers, type FieldParsingError } from "./field-parsers.ts";
|
||||
import { Field, type FieldDefinition, type FieldToType } from "./fields.ts";
|
||||
|
||||
/**
|
||||
* A model is a schema definition for some type of structured data.
|
||||
@ -30,6 +31,35 @@ export class Model<
|
||||
return new Model(name, { ...fields, ...DefaultEntityFields });
|
||||
}
|
||||
private constructor(readonly name: TName, readonly fields: TFields) {}
|
||||
|
||||
public parse(
|
||||
value: unknown,
|
||||
): Result<ModelToType<this>, SchemaParsingError<this>> {
|
||||
const parsingErrors = {} as Record<keyof this["fields"], FieldParsingError>;
|
||||
const parsedValue = {} as ModelToType<this>;
|
||||
|
||||
for (const key in this.fields) {
|
||||
const field = this.fields[key]!;
|
||||
const fieldResult = fieldParsers[field._tag](
|
||||
field as any,
|
||||
(value as any)[key],
|
||||
);
|
||||
|
||||
if (fieldResult.isOk()) {
|
||||
parsedValue[key as keyof ModelToType<this>] = fieldResult.value;
|
||||
} else {
|
||||
parsingErrors[key] = fieldResult.unwrapErrorOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRecordEmpty(parsingErrors)) {
|
||||
return Result.failWith(
|
||||
new SchemaParsingError(parsingErrors, parsedValue),
|
||||
);
|
||||
} else {
|
||||
return Result.succeedWith(parsedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EntityModel = Model<
|
||||
@ -42,9 +72,9 @@ export type AggregateModel = Model<
|
||||
typeof DefaultAggregateFields & ModelFields
|
||||
>;
|
||||
|
||||
export type ModelToType<TModel extends Model> = {
|
||||
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
||||
};
|
||||
export type ModelToType<TModel extends Model> =
|
||||
& ModelToOptionalFields<TModel>
|
||||
& ModelToRequiredFields<TModel>;
|
||||
|
||||
export type ModelFieldNames<TModel extends ModelFields> = Keyof<
|
||||
TModel["fields"]
|
||||
@ -56,6 +86,18 @@ export type ModelAddressableFields<TModel extends Model> = {
|
||||
: never;
|
||||
}[Keyof<TModel["fields"]>];
|
||||
|
||||
export class SchemaParsingError<TModel extends Model>
|
||||
extends TaggedError<"SchemaParsingFailed"> {
|
||||
constructor(
|
||||
public readonly errors: Record<keyof TModel["fields"], FieldParsingError>,
|
||||
public readonly value?: Partial<ModelToType<TModel>>,
|
||||
) {
|
||||
super(
|
||||
"SchemaParsingFailed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type ModelFields = Record<string, FieldDefinition>;
|
||||
|
||||
const DefaultEntityFields = {
|
||||
@ -69,5 +111,31 @@ const DefaultAggregateFields = {
|
||||
isUnsigned: true,
|
||||
hasArbitraryPrecision: true,
|
||||
}),
|
||||
deletedAt: Field.timestamp({ isOptional: true }),
|
||||
deletedAt: Field.posixDate({ isOptional: true }),
|
||||
} as const;
|
||||
|
||||
type ModelToOptionalFields<TModel extends Model> = {
|
||||
[K in OptionalFields<TModel>]?: FieldToType<
|
||||
TModel["fields"][K]
|
||||
>;
|
||||
};
|
||||
|
||||
type ModelToRequiredFields<TModel extends Model> = {
|
||||
[K in RequiredFields<TModel>]: FieldToType<
|
||||
TModel["fields"][K]
|
||||
>;
|
||||
};
|
||||
|
||||
type OptionalFields<TModel extends Model> = {
|
||||
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends {
|
||||
isOptional: true;
|
||||
} ? K
|
||||
: never;
|
||||
}[Keyof<TModel["fields"]>];
|
||||
|
||||
type RequiredFields<TModel extends Model> = {
|
||||
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends {
|
||||
isOptional: true;
|
||||
} ? never
|
||||
: K;
|
||||
}[Keyof<TModel["fields"]>];
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -1,2 +1 @@
|
||||
export * from "./json-utils.ts";
|
||||
export * from "./sort-by-dependencies.ts";
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import { PosixDate } from "@fabric/core";
|
||||
|
||||
export namespace JSONUtils {
|
||||
export function reviver(key: string, value: unknown) {
|
||||
if (PosixDate.isPosixDateJSON(value)) {
|
||||
return PosixDate.fromJson(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parse<T>(json: string): T {
|
||||
return JSON.parse(json, reviver);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
AsyncResult,
|
||||
JSONUtils,
|
||||
MaybePromise,
|
||||
PosixDate,
|
||||
Run,
|
||||
@ -11,7 +12,6 @@ import {
|
||||
EventFromKey,
|
||||
EventStore,
|
||||
EventSubscriber,
|
||||
JSONUtils,
|
||||
StoredEvent,
|
||||
StoreQueryError,
|
||||
} from "@fabric/domain";
|
||||
|
||||
@ -8,7 +8,7 @@ describe("ModelToSQL", () => {
|
||||
name: Field.string(),
|
||||
age: Field.integer(),
|
||||
// isTrue: Field.boolean(),
|
||||
date: Field.timestamp(),
|
||||
date: Field.posixDate(),
|
||||
reference: Field.reference({ targetModel: "somethingElse" }),
|
||||
});
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
||||
DecimalField: (n, f): string => {
|
||||
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
TimestampField: (n, f): string => {
|
||||
PosixDateField: (n, f): string => {
|
||||
return [n, "NUMERIC", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
EmbeddedField: (n, f): string => {
|
||||
@ -49,6 +49,9 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
||||
BooleanField: (n, f): string => {
|
||||
return [n, "BOOLEAN", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
EmailField: (n, f): string => {
|
||||
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
};
|
||||
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { PosixDate, VariantTag } from "@fabric/core";
|
||||
import { JSONUtils, PosixDate, VariantTag } from "@fabric/core";
|
||||
import { FieldDefinition, FieldToType, Model } from "@fabric/domain";
|
||||
|
||||
export function transformRow(model: Model) {
|
||||
@ -39,7 +39,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
||||
ReferenceField: (_, v) => v,
|
||||
FloatField: (_, v) => v,
|
||||
DecimalField: (_, v) => v,
|
||||
TimestampField: (_, v) => new PosixDate(v),
|
||||
EmbeddedField: (_, v: string) => JSON.parse(v),
|
||||
PosixDateField: (_, v) => new PosixDate(v),
|
||||
EmbeddedField: (_, v: string) => JSONUtils.parse(v),
|
||||
BooleanField: (_, v) => v,
|
||||
EmailField: (_, v) => v,
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { VariantTag } from "@fabric/core";
|
||||
import { JSONUtils, VariantTag } from "@fabric/core";
|
||||
import { FieldDefinition, FieldToType } from "@fabric/domain";
|
||||
import { isNullish } from "@fabric/validations";
|
||||
|
||||
type FieldSQLInsertMap = {
|
||||
[K in FieldDefinition[VariantTag]]: (
|
||||
@ -20,13 +21,14 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
||||
ReferenceField: (_, v) => v,
|
||||
FloatField: (_, v) => v,
|
||||
DecimalField: (_, v) => v,
|
||||
TimestampField: (_, v) => v.timestamp,
|
||||
EmbeddedField: (_, v: string) => JSON.stringify(v),
|
||||
PosixDateField: (_, v) => v.timestamp,
|
||||
EmbeddedField: (_, v: string) => JSONUtils.stringify(v),
|
||||
BooleanField: (_, v) => v,
|
||||
EmailField: (_, v) => v,
|
||||
};
|
||||
|
||||
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
||||
if (value === null) {
|
||||
if (isNullish(value)) {
|
||||
return null;
|
||||
}
|
||||
const r = FieldSQLInsertMap[field[VariantTag]] as any;
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
import { Run, UUID } from "@fabric/core";
|
||||
import { Run } from "@fabric/core";
|
||||
import { Field, isLike, Model } from "@fabric/domain";
|
||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
expectTypeOf,
|
||||
test,
|
||||
} from "@fabric/testing";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing";
|
||||
import { SQLiteStateStore } from "./state-store.ts";
|
||||
|
||||
describe("State Store", () => {
|
||||
@ -16,6 +9,7 @@ describe("State Store", () => {
|
||||
Model.entityFrom("demo", {
|
||||
value: Field.float(),
|
||||
owner: Field.reference({ targetModel: "users" }),
|
||||
optional: Field.string({ isOptional: true }),
|
||||
}),
|
||||
Model.entityFrom("users", {
|
||||
name: Field.string(),
|
||||
@ -52,12 +46,12 @@ describe("State Store", () => {
|
||||
|
||||
const result = await store.from("users").select().unwrapOrThrow();
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
{
|
||||
id: UUID;
|
||||
name: string;
|
||||
}[]
|
||||
>();
|
||||
// expectTypeOf(result).toEqualTypeOf<
|
||||
// {
|
||||
// id: UUID;
|
||||
// name: string;
|
||||
// }[]
|
||||
// >();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@ -95,12 +89,12 @@ describe("State Store", () => {
|
||||
})
|
||||
.select().unwrapOrThrow();
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
{
|
||||
id: UUID;
|
||||
name: string;
|
||||
}[]
|
||||
>();
|
||||
// expectTypeOf(result).toEqualTypeOf<
|
||||
// {
|
||||
// id: UUID;
|
||||
// name: string;
|
||||
// }[]
|
||||
// >();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
|
||||
@ -17,13 +17,13 @@ describe("Sanitize String", () => {
|
||||
test("Given a number value it should convert it to a string", () => {
|
||||
const sanitized = parseAndSanitizeString(123);
|
||||
|
||||
expect(sanitized).toBe("123");
|
||||
expect(sanitized).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Given a boolean value it should convert it to a string", () => {
|
||||
const sanitized = parseAndSanitizeString(true);
|
||||
|
||||
expect(sanitized).toBe("true");
|
||||
expect(sanitized).toBe(undefined);
|
||||
});
|
||||
|
||||
test("Given a null value it should return null", () => {
|
||||
|
||||
@ -7,8 +7,8 @@ import { isNullish } from "../nullish/is-nullish.ts";
|
||||
export function parseAndSanitizeString(
|
||||
value: unknown,
|
||||
): string | undefined {
|
||||
if (isNullish(value)) return undefined;
|
||||
return stripLow((String(value)).trim());
|
||||
if (isNullish(value) || typeof value != "string") return undefined;
|
||||
return stripLow(value).trim();
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-control-regex
|
||||
|
||||
Loading…
Reference in New Issue
Block a user