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",
|
"name": "@fabric/core",
|
||||||
|
"version": "0.1.0",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./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 "./array/index.ts";
|
||||||
export * from "./error/index.ts";
|
export * from "./error/index.ts";
|
||||||
export * from "./record/index.ts";
|
export * from "./record/index.ts";
|
||||||
@ -7,3 +8,4 @@ export * from "./time/index.ts";
|
|||||||
export * from "./types/index.ts";
|
export * from "./types/index.ts";
|
||||||
export * from "./utils/index.ts";
|
export * from "./utils/index.ts";
|
||||||
export * from "./variant/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)));
|
return new AsyncResult(Promise.resolve(Result.ok(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,6 @@ export type Nothing = null;
|
|||||||
export const 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;
|
export type Optional<T> = T | Nothing;
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./ensure.ts";
|
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],
|
tag: T[VariantTag],
|
||||||
) {
|
) {
|
||||||
return <TOpts extends Omit<T, VariantTag>>(options: TOpts) => {
|
return <TOpts extends Omit<T, VariantTag>>(options: TOpts = {} as TOpts) => {
|
||||||
return {
|
return {
|
||||||
_tag: tag,
|
_tag: tag,
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@fabric/core": "jsr:@fabric/core",
|
"@fabric/core": "jsr:@fabric/core",
|
||||||
"@fabric/validations": "jsr:@fabric/validations",
|
"@fabric/validations": "jsr:@fabric/validations"
|
||||||
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
return Result.ok(v);
|
||||||
if (!(v instanceof PosixDate)) {
|
|
||||||
return Result.failWith(new InvalidFieldTypeError());
|
|
||||||
}
|
}
|
||||||
|
return Result.failWith(new InvalidFieldTypeError());
|
||||||
return Result.ok(v);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
BooleanField: (f, v) => {
|
BooleanField: (f, v) => {
|
||||||
if (!f.isOptional && v === undefined) {
|
return parseOptionality(f, v, (v) => {
|
||||||
return Result.failWith(new MissingRequiredFieldError());
|
if (typeof v === "boolean") {
|
||||||
}
|
return Result.ok(v);
|
||||||
if (v === undefined) {
|
}
|
||||||
return Result.ok(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof v === "boolean") {
|
return Result.failWith(new InvalidFieldTypeError());
|
||||||
return Result.ok(v);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
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-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";
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import type { UUID } from "@fabric/core";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { describe, expectTypeOf, test } from "@fabric/testing";
|
import { Field } from "./fields.ts";
|
||||||
import type { PosixDate } from "../../core/index.ts";
|
import { Model, type ModelToType, SchemaParsingError } from "./model.ts";
|
||||||
import { Field } from "./fields/index.ts";
|
|
||||||
import { Model, type ModelToType } from "./model.ts";
|
|
||||||
|
|
||||||
describe("CreateModel", () => {
|
describe("CreateModel", () => {
|
||||||
test("should create a model and it's interface type", () => {
|
test("should create a model and it's interface type", () => {
|
||||||
@ -13,14 +11,79 @@ describe("CreateModel", () => {
|
|||||||
});
|
});
|
||||||
type User = ModelToType<typeof User>;
|
type User = ModelToType<typeof User>;
|
||||||
|
|
||||||
expectTypeOf<User>().toEqualTypeOf<{
|
// expectTypeOf<User>().toEqualTypeOf<{
|
||||||
id: UUID;
|
// id: UUID;
|
||||||
streamId: UUID;
|
// streamId: UUID;
|
||||||
streamVersion: bigint;
|
// streamVersion: bigint;
|
||||||
name: string;
|
// name: string;
|
||||||
password: string;
|
// password: string;
|
||||||
phone: string | null;
|
// phone?: string | undefined;
|
||||||
deletedAt: PosixDate | null;
|
// 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";
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import type { FieldToType } from "./fields/field-to-type.ts";
|
import { isRecordEmpty, type Keyof, Result, TaggedError } from "@fabric/core";
|
||||||
import { Field, type FieldDefinition } from "./fields/index.ts";
|
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.
|
* 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 });
|
return new Model(name, { ...fields, ...DefaultEntityFields });
|
||||||
}
|
}
|
||||||
private constructor(readonly name: TName, readonly fields: TFields) {}
|
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<
|
export type EntityModel = Model<
|
||||||
@ -42,9 +72,9 @@ export type AggregateModel = Model<
|
|||||||
typeof DefaultAggregateFields & ModelFields
|
typeof DefaultAggregateFields & ModelFields
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ModelToType<TModel extends Model> = {
|
export type ModelToType<TModel extends Model> =
|
||||||
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
& ModelToOptionalFields<TModel>
|
||||||
};
|
& ModelToRequiredFields<TModel>;
|
||||||
|
|
||||||
export type ModelFieldNames<TModel extends ModelFields> = Keyof<
|
export type ModelFieldNames<TModel extends ModelFields> = Keyof<
|
||||||
TModel["fields"]
|
TModel["fields"]
|
||||||
@ -56,6 +86,18 @@ export type ModelAddressableFields<TModel extends Model> = {
|
|||||||
: never;
|
: never;
|
||||||
}[Keyof<TModel["fields"]>];
|
}[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>;
|
type ModelFields = Record<string, FieldDefinition>;
|
||||||
|
|
||||||
const DefaultEntityFields = {
|
const DefaultEntityFields = {
|
||||||
@ -69,5 +111,31 @@ const DefaultAggregateFields = {
|
|||||||
isUnsigned: true,
|
isUnsigned: true,
|
||||||
hasArbitraryPrecision: true,
|
hasArbitraryPrecision: true,
|
||||||
}),
|
}),
|
||||||
deletedAt: Field.timestamp({ isOptional: true }),
|
deletedAt: Field.posixDate({ isOptional: true }),
|
||||||
} as const;
|
} 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 { 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,
|
||||||
@ -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";
|
||||||
@ -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";
|
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 {
|
import {
|
||||||
AsyncResult,
|
AsyncResult,
|
||||||
|
JSONUtils,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
PosixDate,
|
PosixDate,
|
||||||
Run,
|
Run,
|
||||||
@ -11,7 +12,6 @@ import {
|
|||||||
EventFromKey,
|
EventFromKey,
|
||||||
EventStore,
|
EventStore,
|
||||||
EventSubscriber,
|
EventSubscriber,
|
||||||
JSONUtils,
|
|
||||||
StoredEvent,
|
StoredEvent,
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
|
|||||||
@ -8,7 +8,7 @@ describe("ModelToSQL", () => {
|
|||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
age: Field.integer(),
|
age: Field.integer(),
|
||||||
// isTrue: Field.boolean(),
|
// isTrue: Field.boolean(),
|
||||||
date: Field.timestamp(),
|
date: Field.posixDate(),
|
||||||
reference: Field.reference({ targetModel: "somethingElse" }),
|
reference: Field.reference({ targetModel: "somethingElse" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
|||||||
DecimalField: (n, f): string => {
|
DecimalField: (n, f): string => {
|
||||||
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
TimestampField: (n, f): string => {
|
PosixDateField: (n, f): string => {
|
||||||
return [n, "NUMERIC", modifiersFromOpts(f)].join(" ");
|
return [n, "NUMERIC", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
EmbeddedField: (n, f): string => {
|
EmbeddedField: (n, f): string => {
|
||||||
@ -49,6 +49,9 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
|||||||
BooleanField: (n, f): string => {
|
BooleanField: (n, f): string => {
|
||||||
return [n, "BOOLEAN", modifiersFromOpts(f)].join(" ");
|
return [n, "BOOLEAN", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
|
EmailField: (n, f): string => {
|
||||||
|
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||||
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// 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";
|
import { FieldDefinition, FieldToType, Model } from "@fabric/domain";
|
||||||
|
|
||||||
export function transformRow(model: Model) {
|
export function transformRow(model: Model) {
|
||||||
@ -39,7 +39,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|||||||
ReferenceField: (_, v) => v,
|
ReferenceField: (_, v) => v,
|
||||||
FloatField: (_, v) => v,
|
FloatField: (_, v) => v,
|
||||||
DecimalField: (_, v) => v,
|
DecimalField: (_, v) => v,
|
||||||
TimestampField: (_, v) => new PosixDate(v),
|
PosixDateField: (_, v) => new PosixDate(v),
|
||||||
EmbeddedField: (_, v: string) => JSON.parse(v),
|
EmbeddedField: (_, v: string) => JSONUtils.parse(v),
|
||||||
BooleanField: (_, v) => v,
|
BooleanField: (_, v) => v,
|
||||||
|
EmailField: (_, v) => v,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// 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 { FieldDefinition, FieldToType } from "@fabric/domain";
|
||||||
|
import { isNullish } from "@fabric/validations";
|
||||||
|
|
||||||
type FieldSQLInsertMap = {
|
type FieldSQLInsertMap = {
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
[K in FieldDefinition[VariantTag]]: (
|
||||||
@ -20,13 +21,14 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|||||||
ReferenceField: (_, v) => v,
|
ReferenceField: (_, v) => v,
|
||||||
FloatField: (_, v) => v,
|
FloatField: (_, v) => v,
|
||||||
DecimalField: (_, v) => v,
|
DecimalField: (_, v) => v,
|
||||||
TimestampField: (_, v) => v.timestamp,
|
PosixDateField: (_, v) => v.timestamp,
|
||||||
EmbeddedField: (_, v: string) => JSON.stringify(v),
|
EmbeddedField: (_, v: string) => JSONUtils.stringify(v),
|
||||||
BooleanField: (_, v) => v,
|
BooleanField: (_, v) => v,
|
||||||
|
EmailField: (_, v) => v,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
||||||
if (value === null) {
|
if (isNullish(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const r = FieldSQLInsertMap[field[VariantTag]] as any;
|
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 { Field, isLike, Model } from "@fabric/domain";
|
||||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
||||||
import {
|
import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing";
|
||||||
afterEach,
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
expectTypeOf,
|
|
||||||
test,
|
|
||||||
} from "@fabric/testing";
|
|
||||||
import { SQLiteStateStore } from "./state-store.ts";
|
import { SQLiteStateStore } from "./state-store.ts";
|
||||||
|
|
||||||
describe("State Store", () => {
|
describe("State Store", () => {
|
||||||
@ -16,6 +9,7 @@ describe("State Store", () => {
|
|||||||
Model.entityFrom("demo", {
|
Model.entityFrom("demo", {
|
||||||
value: Field.float(),
|
value: Field.float(),
|
||||||
owner: Field.reference({ targetModel: "users" }),
|
owner: Field.reference({ targetModel: "users" }),
|
||||||
|
optional: Field.string({ isOptional: true }),
|
||||||
}),
|
}),
|
||||||
Model.entityFrom("users", {
|
Model.entityFrom("users", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
@ -52,12 +46,12 @@ describe("State Store", () => {
|
|||||||
|
|
||||||
const result = await store.from("users").select().unwrapOrThrow();
|
const result = await store.from("users").select().unwrapOrThrow();
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
// expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
// {
|
||||||
id: UUID;
|
// id: UUID;
|
||||||
name: string;
|
// name: string;
|
||||||
}[]
|
// }[]
|
||||||
>();
|
// >();
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
@ -95,12 +89,12 @@ describe("State Store", () => {
|
|||||||
})
|
})
|
||||||
.select().unwrapOrThrow();
|
.select().unwrapOrThrow();
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
// expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
// {
|
||||||
id: UUID;
|
// id: UUID;
|
||||||
name: string;
|
// name: string;
|
||||||
}[]
|
// }[]
|
||||||
>();
|
// >();
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,13 +17,13 @@ describe("Sanitize String", () => {
|
|||||||
test("Given a number value it should convert it to a string", () => {
|
test("Given a number value it should convert it to a string", () => {
|
||||||
const sanitized = parseAndSanitizeString(123);
|
const sanitized = parseAndSanitizeString(123);
|
||||||
|
|
||||||
expect(sanitized).toBe("123");
|
expect(sanitized).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Given a boolean value it should convert it to a string", () => {
|
test("Given a boolean value it should convert it to a string", () => {
|
||||||
const sanitized = parseAndSanitizeString(true);
|
const sanitized = parseAndSanitizeString(true);
|
||||||
|
|
||||||
expect(sanitized).toBe("true");
|
expect(sanitized).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Given a null value it should return null", () => {
|
test("Given a null value it should return null", () => {
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { isNullish } from "../nullish/is-nullish.ts";
|
|||||||
export function parseAndSanitizeString(
|
export function parseAndSanitizeString(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (isNullish(value)) return undefined;
|
if (isNullish(value) || typeof value != "string") return undefined;
|
||||||
return stripLow((String(value)).trim());
|
return stripLow(value).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// deno-lint-ignore no-control-regex
|
// deno-lint-ignore no-control-regex
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user