Compare commits

..

No commits in common. "bfb471b1660bb953880bafa367a6cc9cb5311155" and "c5cf78510a9c926d3da4f526c33ad64c1539ca37" have entirely different histories.

151 changed files with 1217 additions and 3157 deletions

View File

@ -9,24 +9,24 @@
"apps/**/*" "apps/**/*"
], ],
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.12.0", "@eslint/js": "^9.10.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9.12.0", "eslint": "^9.10.0",
"husky": "^9.1.6", "husky": "^9.1.6",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tsx": "^4.19.1", "tsx": "^4.19.1",
"typescript": "^5.6.3", "typescript": "^5.6.2",
"typescript-eslint": "^8.8.1", "typescript-eslint": "^8.6.0",
"zx": "^8.1.9" "zx": "^8.1.7"
}, },
"scripts": { "scripts": {
"lint": "eslint . --fix --report-unused-disable-directives", "lint": "eslint . --fix --report-unused-disable-directives",
"format": "prettier --write .", "format": "prettier --write .",
"test": "yarn workspaces foreach -vvpA run test --run --clearScreen false", "test": "yarn workspaces foreach -vvpA run test --run --clearScreen false",
"build": "yarn workspaces foreach -vvpA --topological run build", "build": "yarn workspaces foreach -vvpA --topological-dev run build",
"add-package": "tsx ./scripts/add-package.ts", "add-package": "tsx ./scripts/add-package.ts",
"postinstall": "husky" "postinstall": "husky"
} }

View File

@ -1 +1 @@
# @fabric/core # @ulthar/fabric-core

View File

@ -1,25 +1,31 @@
{ {
"name": "@fabric/core", "name": "@ulthar/fabric-core",
"private": true,
"sideEffects": false,
"type": "module", "type": "module",
"main": "./dist/index.js", "module": "dist/index.js",
"types": "./dist/index.d.ts", "main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { "exports": {
".": "./dist/index.js" ".": "./dist/index.js",
"./domain": "./dist/domain.js",
"./validation": "./dist/validation.js",
"./validation/fields": "./dist/validation/fields/index.js"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"private": true,
"packageManager": "yarn@4.1.1", "packageManager": "yarn@4.1.1",
"devDependencies": { "devDependencies": {
"@vitest/coverage-v8": "^2.1.2", "@types/validator": "^13.12.2",
"typescript": "^5.6.3", "typescript": "^5.6.2",
"vitest": "^2.1.2" "vitest": "^2.1.1"
}, },
"scripts": { "scripts": {
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage",
"build": "tsc -p tsconfig.build.json" "build": "tsc -p tsconfig.build.json"
},
"sideEffects": false,
"dependencies": {
"validator": "^13.12.0"
} }
} }

View File

@ -1,4 +1,4 @@
import { UUID } from "./uuid.js"; import { UUID } from "../types/uuid.js";
/** /**
* An entity is a domain object that is defined by its identity. * An entity is a domain object that is defined by its identity.

View File

@ -0,0 +1,9 @@
import { BaseFile } from "../../../files/base-file.js";
import { Entity } from "../entity.js";
/**
* Represents a file as managed by the domain.
*/
export interface DomainFile extends BaseFile, Entity {
url: string;
}

View File

@ -0,0 +1,9 @@
import { ImageMimeType } from "../../../files/mime-type.js";
import { DomainFile } from "./domain-file.js";
/**
* Represents an image file.
*/
export interface ImageFile extends DomainFile {
mimeType: ImageMimeType;
}

View File

@ -0,0 +1,2 @@
export * from "./domain-file.js";
export * from "./image-file.js";

View File

@ -0,0 +1 @@
export * from "./entity.js";

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedVariant } from "../../variant/variant.js";
import { UUID } from "../types/uuid.js";
/**
* An event is a tagged variant with a payload and a timestamp.
*/
export interface Event<TTag extends string = string, TPayload = any>
extends TaggedVariant<TTag> {
streamId: UUID;
payload: TPayload;
}

View File

@ -0,0 +1,5 @@
export * from "./entity/index.js";
export * from "./models/index.js";
export * from "./security/index.js";
export * from "./types/index.js";
export * from "./use-case/index.js";

View File

@ -0,0 +1,32 @@
import { FieldDefinition } from "./fields/index.js";
export interface ModelDefinition<
TName extends string = string,
TFields extends Record<string, FieldDefinition> = Record<
string,
FieldDefinition
>,
> {
name: TName;
fields: TFields;
}
export type ModelName<
TModel extends ModelDefinition<string, Record<string, FieldDefinition>>,
> = TModel["name"];
export type ModelFromName<
TModels extends ModelDefinition<string, Record<string, FieldDefinition>>,
TName extends ModelName<TModels>,
> = Extract<TModels, { name: TName }>;
export type ModelFieldNames<
TModel extends ModelDefinition<string, Record<string, FieldDefinition>>,
> = keyof TModel["fields"];
export function createModel<
TName extends string,
TFields extends Record<string, FieldDefinition>,
>(opts: ModelDefinition<TName, TFields>): ModelDefinition<TName, TFields> {
return opts;
}

View File

@ -0,0 +1,9 @@
import { UUID } from "../../types/uuid.js";
import { StringField } from "./string-field.js";
import { UUIDField } from "./uuid-field.js";
export type FieldToType<TField> = TField extends StringField
? string
: TField extends UUIDField
? UUID
: never;

View File

@ -0,0 +1,10 @@
import { createStringField, StringField } from "./string-field.js";
import { createUUIDField, UUIDField } from "./uuid-field.js";
export * from "./base-field.js";
export type FieldDefinition = StringField | UUIDField;
export namespace Field {
export const string = createStringField;
export const uuid = createUUIDField;
}

View File

