Merge pull request 'Feature: Basic Events, Models and Projections' (#2) from feat-base-projections into main
Reviewed-on: #2
This commit is contained in:
commit
bfb471b166
12
package.json
12
package.json
@ -9,24 +9,24 @@
|
|||||||
"apps/**/*"
|
"apps/**/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.10.0",
|
"@eslint/js": "^9.12.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.10.0",
|
"eslint": "^9.12.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.2",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.6.0",
|
"typescript-eslint": "^8.8.1",
|
||||||
"zx": "^8.1.7"
|
"zx": "^8.1.9"
|
||||||
},
|
},
|
||||||
"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-dev run build",
|
"build": "yarn workspaces foreach -vvpA --topological run build",
|
||||||
"add-package": "tsx ./scripts/add-package.ts",
|
"add-package": "tsx ./scripts/add-package.ts",
|
||||||
"postinstall": "husky"
|
"postinstall": "husky"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
# @ulthar/fabric-core
|
# @fabric/core
|
||||||
|
|||||||
@ -1,31 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@ulthar/fabric-core",
|
"name": "@fabric/core",
|
||||||
|
"private": true,
|
||||||
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"main": "dist/index.js",
|
"types": "./dist/index.d.ts",
|
||||||
"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": {
|
||||||
"@types/validator": "^13.12.2",
|
"@vitest/coverage-v8": "^2.1.2",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.2"
|
||||||
},
|
},
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { ImageMimeType } from "../../../files/mime-type.js";
|
|
||||||
import { DomainFile } from "./domain-file.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an image file.
|
|
||||||
*/
|
|
||||||
export interface ImageFile extends DomainFile {
|
|
||||||
mimeType: ImageMimeType;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./domain-file.js";
|
|
||||||
export * from "./image-file.js";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./entity.js";
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
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";
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./create-model.js";
|
|
||||||
export * from "./fields/index.js";
|
|
||||||
export * from "./model-to-type.js";
|
|
||||||
export * from "./relations/index.js";
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
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]>;
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { OneToOneRelation } from "./one-to-one.js";
|
|
||||||
|
|
||||||
export type RelationDefinition = OneToOneRelation<string, string>;
|
|
||||||
|
|
||||||
export namespace Relations {}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./email.js";
|
|
||||||
export * from "./semver.js";
|
|
||||||
export * from "./uuid.js";
|
|
||||||
@ -3,14 +3,15 @@ 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 class TaggedError<Tag extends string>
|
export abstract class TaggedError<Tag extends string = string>
|
||||||
extends Error
|
extends Error
|
||||||
implements TaggedVariant<Tag>
|
implements TaggedVariant<Tag>
|
||||||
{
|
{
|
||||||
readonly [VariantTag]: Tag;
|
readonly [VariantTag]: Tag;
|
||||||
|
|
||||||
constructor(tag: Tag) {
|
constructor(tag: Tag, message?: string) {
|
||||||
super();
|
super(message);
|
||||||
this[VariantTag] = tag;
|
this[VariantTag] = tag;
|
||||||
|
this.name = tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,7 @@ 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(readonly context: Record<string, unknown> = {}) {
|
constructor(message?: string) {
|
||||||
super("UnexpectedError");
|
super("UnexpectedError", message);
|
||||||
this.message = "An unexpected error occurred";
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `UnexpectedError: ${this.message}\n${JSON.stringify(this.context, null, 2)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 "./storage/index.js";
|
export * from "./run/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";
|
||||||
|
|||||||
@ -1,11 +1,33 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un AsyncResult representa el resultado de una operación asíncrona que puede
|
* An AsyncResult represents the result of an asynchronous operation that can
|
||||||
* resolver en un valor de tipo `TValue` o en un error de tipo `TError`.
|
* resolve to a value of type `TValue` or an error of type `TError`.
|
||||||
*/
|
*/
|
||||||
export type AsyncResult<
|
export type AsyncResult<
|
||||||
TValue,
|
TValue = any,
|
||||||
TError extends TaggedError<string> = never,
|
TError extends TaggedError = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
57
packages/fabric/core/src/result/result.spec.ts
Normal file
57
packages/fabric/core/src/result/result.spec.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,9 +1,151 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un Result representa el resultado de una operación
|
* A Result represents the outcome of an operation
|
||||||
* que puede ser un valor de tipo `TValue` o un error `TError`.
|
* that can be either a value of type `TValue` or an error `TError`.
|
||||||
*/
|
*/
|
||||||
export type Result<TValue, TError extends TaggedError<string>> =
|
export class Result<TValue, TError extends TaggedError = never> {
|
||||||
| TValue
|
static succeedWith<T>(value: T): Result<T, never> {
|
||||||
| TError;
|
return new Result<T, never>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
packages/fabric/core/src/run/index.ts
Normal file
1
packages/fabric/core/src/run/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./run.js";
|
||||||
28
packages/fabric/core/src/run/run.spec.ts
Normal file
28
packages/fabric/core/src/run/run.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
87
packages/fabric/core/src/run/run.ts
Normal file
87
packages/fabric/core/src/run/run.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
/* 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
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";
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
/* 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>>>;
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
/* 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>;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,38 @@
|
|||||||
|
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) {}
|
constructor(public readonly timestamp: number = Date.now()) {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export type EnumToValues<T extends Record<string, string>> = T[keyof T];
|
export type EnumToType<T extends Record<string, string>> = T[keyof T];
|
||||||
|
|||||||
@ -3,3 +3,4 @@ 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";
|
||||||
|
|||||||
1
packages/fabric/core/src/types/record.ts
Normal file
1
packages/fabric/core/src/types/record.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type EmptyRecord = Record<string, never>;
|
||||||
8
packages/fabric/core/src/utils/ensure-value.ts
Normal file
8
packages/fabric/core/src/utils/ensure-value.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { UnexpectedError } from "../error/unexpected-error.js";
|
||||||
|
|
||||||
|
export function ensureValue<T>(value?: T): T {
|
||||||
|
if (!value) {
|
||||||
|
throw new UnexpectedError("Value is undefined");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
1
packages/fabric/core/src/utils/index.ts
Normal file
1
packages/fabric/core/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ensure-value.js";
|
||||||
@ -1,17 +1,21 @@
|
|||||||
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>> = {
|
export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = {
|
||||||
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>>;
|
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function match<const TVariant extends TaggedVariant<string>>(
|
export function match<const TVariant extends TaggedVariant<string>>(
|
||||||
v: TVariant,
|
v: TVariant,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
case<const TMatcher extends VariantMatcher<TVariant>>(
|
case<
|
||||||
cases: TMatcher,
|
const TReturnType,
|
||||||
): ReturnType<TMatcher[TVariant[VariantTag]]> {
|
const TMatcher extends VariantMatcher<
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
41
packages/fabric/core/src/variant/variant.spec.ts
Normal file
41
packages/fabric/core/src/variant/variant.spec.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,5 +7,17 @@ export interface TaggedVariant<TTag extends string> {
|
|||||||
|
|
||||||
export type VariantFromTag<
|
export type VariantFromTag<
|
||||||
TVariant extends TaggedVariant<string>,
|
TVariant extends TaggedVariant<string>,
|
||||||
TTag extends TVariant[typeof VariantTag],
|
TTag extends TVariant[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
1
packages/fabric/domain/README.md
Normal file
1
packages/fabric/domain/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# model
|
||||||
30
packages/fabric/domain/package.json
Normal file
30
packages/fabric/domain/package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { TaggedError } from "../../error/tagged-error.js";
|
import { TaggedError } from "@fabric/core";
|
||||||
|
|
||||||
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> {
|
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> {
|
||||||
context: { key: string; dep: string };
|
context: { key: string; dep: string };
|
||||||
7
packages/fabric/domain/src/errors/query-error.ts
Normal file
7
packages/fabric/domain/src/errors/query-error.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { TaggedError } from "@fabric/core";
|
||||||
|
|
||||||
|
export class StoreQueryError extends TaggedError<"StoreQueryError"> {
|
||||||
|
constructor(public message: string) {
|
||||||
|
super("StoreQueryError", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
packages/fabric/domain/src/events/event-store.ts
Normal file
42
packages/fabric/domain/src/events/event-store.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
18
packages/fabric/domain/src/events/event.ts
Normal file
18
packages/fabric/domain/src/events/event.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* 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 }>;
|
||||||
3
packages/fabric/domain/src/events/index.ts
Normal file
3
packages/fabric/domain/src/events/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./event-store.js";
|
||||||
|
export * from "./event.js";
|
||||||
|
export * from "./stored-event.js";
|
||||||
10
packages/fabric/domain/src/events/stored-event.ts
Normal file
10
packages/fabric/domain/src/events/stored-event.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
9
packages/fabric/domain/src/files/image-file.ts
Normal file
9
packages/fabric/domain/src/files/image-file.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ImageMimeType } from "./mime-type.js";
|
||||||
|
import { StoredFile } from "./stored-file.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an image file.
|
||||||
|
*/
|
||||||
|
export interface ImageFile extends StoredFile {
|
||||||
|
mimeType: ImageMimeType;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Base64String } from "../domain/types/base-64.js";
|
import { Base64String } from "../types/base-64.js";
|
||||||
import { BaseFile } from "./base-file.js";
|
import { BaseFile } from "./base-file.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1,8 +1,10 @@
|
|||||||
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 "./uploaded-file.js";
|
export * from "./stored-file.js";
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { TaggedError } from "../error/tagged-error.js";
|
import { TaggedError } from "@fabric/core";
|
||||||
|
|
||||||
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
|
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -1,8 +1,5 @@
|
|||||||
import validator from "validator";
|
import { isRecord } from "@fabric/core";
|
||||||
import { isRecord } from "../record/is-record.js";
|
import { InMemoryFile } from "./in-memory-file.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 {
|
||||||
@ -10,10 +7,8 @@ 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 &&
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { DomainFile } from "../domain/entity/files/domain-file.js";
|
import { StoredFile } from "./stored-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 DomainFile {
|
export interface MediaFile extends StoredFile {
|
||||||
mimeType: `image/${string}` | `video/${string}` | `audio/${string}`;
|
mimeType: `image/${string}` | `video/${string}` | `audio/${string}`;
|
||||||
}
|
}
|
||||||
9
packages/fabric/domain/src/files/stored-file.ts
Normal file
9
packages/fabric/domain/src/files/stored-file.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
9
packages/fabric/domain/src/index.ts
Normal file
9
packages/fabric/domain/src/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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";
|
||||||
1
packages/fabric/domain/src/mocks.ts
Normal file
1
packages/fabric/domain/src/mocks.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./services/mocks.js";
|
||||||
21
packages/fabric/domain/src/models/fields/decimal.ts
Normal file
21
packages/fabric/domain/src/models/fields/decimal.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
20
packages/fabric/domain/src/models/fields/embedded.ts
Normal file
20
packages/fabric/domain/src/models/fields/embedded.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
37
packages/fabric/domain/src/models/fields/field-to-type.ts
Normal file
37
packages/fabric/domain/src/models/fields/field-to-type.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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;
|
||||||
18
packages/fabric/domain/src/models/fields/float.ts
Normal file
18
packages/fabric/domain/src/models/fields/float.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
32
packages/fabric/domain/src/models/fields/index.ts
Normal file
32
packages/fabric/domain/src/models/fields/index.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
20
packages/fabric/domain/src/models/fields/integer.ts
Normal file
20
packages/fabric/domain/src/models/fields/integer.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
65
packages/fabric/domain/src/models/fields/reference-field.ts
Normal file
65
packages/fabric/domain/src/models/fields/reference-field.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
|
import { TaggedVariant, VariantTag } from "@fabric/core";
|
||||||
import { BaseField } from "./base-field.js";
|
import { BaseField } from "./base-field.js";
|
||||||
|
|
||||||
export interface StringFieldOptions extends BaseField {
|
export interface StringFieldOptions extends BaseField {
|
||||||
18
packages/fabric/domain/src/models/fields/timestamp.ts
Normal file
18
packages/fabric/domain/src/models/fields/timestamp.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
19
packages/fabric/domain/src/models/fields/uuid-field.ts
Normal file
19
packages/fabric/domain/src/models/fields/uuid-field.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
5
packages/fabric/domain/src/models/index.ts
Normal file
5
packages/fabric/domain/src/models/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./fields/index.js";
|
||||||
|
export * from "./model-schema.js";
|
||||||
|
export * from "./model.js";
|
||||||
|
export * from "./query/index.js";
|
||||||
|
export * from "./state-store.js";
|
||||||
7
packages/fabric/domain/src/models/model-schema.ts
Normal file
7
packages/fabric/domain/src/models/model-schema.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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 }>;
|
||||||
|
};
|
||||||
24
packages/fabric/domain/src/models/model.spec.ts
Normal file
24
packages/fabric/domain/src/models/model.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
}>();
|
||||||
|
});
|
||||||
|
});
|
||||||
64
packages/fabric/domain/src/models/model.ts
Normal file
64
packages/fabric/domain/src/models/model.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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"]>];
|
||||||
24
packages/fabric/domain/src/models/query/aggregate-options.ts
Normal file
24
packages/fabric/domain/src/models/query/aggregate-options.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { Keyof, TaggedVariant } from "@fabric/core";
|
||||||
|
|
||||||
|
export type AggregateOptions<T = any> = Record<string, AggregateFn<T>>;
|
||||||
|
|
||||||
|
export type AggregateFn<T> = CountAggregate<T>;
|
||||||
|
|
||||||
|
export interface CountAggregate<T> extends TaggedVariant<"AggregateCount"> {
|
||||||
|
field: Keyof<T>;
|
||||||
|
}
|
||||||
|
export interface SumAggregate<T> extends TaggedVariant<"AggregateSum"> {
|
||||||
|
field: Keyof<T>;
|
||||||
|
}
|
||||||
|
export interface AvgAggregate<T> extends TaggedVariant<"AggregateAvg"> {
|
||||||
|
field: Keyof<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MinAggregate<T> extends TaggedVariant<"AggregateMin"> {
|
||||||
|
field: Keyof<T>;
|
||||||
|
}
|
||||||
|
export interface MaxAggregate<T> extends TaggedVariant<"AggregateMax"> {
|
||||||
|
field: Keyof<T>;
|
||||||
|
}
|
||||||
@ -4,19 +4,21 @@ export type FilterOptions<T = any> =
|
|||||||
| SingleFilterOption<T>
|
| SingleFilterOption<T>
|
||||||
| MultiFilterOption<T>;
|
| MultiFilterOption<T>;
|
||||||
|
|
||||||
export type SingleFilterOption<T = any> = {
|
export type FilterValue<T = any, K extends keyof T = keyof T> =
|
||||||
[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 = Symbol("$type");
|
export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type";
|
||||||
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("$value");
|
export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value";
|
||||||
export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("$operator");
|
export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator";
|
||||||
|
|
||||||
export type LikeFilterOption<T> = T extends string
|
export type LikeFilterOption<T> = T extends string
|
||||||
? {
|
? {
|
||||||
@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { AsyncResult } from "../../result/async-result.js";
|
import { AsyncResult, Keyof, Optional } from "@fabric/core";
|
||||||
import { Keyof } from "../../types/keyof.js";
|
import { StoreQueryError } from "../../errors/query-error.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";
|
||||||
|
|
||||||
@ -15,10 +14,10 @@ export interface StoreQuery<T> {
|
|||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreSortableQuery<T> {
|
export interface StoreSortableQuery<T> {
|
||||||
@ -30,10 +29,10 @@ export interface StoreSortableQuery<T> {
|
|||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreLimitableQuery<T> {
|
export interface StoreLimitableQuery<T> {
|
||||||
@ -44,10 +43,10 @@ export interface StoreLimitableQuery<T> {
|
|||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectableQuery<T> {
|
export interface SelectableQuery<T> {
|
||||||
@ -56,10 +55,10 @@ export interface SelectableQuery<T> {
|
|||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryDefinition<K extends string = string> {
|
export interface QueryDefinition<K extends string = string> {
|
||||||
19
packages/fabric/domain/src/models/state-store.ts
Normal file
19
packages/fabric/domain/src/models/state-store.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { AsyncResult } from "@fabric/core";
|
||||||
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
|
import { ModelSchemaFromModels } from "./model-schema.js";
|
||||||
|
import { Model, ModelToType } from "./model.js";
|
||||||
|
import { StoreQuery } from "./query/query.js";
|
||||||
|
|
||||||
|
export interface ReadonlyStateStore<TModel extends Model> {
|
||||||
|
from<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
|
collection: T,
|
||||||
|
): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WritableStateStore<TModel extends Model>
|
||||||
|
extends ReadonlyStateStore<TModel> {
|
||||||
|
insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
|
collection: T,
|
||||||
|
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
||||||
|
): AsyncResult<void, StoreQueryError>;
|
||||||
|
}
|
||||||
13
packages/fabric/domain/src/projections/projection.ts
Normal file
13
packages/fabric/domain/src/projections/projection.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { VariantTag } from "@fabric/core";
|
||||||
|
import { Event } from "../events/event.js";
|
||||||
|
import { StoredEvent } from "../events/stored-event.js";
|
||||||
|
import { Model, ModelToType } from "../models/model.js";
|
||||||
|
|
||||||
|
export interface Projection<TModel extends Model, TEvents extends Event> {
|
||||||
|
model: TModel;
|
||||||
|
events: TEvents[VariantTag][];
|
||||||
|
projection: (
|
||||||
|
event: StoredEvent<TEvents>,
|
||||||
|
model?: ModelToType<TModel>,
|
||||||
|
) => ModelToType<TModel>;
|
||||||
|
}
|
||||||
1
packages/fabric/domain/src/services/index.ts
Normal file
1
packages/fabric/domain/src/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./uuid-generator.js";
|
||||||
1
packages/fabric/domain/src/services/mocks.ts
Normal file
1
packages/fabric/domain/src/services/mocks.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./uuid-generator.mock.js";
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { UUID } from "../types/uuid.js";
|
||||||
|
import { UUIDGenerator } from "./uuid-generator.js";
|
||||||
|
|
||||||
|
export const UUIDGeneratorMock: UUIDGenerator = {
|
||||||
|
generate(): UUID {
|
||||||
|
return crypto.randomUUID() as UUID;
|
||||||
|
},
|
||||||
|
};
|
||||||
5
packages/fabric/domain/src/services/uuid-generator.ts
Normal file
5
packages/fabric/domain/src/services/uuid-generator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { UUID } from "../types/uuid.js";
|
||||||
|
|
||||||
|
export interface UUIDGenerator {
|
||||||
|
generate(): UUID;
|
||||||
|
}
|
||||||
1
packages/fabric/domain/src/types/decimal.ts
Normal file
1
packages/fabric/domain/src/types/decimal.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Decimal } from "decimal.js";
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user