@ -1,4 +1,4 @@
import { TaggedVariant, VariantTag } from "@fabric/core"; import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
import { BaseField } from "./base-field.js"; import { BaseField } from "./base-field.js";
export interface StringFieldOptions extends BaseField { export interface StringFieldOptions extends BaseField {

View File

@ -0,0 +1,15 @@
import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
import { BaseField } from "./base-field.js";
export interface UUIDOptions extends BaseField {
isPrimaryKey?: boolean;
}
export interface UUIDField extends TaggedVariant<"UUIDField">, UUIDOptions {}
export function createUUIDField(opts: UUIDOptions): UUIDField {
return {
[VariantTag]: "UUIDField",
...opts,
};
}

View File

@ -0,0 +1,4 @@
export * from "./create-model.js";
export * from "./fields/index.js";
export * from "./model-to-type.js";
export * from "./relations/index.js";

View File

@ -0,0 +1,6 @@
import { ModelDefinition } from "./create-model.js";
import { FieldToType } from "./fields/field-to-type.js";
export type ModelToType<TModel extends ModelDefinition> = {
[K in keyof TModel["fields"]]: FieldToType<TModel["fields"][K]>;
};

View File

@ -0,0 +1,5 @@
import { OneToOneRelation } from "./one-to-one.js";
export type RelationDefinition = OneToOneRelation<string, string>;
export namespace Relations {}

View File

@ -0,0 +1,34 @@
import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
export interface OneToManyRelationOptions<
TOwner extends string,
TTarget extends string,
> {
/**
* The owner of the relation. In this case is the "one" side of the relation.
*/
owner: TOwner;
/**
* The target of the relation. In this case is the "many" side of the relation.
*/
target: TTarget;
}
export interface OneToManyRelation<
TOwner extends string,
TTarget extends string,
> extends TaggedVariant<"ONE_TO_MANY_RELATION">,
OneToManyRelationOptions<TOwner, TTarget> {}
export function createOneToManyRelation<
TOwner extends string,
TTarget extends string,
>(
opts: OneToManyRelationOptions<TOwner, TTarget>,
): OneToManyRelation<TOwner, TTarget> {
return {
[VariantTag]: "ONE_TO_MANY_RELATION",
...opts,
};
}

View File

@ -0,0 +1,33 @@
import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
export interface OneToOneRelationOptions<
TOwner extends string,
TTarget extends string,
> {
/**
* The owner of the relation.
*/
owner: TOwner;
/**
* The target of the relation
*
*/
target: TTarget;
}
export interface OneToOneRelation<TOwner extends string, TTarget extends string>
extends TaggedVariant<"ONE_TO_ONE_RELATION">,
OneToOneRelationOptions<TOwner, TTarget> {}
export function createOneToOneRelation<
TOwner extends string,
TTarget extends string,
>(
opts: OneToOneRelationOptions<TOwner, TTarget>,
): OneToOneRelation<TOwner, TTarget> {
return {
[VariantTag]: "ONE_TO_ONE_RELATION",
...opts,
};
}

View File

@ -0,0 +1,3 @@
export * from "./email.js";
export * from "./semver.js";
export * from "./uuid.js";

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "@fabric/core"; import { TaggedError } from "../../error/tagged-error.js";
import { UseCase } from "./use-case.js"; import { UseCase } from "./use-case.js";
export type UseCaseDefinition< export type UseCaseDefinition<
@ -25,6 +25,11 @@ interface BasicUseCaseDefinition<
*/ */
isAuthRequired: boolean; isAuthRequired: boolean;
/**
* The required permissions to execute the use case.
**/
requiredPermissions?: string[];
/** /**
* The use case function. * The use case function.
*/ */

View File

@ -1,4 +1,5 @@
import { AsyncResult, TaggedError } from "@fabric/core"; import { TaggedError } from "../../error/tagged-error.js";
import { AsyncResult } from "../../result/async-result.js";
/** /**
* A use case is a piece of domain logic that can be executed. * A use case is a piece of domain logic that can be executed.

View File

@ -3,15 +3,14 @@ import { TaggedVariant, VariantTag } from "../variant/index.js";
/** /**
* A TaggedError is a tagged variant with an error message. * A TaggedError is a tagged variant with an error message.
*/ */
export abstract class TaggedError<Tag extends string = string> export class TaggedError<Tag extends string>
extends Error extends Error
implements TaggedVariant<Tag> implements TaggedVariant<Tag>
{ {
readonly [VariantTag]: Tag; readonly [VariantTag]: Tag;
constructor(tag: Tag, message?: string) { constructor(tag: Tag) {
super(message); super();
this[VariantTag] = tag; this[VariantTag] = tag;
this.name = tag;
} }
} }

View File

@ -8,7 +8,12 @@ import { TaggedError } from "./tagged-error.js";
* we must be prepared to handle. * we must be prepared to handle.
*/ */
export class UnexpectedError extends TaggedError<"UnexpectedError"> { export class UnexpectedError extends TaggedError<"UnexpectedError"> {
constructor(message?: string) { constructor(readonly context: Record<string, unknown> = {}) {
super("UnexpectedError", message); super("UnexpectedError");
this.message = "An unexpected error occurred";
}
toString() {
return `UnexpectedError: ${this.message}\n${JSON.stringify(this.context, null, 2)}`;
} }
} }

View File

@ -1,10 +1,8 @@
export * from "./base-file.js"; export * from "./base-file.js";
export * from "./bytes.js"; export * from "./bytes.js";
export * from "./image-file.js";
export * from "./in-memory-file.js";
export * from "./invalid-file-type-error.js"; export * from "./invalid-file-type-error.js";
export * from "./is-in-memory-file.js";
export * from "./is-mime-type.js"; export * from "./is-mime-type.js";
export * from "./is-uploaded-file.js";
export * from "./media-file.js"; export * from "./media-file.js";
export * from "./mime-type.js"; export * from "./mime-type.js";
export * from "./stored-file.js"; export * from "./uploaded-file.js";

View File

@ -1,4 +1,4 @@
import { TaggedError } from "@fabric/core"; import { TaggedError } from "../error/tagged-error.js";
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> { export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
constructor() { constructor() {

View File

@ -1,5 +1,8 @@
import { isRecord } from "@fabric/core"; import validator from "validator";
import { InMemoryFile } from "./in-memory-file.js"; import { isRecord } from "../record/is-record.js";
import { InMemoryFile } from "./uploaded-file.js";
const { isBase64, isMimeType } = validator;
export function isInMemoryFile(value: unknown): value is InMemoryFile { export function isInMemoryFile(value: unknown): value is InMemoryFile {
try { try {
@ -7,8 +10,10 @@ export function isInMemoryFile(value: unknown): value is InMemoryFile {
isRecord(value) && isRecord(value) &&
"data" in value && "data" in value &&
typeof value.data === "string" && typeof value.data === "string" &&
isBase64(value.data.split(",")[1]) &&
"mimeType" in value && "mimeType" in value &&
typeof value.mimeType === "string" && typeof value.mimeType === "string" &&
isMimeType(value.mimeType) &&
"name" in value && "name" in value &&
typeof value.name === "string" && typeof value.name === "string" &&
"sizeInBytes" in value && "sizeInBytes" in value &&

View File

@ -1,8 +1,8 @@
import { StoredFile } from "./stored-file.js"; import { DomainFile } from "../domain/entity/files/domain-file.js";
/** /**
* Represents a media file, either an image, a video or an audio file. * Represents a media file, either an image, a video or an audio file.
*/ */
export interface MediaFile extends StoredFile { export interface MediaFile extends DomainFile {
mimeType: `image/${string}` | `video/${string}` | `audio/${string}`; mimeType: `image/${string}` | `video/${string}` | `audio/${string}`;
} }

View File

@ -1,4 +1,4 @@
import { Base64String } from "../types/base-64.js"; import { Base64String } from "../domain/types/base-64.js";
import { BaseFile } from "./base-file.js"; import { BaseFile } from "./base-file.js";
/** /**

View File

@ -1,9 +1,9 @@
export * from "./array/index.js"; export * from "./array/index.js";
export * from "./domain/index.js";
export * from "./error/index.js"; export * from "./error/index.js";
export * from "./record/index.js"; export * from "./record/index.js";
export * from "./result/index.js"; export * from "./result/index.js";
export * from "./run/index.js"; export * from "./storage/index.js";
export * from "./time/index.js"; export * from "./time/index.js";
export * from "./types/index.js"; export * from "./types/index.js";
export * from "./utils/index.js";
export * from "./variant/index.js"; export * from "./variant/index.js";

View File

@ -1,33 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "../error/tagged-error.js"; import { TaggedError } from "../error/tagged-error.js";
import { UnexpectedError } from "../error/unexpected-error.js";
import { MaybePromise } from "../types/maybe-promise.js";
import { Result } from "./result.js"; import { Result } from "./result.js";
/** /**
* An AsyncResult represents the result of an asynchronous operation that can * Un AsyncResult representa el resultado de una operación asíncrona que puede
* resolve to a value of type `TValue` or an error of type `TError`. * resolver en un valor de tipo `TValue` o en un error de tipo `TError`.
*/ */
export type AsyncResult< export type AsyncResult<
TValue = any, TValue,
TError extends TaggedError = never, TError extends TaggedError<string> = never,
> = Promise<Result<TValue, TError>>; > = Promise<Result<TValue, TError>>;
export namespace AsyncResult {
export async function tryFrom<T, TError extends TaggedError>(
fn: () => MaybePromise<T>,
errorMapper: (error: any) => TError,
): AsyncResult<T, TError> {
try {
return Result.succeedWith(await fn());
} catch (error) {
return Result.failWith(errorMapper(error));
}
}
export async function from<T>(
fn: () => MaybePromise<T>,
): AsyncResult<T, never> {
return tryFrom(fn, (error) => new UnexpectedError(error) as never);
}
}

View File

@ -1,57 +0,0 @@
import { describe, expect, expectTypeOf, it, vitest } from "vitest";
import { UnexpectedError } from "../error/unexpected-error.js";
import { Result } from "./result.js";
describe("Result", () => {
describe("isOk", () => {
it("should return true if the result is ok", () => {
const result = Result.succeedWith(1) as Result<number, UnexpectedError>;
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value).toEqual(1);
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
}
});
});
describe("isError", () => {
it("should return true if the result is an error", () => {
const result = Result.failWith(new UnexpectedError()) as Result<
number,
UnexpectedError
>;
expect(result.isError()).toBe(true);
if (result.isError()) {
expect(result.value).toBeInstanceOf(UnexpectedError);
expectTypeOf(result).toEqualTypeOf<Result<never, UnexpectedError>>();
}
});
});
describe("Map", () => {
it("should return the result of the last function", () => {
const x = 0;
const result = Result.succeedWith(x + 1).map((x) => x * 2);
expect(result.unwrapOrThrow()).toEqual(2);
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
});
it("should not execute the function if the result is an error", () => {
const fn = vitest.fn();
const result = Result.failWith(new UnexpectedError()).map(fn);
expect(result.isError()).toBe(true);
expect(fn).not.toHaveBeenCalled();
});
});
});

View File

@ -1,151 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { isError } from "../error/is-error.js";
import { TaggedError } from "../error/tagged-error.js"; import { TaggedError } from "../error/tagged-error.js";
/** /**
* A Result represents the outcome of an operation * Un Result representa el resultado de una operación
* that can be either a value of type `TValue` or an error `TError`. * que puede ser un valor de tipo `TValue` o un error `TError`.
*/ */
export class Result<TValue, TError extends TaggedError = never> { export type Result<TValue, TError extends TaggedError<string>> =
static succeedWith<T>(value: T): Result<T, never> { | TValue
return new Result<T, never>(value); | TError;
}
static failWith<T extends TaggedError>(error: T): Result<never, T> {
return new Result<never, T>(error);
}
static ok(): Result<void, never>;
static ok<T>(value: T): Result<T, never>;
static ok(value?: any) {
return new Result(value ?? undefined);
}
static tryFrom<T, TError extends TaggedError>(
fn: () => T,
errorMapper: (error: any) => TError,
): Result<T, TError> {
try {
return Result.succeedWith(fn());
} catch (error) {
return Result.failWith(errorMapper(error));
}
}
private constructor(readonly value: TValue | TError) {}
/**
* Unwrap the value of the result.
* If the result is an error, it will throw the error.
*/
unwrapOrThrow(): TValue {
if (isError(this.value)) {
throw this.value;
}
return this.value as TValue;
}
/**
* Throw the error if the result is an error.
* otherwise, do nothing.
*/
orThrow(): void {
if (isError(this.value)) {
throw this.value;
}
}
unwrapErrorOrThrow(): TError {
if (!isError(this.value)) {
throw new Error("Result is not an error");
}
return this.value;
}
/**
* Check if the result is a success.
*/
isOk(): this is Result<TValue, never> {
return !isError(this.value);
}
/**
* Check if the result is an error.
*/
isError(): this is Result<never, TError> {
return isError(this.value);
}
/**
* Map a function over the value of the result.
*/
map<TMappedValue>(
fn: (value: TValue) => TMappedValue,
): Result<TMappedValue, TError> {
if (!isError(this.value)) {
return Result.succeedWith(fn(this.value as TValue));
}
return this as any;
}
/**
* Maps a function over the value of the result and flattens the result.
*/
flatMap<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => Result<TMappedValue, TMappedError>,
): Result<TMappedValue, TError | TMappedError> {
if (!isError(this.value)) {
return fn(this.value as TValue) as any;
}
return this as any;
}
/**
* Try to map a function over the value of the result.
* If the function throws an error, the result will be a failure.
*/
tryMap<TMappedValue>(
fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError,
): Result<TMappedValue, TError> {
if (!isError(this.value)) {
try {
return Result.succeedWith(fn(this.value as TValue));
} catch (error) {
return Result.failWith(errMapper(error));
}
}
return this as any;
}
/**
* Map a function over the error of the result.
*/
mapError<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError,
): Result<TValue, TMappedError> {
if (isError(this.value)) {
return Result.failWith(fn(this.value as TError));
}
return this as unknown as Result<TValue, TMappedError>;
}
/**
* Taps a function if the result is a success.
* This is useful for side effects that do not modify the result.
*/
tap(fn: (value: TValue) => void): Result<TValue, TError> {
if (!isError(this.value)) {
fn(this.value as TValue);
}
return this;
}
}

View File

@ -1 +0,0 @@
export * from "./run.js";

View File

@ -1,28 +0,0 @@
import { describe, expect, it } from "vitest";
import { UnexpectedError } from "../error/unexpected-error.js";
import { Result } from "../result/result.js";
import { Run } from "./run.js";
describe("Run", () => {
describe("In Sequence", () => {
it("should pipe the results of multiple async functions", async () => {
const result = await Run.seq(
async () => Result.succeedWith(1),
async (x) => Result.succeedWith(x + 1),
async (x) => Result.succeedWith(x * 2),
);
expect(result.unwrapOrThrow()).toEqual(4);
});
it("should return the first error if one of the functions fails", async () => {
const result = await Run.seq(
async () => Result.succeedWith(1),
async () => Result.failWith(new UnexpectedError()),
async (x) => Result.succeedWith(x * 2),
);
expect(result.isError()).toBe(true);
});
});
});

View File

@ -1,87 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "../error/tagged-error.js";
import { AsyncResult } from "../result/async-result.js";
export namespace Run {
// prettier-ignore
export async function seq<
T1, TE1 extends TaggedError,
T2, TE2 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): AsyncResult<T2, TE1 | TE2>;
// prettier-ignore
export async function seq<
T1, TE1 extends TaggedError,
T2, TE2 extends TaggedError,
T3, TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): AsyncResult<T3, TE1 | TE2 | TE3>;
// prettier-ignore
export async function seq<
T1, TE1 extends TaggedError,
T2, TE2 extends TaggedError,
T3, TE3 extends TaggedError,
T4, TE4 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
fn4: (value: T3) => AsyncResult<T4, TE4>,
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
export async function seq(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): AsyncResult<any, any> {
let result = await fns[0]();
for (let i = 1; i < fns.length; i++) {
if (result.isError()) {
return result;
}
result = await fns[i](result.unwrapOrThrow());
}
return result;
}
// prettier-ignore
export async function seqUNSAFE<
T1, TE1 extends TaggedError,
T2, TE2 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): Promise<T2>;
// prettier-ignore
export async function seqUNSAFE<
T1,TE1 extends TaggedError,
T2,TE2 extends TaggedError,
T3,TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): Promise<T2>;
export async function seqUNSAFE(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): Promise<any> {
const result = await (seq as any)(...fns);
if (result.isError()) {
throw result.unwrapOrThrow();
}
return result.unwrapOrThrow();
}
export async function UNSAFE<T, TError extends TaggedError>(
fn: () => AsyncResult<T, TError>,
): Promise<T> {
return (await fn()).unwrapOrThrow();
}
}

View File

@ -1,4 +1,4 @@
import { TaggedError } from "@fabric/core"; import { TaggedError } from "../../error/tagged-error.js";
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> { export class CircularDependencyError extends TaggedError<"CircularDependencyError"> {
context: { key: string; dep: string }; context: { key: string; dep: string };

View File

@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "../../error/tagged-error.js";
export class StoreQueryError extends TaggedError<"StoreQueryError"> {
constructor(
public message: string,
public context: any,
) {
super("StoreQueryError");
}
}

View File

@ -0,0 +1,43 @@
import { Event } from "../domain/events/event.js";
import { UUID } from "../domain/index.js";
import { AsyncResult } from "../result/async-result.js";
import { PosixDate } from "../time/posix-date.js";
import { MaybePromise } from "../types/maybe-promise.js";
import { StoreQueryError } from "./errors/query-error.js";
export interface EventStore<TEvent extends Event = Event> {
getStream<TEventStreamEvent extends TEvent>(
streamId: UUID,
): AsyncResult<EventStream<TEventStreamEvent>, StoreQueryError>;
appendToStream<TEvent extends Event>(
streamId: UUID,
events: TEvent,
): AsyncResult<void, StoreQueryError>;
}
export interface EventStream<TEvent extends Event = Event> {
getCurrentVersion(): bigint;
append(events: TEvent): AsyncResult<StoredEvent<TEvent>, StoreQueryError>;
subscribe(callback: (event: StoredEvent<TEvent>) => MaybePromise<void>): void;
getEvents(
opts?: EventFilterOptions,
): AsyncResult<StoredEvent<TEvent>[], StoreQueryError>;
}
export interface EventFilterOptions {
fromDate?: PosixDate;
toDate?: PosixDate;
fromVersion?: number;
toVersion?: number;
limit?: number;
offset?: number;
}
export type StoredEvent<TEvent extends Event = Event> = TEvent & {
version: bigint;
timestamp: number;
};

View File

@ -0,0 +1,5 @@
export * from "./errors/index.js";
export * from "./event-store.js";
export * from "./query/index.js";
export * from "./state-store.js";
export * from "./storage-driver.js";

View File

@ -4,21 +4,19 @@ export type FilterOptions<T = any> =
| SingleFilterOption<T> | SingleFilterOption<T>
| MultiFilterOption<T>; | MultiFilterOption<T>;
export type FilterValue<T = any, K extends keyof T = keyof T> = export type SingleFilterOption<T = any> = {
[K in keyof T]?:
| T[K] | T[K]
| LikeFilterOption<T[K]> | LikeFilterOption<T[K]>
| ComparisonFilterOption<T[K]> | ComparisonFilterOption<T[K]>
| InFilterOption<T[K]>; | InFilterOption<T[K]>;
export type SingleFilterOption<T = any> = {
[K in keyof T]?: FilterValue<T, K>;
}; };
export type MultiFilterOption<T = any> = SingleFilterOption<T>[]; export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type"; export const FILTER_OPTION_TYPE_SYMBOL = Symbol("$type");
export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value"; export const FILTER_OPTION_VALUE_SYMBOL = Symbol("$value");
export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator"; export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("$operator");
export type LikeFilterOption<T> = T extends string export type LikeFilterOption<T> = T extends string
? { ? {

View File

@ -1,3 +1,4 @@
export * from "./filter-options.js"; export * from "./filter-options.js";
export * from "./order-by-options.js"; export * from "./order-by-options.js";
export * from "./query-builder.js";
export * from "./query.js"; export * from "./query.js";

View File

@ -0,0 +1,75 @@
import {
ModelDefinition,
ModelFromName,
ModelName,
} from "../../domain/models/create-model.js";
import { ModelToType } from "../../domain/models/model-to-type.js";
import { AsyncResult } from "../../result/async-result.js";
import { Keyof } from "../../types/index.js";
import { StoreQueryError } from "../errors/query-error.js";
import { StorageDriver } from "../storage-driver.js";
import { FilterOptions } from "./filter-options.js";
import { OrderByOptions } from "./order-by-options.js";
import {
QueryDefinition,
SelectableQuery,
StoreLimitableQuery,
StoreQuery,
StoreSortableQuery,
} from "./query.js";
export class QueryBuilder<
TModels extends ModelDefinition,
TEntityName extends ModelName<TModels>,
T = ModelToType<ModelFromName<TModels, TEntityName>>,
> implements StoreQuery<T>
{
constructor(
private driver: StorageDriver,
private query: QueryDefinition<TEntityName>,
) {}
where(where: FilterOptions<T>): StoreSortableQuery<T> {
this.query = {
...this.query,
where,
};
return this;
}
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
this.query = {
...this.query,
orderBy: opts,
};
return this;
}
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
this.query = {
...this.query,
limit,
offset,
};
return this;
}
select<K extends Keyof<T>>(
keys?: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError> {
return this.driver.select({
...this.query,
keys,
});
}
selectOne<K extends Keyof<T>>(
keys?: K[],
): AsyncResult<Pick<T, K>, StoreQueryError> {
return this.driver.selectOne({
...this.query,
keys,
});
}
}

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { AsyncResult, Keyof, Optional } from "@fabric/core"; import { AsyncResult } from "../../result/async-result.js";
import { StoreQueryError } from "../../errors/query-error.js"; import { Keyof } from "../../types/keyof.js";
import { StoreQueryError } from "../errors/query-error.js";
import { FilterOptions } from "./filter-options.js"; import { FilterOptions } from "./filter-options.js";
import { OrderByOptions } from "./order-by-options.js"; import { OrderByOptions } from "./order-by-options.js";
@ -14,10 +15,10 @@ export interface StoreQuery<T> {
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<T, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Pick<T, K>, StoreQueryError>;
} }
export interface StoreSortableQuery<T> { export interface StoreSortableQuery<T> {
@ -29,10 +30,10 @@ export interface StoreSortableQuery<T> {
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<T, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Pick<T, K>, StoreQueryError>;
} }
export interface StoreLimitableQuery<T> { export interface StoreLimitableQuery<T> {
@ -43,10 +44,10 @@ export interface StoreLimitableQuery<T> {
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<T, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Pick<T, K>, StoreQueryError>;
} }
export interface SelectableQuery<T> { export interface SelectableQuery<T> {
@ -55,10 +56,10 @@ export interface SelectableQuery<T> {
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<T, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Pick<T, K>, StoreQueryError>;
} }
export interface QueryDefinition<K extends string = string> { export interface QueryDefinition<K extends string = string> {

View File

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
ModelDefinition,
ModelFromName,
ModelName,
} from "../domain/models/create-model.js";
import { ModelToType } from "../domain/models/model-to-type.js";
import { StoreQuery } from "./query/query.js";
export interface StateStore<
TModels extends ModelDefinition<string, Record<string, any>>,
> {
from<TEntityName extends ModelName<TModels>>(
entityName: TEntityName,
): StoreQuery<ModelToType<ModelFromName<TModels, TEntityName>>>;
}

View File

@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ModelDefinition } from "../domain/models/create-model.js";
import { UnexpectedError } from "../error/unexpected-error.js";
import { AsyncResult } from "../result/async-result.js";
import { CircularDependencyError } from "./errors/circular-dependency-error.js";
import { StoreQueryError } from "./errors/query-error.js";
import { QueryDefinition } from "./query/query.js";
export interface StorageDriver {
/**
* Insert data into the store
*/
insert(
collectionName: string,
record: Record<string, any>,
): AsyncResult<void, StoreQueryError>;
/**
* Run a select query against the store.
*/
select(query: QueryDefinition): AsyncResult<any[], StoreQueryError>;
/**
* Run a select query against the store.
*/
selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError>;
/**
* Sincronice the store with the schema.
*/
sync(
schema: ModelDefinition[],
): AsyncResult<void, StoreQueryError | CircularDependencyError>;
/**
* Drop the store. This is a destructive operation.
*/
drop(): AsyncResult<void, StoreQueryError>;
/**
* Close the store.
*/
close(): AsyncResult<void, UnexpectedError>;
/**
* Update a record in the store.
*/
update(
collectionName: string,
id: string,
record: Record<string, any>,
): AsyncResult<void, StoreQueryError>;
/**
* Delete a record from the store.
*/
delete(
collectionName: string,
id: string,
): AsyncResult<void, StoreQueryError>;
}

View File

@ -15,7 +15,7 @@ describe("sortByDependencies", () => {
const result = sortByDependencies(array, { const result = sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}).unwrapOrThrow(); });
expect(result).toEqual([ expect(result).toEqual([
{ id: 3, name: "C", dependencies: [] }, { id: 3, name: "C", dependencies: [] },
@ -35,7 +35,7 @@ describe("sortByDependencies", () => {
sortByDependencies(array, { sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}).unwrapErrorOrThrow(), }),
).toBeInstanceOf(CircularDependencyError); ).toBeInstanceOf(CircularDependencyError);
}); });
@ -45,7 +45,7 @@ describe("sortByDependencies", () => {
const result = sortByDependencies(array, { const result = sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}).unwrapOrThrow(); });
expect(result).toEqual([]); expect(result).toEqual([]);
}); });

View File

@ -1,4 +1,4 @@
import { Result } from "@fabric/core"; import { Result } from "../../result/result.js";
import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { CircularDependencyError } from "../errors/circular-dependency-error.js";
export function sortByDependencies<T>( export function sortByDependencies<T>(
@ -34,15 +34,14 @@ export function sortByDependencies<T>(
visited.add(key); visited.add(key);
sorted.push(key); sorted.push(key);
}; };
return Result.tryFrom( try {
() => {
graph.forEach((deps, key) => { graph.forEach((deps, key) => {
visit(key, []); visit(key, []);
}); });
} catch (e) {
return e as CircularDependencyError;
}
return sorted.map( return sorted.map(
(key) => array.find((element) => keyGetter(element) === key) as T, (key) => array.find((element) => keyGetter(element) === key) as T,
); );
},
(e) => e as CircularDependencyError,
);
} }

View File

@ -1,38 +1,9 @@
import { isRecord } from "../record/is-record.js";
import { TaggedVariant } from "../variant/variant.js"; import { TaggedVariant } from "../variant/variant.js";
export class PosixDate { export class PosixDate {
constructor(public readonly timestamp: number = Date.now()) {} constructor(public readonly timestamp: number) {}
public toJSON(): PosixDateJSON {
return {
type: "posix-date",
timestamp: this.timestamp,
};
}
public static fromJson(json: PosixDateJSON): PosixDate {
return new PosixDate(json.timestamp);
}
public static isPosixDateJSON(value: unknown): value is PosixDateJSON {
if (
isRecord(value) &&
"type" in value &&
"timestamp" in value &&
value["type"] === "posix-date" &&
typeof value["timestamp"] === "number"
)
return true;
return false;
}
} }
export interface TimeZone extends TaggedVariant<"TimeZone"> { export interface TimeZone extends TaggedVariant<"TimeZone"> {
timestamp: number; timestamp: number;
} }
export interface PosixDateJSON {
type: "posix-date";
timestamp: number;
}

View File

@ -1 +1 @@
export type EnumToType<T extends Record<string, string>> = T[keyof T]; export type EnumToValues<T extends Record<string, string>> = T[keyof T];

View File

@ -3,4 +3,3 @@ export * from "./fn.js";
export * from "./keyof.js"; export * from "./keyof.js";
export * from "./maybe-promise.js"; export * from "./maybe-promise.js";
export * from "./optional.js"; export * from "./optional.js";
export * from "./record.js";

View File

@ -1 +0,0 @@
export type EmptyRecord = Record<string, never>;

View File

@ -1,8 +0,0 @@
import { UnexpectedError } from "../error/unexpected-error.js";
export function ensureValue<T>(value?: T): T {
if (!value) {
throw new UnexpectedError("Value is undefined");
}
return value;
}

View File

@ -1 +0,0 @@
export * from "./ensure-value.js";

View File

@ -1,21 +1,17 @@
import { Fn } from "../types/fn.js"; import { Fn } from "../types/fn.js";
import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js"; import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js";
export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = { export type VariantMatcher<TVariant extends TaggedVariant<string>> = {
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>; [K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>>;
}; };
export function match<const TVariant extends TaggedVariant<string>>( export function match<const TVariant extends TaggedVariant<string>>(
v: TVariant, v: TVariant,
) { ) {
return { return {
case< case<const TMatcher extends VariantMatcher<TVariant>>(
const TReturnType, cases: TMatcher,
const TMatcher extends VariantMatcher< ): ReturnType<TMatcher[TVariant[VariantTag]]> {
TVariant,
TReturnType
> = VariantMatcher<TVariant, TReturnType>,
>(cases: TMatcher): TReturnType {
if (!(v[VariantTag] in cases)) { if (!(v[VariantTag] in cases)) {
throw new Error("Non-exhaustive pattern match"); throw new Error("Non-exhaustive pattern match");
} }

View File

@ -1,41 +0,0 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { TaggedVariant, Variant, VariantTag } from "./variant.js";
interface SuccessVariant extends TaggedVariant<"success"> {
[VariantTag]: "success";
data: string;
}
interface ErrorVariant extends TaggedVariant<"error"> {
[VariantTag]: "error";
message: string;
}
describe("Variant", () => {
describe("isVariant", () => {
const successVariant = {
[VariantTag]: "success",
data: "Operation successful",
} as SuccessVariant | ErrorVariant;
const errorVariant = {
[VariantTag]: "error",
message: "Operation failed",
} as SuccessVariant | ErrorVariant;
it("should return true for a matching tag and correctly infer it", () => {
if (Variant.is(successVariant, "success")) {
expectTypeOf(successVariant).toEqualTypeOf<SuccessVariant>();
}
if (Variant.is(errorVariant, "error")) {
expectTypeOf(errorVariant).toEqualTypeOf<ErrorVariant>();
}
});
it("should return false for a non-matching tag", () => {
expect(Variant.is(successVariant, "error")).toBe(false);
expect(Variant.is(errorVariant, "success")).toBe(false);
});
});
});

View File

@ -7,17 +7,5 @@ export interface TaggedVariant<TTag extends string> {
export type VariantFromTag< export type VariantFromTag<
TVariant extends TaggedVariant<string>, TVariant extends TaggedVariant<string>,
TTag extends TVariant[VariantTag], TTag extends TVariant[typeof VariantTag],
> = Extract<TVariant, { [VariantTag]: TTag }>; > = Extract<TVariant, { [VariantTag]: TTag }>;
export namespace Variant {
export function is<
TVariant extends TaggedVariant<string>,
TTag extends TVariant[VariantTag],
>(
variant: TVariant,
tag: TTag,
): variant is Extract<TVariant, { [VariantTag]: TTag }> {
return variant[VariantTag] === tag;
}
}

View File

@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
test: { test: {
coverage: { coverage: {
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], exclude: ["**/index.ts"],
}, },
passWithNoTests: true, passWithNoTests: true,
}, },

View File

@ -1 +0,0 @@
# model

View File

@ -1,30 +0,0 @@
{
"name": "@fabric/domain",
"private": true,
"sideEffects": false,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./mocks": "./dist/mocks.js"
},
"files": [
"dist"
],
"packageManager": "yarn@4.1.1",
"devDependencies": {
"@vitest/coverage-v8": "^2.1.2",
"typescript": "^5.6.3",
"vitest": "^2.1.2"
},
"dependencies": {
"@fabric/core": "workspace:^",
"decimal.js": "^10.4.3"
},
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage",
"build": "tsc -p tsconfig.build.json"
}
}

View File

@ -1,7 +0,0 @@
import { TaggedError } from "@fabric/core";
export class StoreQueryError extends TaggedError<"StoreQueryError"> {
constructor(public message: string) {
super("StoreQueryError", message);
}
}

View File

@ -1,42 +0,0 @@
import {
AsyncResult,
MaybePromise,
PosixDate,
VariantFromTag,
VariantTag,
} from "@fabric/core";
import { StoreQueryError } from "../errors/query-error.js";
import { UUID } from "../types/uuid.js";
import { Event } from "./event.js";
import { StoredEvent } from "./stored-event.js";
export interface EventStore<TEvents extends Event> {
/**
* Store a new event in the event store.
*/
append<T extends TEvents>(
event: T,
): AsyncResult<StoredEvent<T>, StoreQueryError>;
getEventsFromStream(
streamId: UUID,
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
subscribe<TEventKey extends TEvents[VariantTag]>(
events: TEventKey[],
subscriber: EventSubscriber<VariantFromTag<TEvents, TEventKey>>,
): void;
}
export type EventSubscriber<TEvents extends Event = Event> = (
event: StoredEvent<TEvents>,
) => MaybePromise<void>;
export interface EventFilterOptions {
fromDate?: PosixDate;
toDate?: PosixDate;
fromVersion?: bigint;
toVersion?: bigint;
limit?: number;
offset?: number;
}

View File

@ -1,18 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VariantTag } from "@fabric/core";
import { UUID } from "../types/uuid.js";
/**
* An event is a tagged variant with a payload and a timestamp.
*/
export interface Event<TTag extends string = string, TPayload = any> {
readonly [VariantTag]: TTag;
readonly id: UUID;
readonly streamId: UUID;
readonly payload: TPayload;
}
export type EventFromKey<
TEvents extends Event,
TKey extends TEvents[VariantTag],
> = Extract<TEvents, { [VariantTag]: TKey }>;

View File

@ -1,3 +0,0 @@
export * from "./event-store.js";
export * from "./event.js";
export * from "./stored-event.js";

View File

@ -1,10 +0,0 @@
import { PosixDate } from "@fabric/core";
import { Event } from "./event.js";
/**
* A stored event is an inmutable event, already stored, with it's version in the stream and timestamp.
*/
export type StoredEvent<TEvent extends Event> = TEvent & {
readonly version: bigint;
readonly timestamp: PosixDate;
};

View File

@ -1,9 +0,0 @@
import { ImageMimeType } from "./mime-type.js";
import { StoredFile } from "./stored-file.js";
/**
* Represents an image file.
*/
export interface ImageFile extends StoredFile {
mimeType: ImageMimeType;
}

View File

@ -1,9 +0,0 @@
import { Entity } from "../types/entity.js";
import { BaseFile } from "./base-file.js";
/**
* Represents a file as managed by the domain.
*/
export interface StoredFile extends BaseFile, Entity {
url: string;
}

View File

@ -1,9 +0,0 @@
export * from "./errors/index.js";
export * from "./events/index.js";
export * from "./files/index.js";
export * from "./models/index.js";
export * from "./security/index.js";
export * from "./services/index.js";
export * from "./types/index.js";
export * from "./use-case/index.js";
export * from "./utils/index.js";

View File

@ -1 +0,0 @@
export * from "./services/mocks.js";

View File

@ -1,21 +0,0 @@
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
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;
}

View File

@ -1,20 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars
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;
}

View File

@ -1,37 +0,0 @@
import { PosixDate } from "@fabric/core";
import { Decimal } from "decimal.js";
import { UUID } from "../../types/uuid.js";
import { DecimalField } from "./decimal.js";
import { EmbeddedField } from "./embedded.js";
import { FloatField } from "./float.js";
import { IntegerField } from "./integer.js";
import { ReferenceField } from "./reference-field.js";
import { StringField } from "./string-field.js";
import { TimestampField } from "./timestamp.js";
import { UUIDField } from "./uuid-field.js";
/**
* 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 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;

View File

@ -1,18 +0,0 @@
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
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;
}

View File

@ -1,32 +0,0 @@
import { createDecimalField, DecimalField } from "./decimal.js";
import { createEmbeddedField, EmbeddedField } from "./embedded.js";
import { createFloatField, FloatField } from "./float.js";
import { createIntegerField, IntegerField } from "./integer.js";
import { createReferenceField, ReferenceField } from "./reference-field.js";
import { createStringField, StringField } from "./string-field.js";
import { createTimestampField, TimestampField } from "./timestamp.js";
import { createUUIDField, UUIDField } from "./uuid-field.js";
export * from "./base-field.js";
export * from "./field-to-type.js";
export * from "./reference-field.js";
export type FieldDefinition =
| StringField
| UUIDField
| IntegerField
| FloatField
| DecimalField
| ReferenceField
| TimestampField
| EmbeddedField;
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;
}

View File

@ -1,20 +0,0 @@
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
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;
}

View File

@ -1,80 +0,0 @@
import { isError } from "@fabric/core";
import { describe, expect, it } from "vitest";
import { defineModel } from "../model.js";
import { Field } from "./index.js";
import {
InvalidReferenceFieldError,
validateReferenceField,
} from "./reference-field.js";
describe("Validate Reference Field", () => {
const schema = {
User: defineModel("User", {
name: Field.string(),
password: Field.string(),
otherUnique: Field.integer({ isUnique: true }),
otherNotUnique: Field.uuid(),
otherUser: Field.reference({
targetModel: "User",
}),
}),
};
it("should return an error when the target model is not in the schema", () => {
const result = validateReferenceField(
schema,
Field.reference({
targetModel: "foo",
}),
).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError);
});
it("should not return an error if the target model is in the schema", () => {
validateReferenceField(
schema,
Field.reference({
targetModel: "User",
}),
).unwrapOrThrow();
});
it("should return an error if the target key is not in the target model", () => {
const result = validateReferenceField(
schema,
Field.reference({
targetModel: "User",
targetKey: "foo",
}),
).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError);
});
it("should return error if the target key is not unique", () => {
const result = validateReferenceField(
schema,
Field.reference({
targetModel: "User",
targetKey: "otherNotUnique",
}),
).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError);
});
it("should not return an error if the target key is in the target model and is unique", () => {
const result = validateReferenceField(
schema,
Field.reference({
targetModel: "User",
targetKey: "otherUnique",
}),
);
if (isError(result)) {
throw result.toString();
}
});
});

View File

@ -1,65 +0,0 @@
import { Result, TaggedError, TaggedVariant, VariantTag } from "@fabric/core";
import { ModelSchema } from "../model-schema.js";
import { BaseField } from "./base-field.js";
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 {
return field.targetKey || "id";
}
export function validateReferenceField(
schema: ModelSchema,
field: ReferenceField,
): Result<void, InvalidReferenceFieldError> {
if (!schema[field.targetModel]) {
return Result.failWith(
new InvalidReferenceFieldError(
`The target model '${field.targetModel}' is not in the schema.`,
),
);
}
if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) {
return Result.failWith(
new InvalidReferenceFieldError(
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
),
);
}
if (
field.targetKey &&
!schema[field.targetModel].fields[field.targetKey].isUnique
) {
return Result.failWith(
new InvalidReferenceFieldError(
`The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
),
);
}
return Result.ok();
}
export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> {
constructor(readonly reason: string) {
super("InvalidReferenceField");
}
}

View File

@ -1,18 +0,0 @@
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
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;
}

View File

@ -1,19 +0,0 @@
import { TaggedVariant, VariantTag } from "@fabric/core";
import { BaseField } from "./base-field.js";
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;
}

View File

@ -1,5 +0,0 @@
export * from "./fields/index.js";
export * from "./model-schema.js";
export * from "./model.js";
export * from "./query/index.js";
export * from "./state-store.js";

View File

@ -1,7 +0,0 @@
import { Model } from "./model.js";
export type ModelSchema = Record<string, Model>;
export type ModelSchemaFromModels<TModels extends Model> = {
[K in TModels["name"]]: Extract<TModels, { name: K }>;
};

View File

@ -1,24 +0,0 @@
import { describe, expectTypeOf, it } from "vitest";
import { UUID } from "../types/uuid.js";
import { Field } from "./fields/index.js";
import { defineModel, ModelToType } from "./model.js";
describe("CreateModel", () => {
it("should create a model and it's interface type", () => {
const User = defineModel("User", {
name: Field.string(),
password: Field.string(),
phone: Field.string({ isOptional: true }),
});
type User = ModelToType<typeof User>;
expectTypeOf<User>().toEqualTypeOf<{
id: UUID;
streamId: UUID;
streamVersion: bigint;
name: string;
password: string;
phone: string | null;
}>();
});
});

View File

@ -1,64 +0,0 @@
import { Keyof } from "@fabric/core";
import { FieldToType } from "./fields/field-to-type.js";
import { Field, FieldDefinition } from "./fields/index.js";
export type CustomModelFields = Record<string, FieldDefinition>;
export interface Collection<
TName extends string = string,
TFields extends CustomModelFields = CustomModelFields,
> {
name: TName;
fields: TFields;
}
export const DefaultModelFields = {
id: Field.uuid({ isPrimaryKey: true }),
streamId: Field.uuid({ isIndexed: true }),
streamVersion: Field.integer({
isUnsigned: true,
hasArbitraryPrecision: true,
}),
deletedAt: Field.timestamp({ isOptional: true }),
};
export interface Model<
TName extends string = string,
TFields extends CustomModelFields = CustomModelFields,
> extends Collection<TName, TFields> {
fields: typeof DefaultModelFields & TFields;
}
export function defineModel<
TName extends string,
TFields extends CustomModelFields,
>(name: TName, fields: TFields): Model<TName, TFields> {
return {
name,
fields: { ...DefaultModelFields, ...fields },
} as const;
}
export function defineCollection<
TName extends string,
TFields extends CustomModelFields,
>(name: TName, fields: TFields): Collection<TName, TFields> {
return {
name,
fields,
} as const;
}
export type ModelToType<TModel extends Collection> = {
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
};
export type ModelFieldNames<TModel extends CustomModelFields> = Keyof<
TModel["fields"]
>;
export type ModelAddressableFields<TModel extends Model> = {
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends { isUnique: true }
? K
: never;
}[Keyof<TModel["fields"]>];

Some files were not shown because too many files have changed in this diff Show More