Migrate all async computations to Effects
This commit is contained in:
parent
8e4e91c6d7
commit
cd472e6600
@ -10,8 +10,7 @@
|
|||||||
"packages/fabric/testing",
|
"packages/fabric/testing",
|
||||||
"packages/fabric/validations",
|
"packages/fabric/validations",
|
||||||
"packages/templates/domain",
|
"packages/templates/domain",
|
||||||
"packages/templates/lib",
|
"packages/templates/lib"
|
||||||
"apps/syntropy/domain"
|
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
@ -127,11 +127,15 @@
|
|||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"members": {
|
"members": {
|
||||||
|
"packages/fabric/core": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@quentinadam/decimal@~0.1.6"
|
||||||
|
]
|
||||||
|
},
|
||||||
"packages/fabric/domain": {
|
"packages/fabric/domain": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@fabric/core@*",
|
"jsr:@fabric/core@*",
|
||||||
"jsr:@fabric/validations@*",
|
"jsr:@fabric/validations@*"
|
||||||
"jsr:@quentinadam/decimal@~0.1.6"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packages/fabric/sqlite-store": {
|
"packages/fabric/sqlite-store": {
|
||||||
|
|||||||
@ -97,7 +97,7 @@ describe("Effect", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Effect.flatMap should skip maps when the Result is an error", async () => {
|
test("Effect.flatMap should skip maps when the Result is an error", async () => {
|
||||||
const mockFn = fn() as () => Effect<void, number, UnexpectedError>;
|
const mockFn = fn() as () => Effect<number, UnexpectedError>;
|
||||||
const effect = Effect.failWith(new UnexpectedError("failure")).flatMap(
|
const effect = Effect.failWith(new UnexpectedError("failure")).flatMap(
|
||||||
mockFn,
|
mockFn,
|
||||||
);
|
);
|
||||||
@ -138,4 +138,16 @@ describe("Effect", () => {
|
|||||||
|
|
||||||
expectTypeOf<Deps>().toEqualTypeOf<{ a: number }>();
|
expectTypeOf<Deps>().toEqualTypeOf<{ a: number }>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Effect.seq should run multiple effects in sequence", async () => {
|
||||||
|
const effect = Effect.seq(
|
||||||
|
() => Effect.ok(1),
|
||||||
|
(x) => Effect.ok(x + 1),
|
||||||
|
(x) => Effect.ok(x * 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await effect.run();
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
expect(result.unwrapOrThrow()).toBe(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,19 +5,19 @@ import type { MaybePromise } from "../types/maybe-promise.ts";
|
|||||||
import type { MergeTypes } from "../types/merge-types.ts";
|
import type { MergeTypes } from "../types/merge-types.ts";
|
||||||
|
|
||||||
export class Effect<
|
export class Effect<
|
||||||
TDeps = void,
|
|
||||||
TValue = any,
|
TValue = any,
|
||||||
TError extends TaggedError = never,
|
TError extends TaggedError = never,
|
||||||
|
TDeps = void,
|
||||||
> {
|
> {
|
||||||
static from<TValue, TError extends TaggedError = never, TDeps = void>(
|
static from<TValue, TError extends TaggedError = never, TDeps = void>(
|
||||||
fn: (deps: TDeps) => MaybePromise<Result<TValue, TError>>,
|
fn: (deps: TDeps) => MaybePromise<Result<TValue, TError>>,
|
||||||
): Effect<TDeps, TValue, TError> {
|
): Effect<TValue, TError, TDeps> {
|
||||||
return new Effect(fn);
|
return new Effect(fn);
|
||||||
}
|
}
|
||||||
static tryFrom<TValue, TError extends TaggedError = never, TDeps = void>(
|
static tryFrom<TValue, TError extends TaggedError = never, TDeps = void>(
|
||||||
fn: () => MaybePromise<TValue>,
|
fn: () => MaybePromise<TValue>,
|
||||||
errorMapper: (error: any) => TError,
|
errorMapper: (error: any) => TError,
|
||||||
): Effect<TDeps, TValue, TError> {
|
): Effect<TValue, TError, TDeps> {
|
||||||
return new Effect(
|
return new Effect(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
@ -30,13 +30,15 @@ export class Effect<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ok<TValue>(value: TValue): Effect<void, TValue, never> {
|
static ok(): Effect<void>;
|
||||||
|
static ok<TValue>(value: TValue): Effect<TValue>;
|
||||||
|
static ok<TValue>(value?: TValue): Effect<any> {
|
||||||
return new Effect(() => Result.ok(value));
|
return new Effect(() => Result.ok(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
static failWith<TError extends TaggedError>(
|
static failWith<TError extends TaggedError>(
|
||||||
error: TError,
|
error: TError,
|
||||||
): Effect<void, never, TError> {
|
): Effect<never, TError> {
|
||||||
return new Effect(() => Result.failWith(error));
|
return new Effect(() => Result.failWith(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ export class Effect<
|
|||||||
|
|
||||||
map<TNewValue>(
|
map<TNewValue>(
|
||||||
fn: (value: TValue) => MaybePromise<TNewValue>,
|
fn: (value: TValue) => MaybePromise<TNewValue>,
|
||||||
): Effect<TDeps, TNewValue, TError> {
|
): Effect<TNewValue, TError, TDeps> {
|
||||||
return new Effect(async (deps: TDeps) => {
|
return new Effect(async (deps: TDeps) => {
|
||||||
const result = await this.fn(deps);
|
const result = await this.fn(deps);
|
||||||
if (result.isError()) {
|
if (result.isError()) {
|
||||||
@ -62,8 +64,8 @@ export class Effect<
|
|||||||
flatMap<TNewValue, TNewError extends TaggedError, TNewDeps = void>(
|
flatMap<TNewValue, TNewError extends TaggedError, TNewDeps = void>(
|
||||||
fn: (
|
fn: (
|
||||||
value: TValue,
|
value: TValue,
|
||||||
) => Effect<TNewDeps, TNewValue, TNewError>,
|
) => Effect<TNewValue, TNewError, TNewDeps>,
|
||||||
): Effect<MergeTypes<TDeps, TNewDeps>, TNewValue, TError | TNewError> {
|
): Effect<TNewValue, TError | TNewError, MergeTypes<TDeps, TNewDeps>> {
|
||||||
return new Effect(async (deps: TDeps & TNewDeps) => {
|
return new Effect(async (deps: TDeps & TNewDeps) => {
|
||||||
const result = await this.fn(deps);
|
const result = await this.fn(deps);
|
||||||
if (result.isError()) {
|
if (result.isError()) {
|
||||||
@ -71,16 +73,72 @@ export class Effect<
|
|||||||
}
|
}
|
||||||
return await fn(result.value as TValue).fn(deps);
|
return await fn(result.value as TValue).fn(deps);
|
||||||
}) as Effect<
|
}) as Effect<
|
||||||
MergeTypes<TDeps, TNewDeps>,
|
|
||||||
TNewValue,
|
TNewValue,
|
||||||
TError | TNewError
|
TError | TNewError,
|
||||||
|
MergeTypes<TDeps, TNewDeps>
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(deps: TDeps): Promise<Result<TValue, TError>> {
|
async run(deps: TDeps): Promise<Result<TValue, TError>> {
|
||||||
return await this.fn(deps);
|
return await this.fn(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async runOrThrow(deps: TDeps): Promise<TValue> {
|
||||||
|
return (await this.fn(deps)).unwrapOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async failOrThrow(deps: TDeps): Promise<TError> {
|
||||||
|
return (await this.fn(deps)).unwrapErrorOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
static seq<
|
||||||
|
T1,
|
||||||
|
TE1 extends TaggedError,
|
||||||
|
T2,
|
||||||
|
TE2 extends TaggedError,
|
||||||
|
>(
|
||||||
|
fn1: () => Effect<T1, TE1>,
|
||||||
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
|
): Effect<T2, TE1 | TE2>;
|
||||||
|
static seq<
|
||||||
|
T1,
|
||||||
|
TE1 extends TaggedError,
|
||||||
|
T2,
|
||||||
|
TE2 extends TaggedError,
|
||||||
|
T3,
|
||||||
|
TE3 extends TaggedError,
|
||||||
|
>(
|
||||||
|
fn1: () => Effect<T1, TE1>,
|
||||||
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
|
fn3: (value: T2) => Effect<T3, TE3>,
|
||||||
|
): Effect<T3, TE1 | TE2 | TE3>;
|
||||||
|
static seq<
|
||||||
|
T1,
|
||||||
|
TE1 extends TaggedError,
|
||||||
|
T2,
|
||||||
|
TE2 extends TaggedError,
|
||||||
|
T3,
|
||||||
|
TE3 extends TaggedError,
|
||||||
|
T4,
|
||||||
|
TE4 extends TaggedError,
|
||||||
|
>(
|
||||||
|
fn1: () => Effect<T1, TE1>,
|
||||||
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
|
fn3: (value: T2) => Effect<T3, TE3>,
|
||||||
|
fn4: (value: T3) => Effect<T4, TE4>,
|
||||||
|
): Effect<T4, TE1 | TE2 | TE3 | TE4>;
|
||||||
|
static seq(
|
||||||
|
...fns: ((...args: any[]) => Effect<any, any>)[]
|
||||||
|
): Effect<any, any> {
|
||||||
|
let result = fns[0]!();
|
||||||
|
|
||||||
|
for (let i = 1; i < fns.length; i++) {
|
||||||
|
result = result.flatMap((value) => fns[i]!(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtractEffectDependencies<T> = T extends
|
export type ExtractEffectDependencies<T> = T extends
|
||||||
Effect<infer TDeps, any, any> ? TDeps : never;
|
Effect<any, any, infer TDeps> ? TDeps : never;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type TaggedVariant, VariantTag } from "../variant/index.ts";
|
import { type TaggedVariant, VariantTag } from "../variant/variant.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TaggedError is a tagged variant with an error message.
|
* A TaggedError is a tagged variant with an error message.
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Decimal from "decimal";
|
import Decimal from "decimal";
|
||||||
export * from "./array/index.ts";
|
export * from "./array/index.ts";
|
||||||
|
export * from "./effect/index.ts";
|
||||||
export * from "./error/index.ts";
|
export * from "./error/index.ts";
|
||||||
export * from "./record/index.ts";
|
export * from "./record/index.ts";
|
||||||
export * from "./result/index.ts";
|
export * from "./result/index.ts";
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
// deno-lint-ignore-file no-namespace no-explicit-any no-async-promise-executor
|
|
||||||
import { UnexpectedError } from "@fabric/core";
|
|
||||||
import type { TaggedError } from "../error/tagged-error.ts";
|
|
||||||
import type { MaybePromise } from "../types/maybe-promise.ts";
|
|
||||||
import { Result } from "./result.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An AsyncResult represents the result of an asynchronous operation that can
|
|
||||||
* resolve to a value of type `TValue` or an error of type `TError`.
|
|
||||||
*/
|
|
||||||
export class AsyncResult<
|
|
||||||
TValue = any,
|
|
||||||
TError extends TaggedError = never,
|
|
||||||
> {
|
|
||||||
static tryFrom<T, TError extends TaggedError>(
|
|
||||||
fn: () => MaybePromise<T>,
|
|
||||||
errorMapper: (error: any) => TError,
|
|
||||||
): AsyncResult<T, TError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
new Promise<Result<T, TError>>(async (resolve) => {
|
|
||||||
try {
|
|
||||||
const value = await fn();
|
|
||||||
resolve(Result.ok(value));
|
|
||||||
} catch (error) {
|
|
||||||
resolve(Result.failWith(errorMapper(error)));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
fn,
|
|
||||||
(error) => new UnexpectedError(error) as never,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ok(): AsyncResult<void, never>;
|
|
||||||
static ok<T>(value: T): AsyncResult<T, never>;
|
|
||||||
static ok(value?: any) {
|
|
||||||
return new AsyncResult(Promise.resolve(Result.ok(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static succeedWith = AsyncResult.ok;
|
|
||||||
|
|
||||||
static failWith<TError extends TaggedError>(
|
|
||||||
error: TError,
|
|
||||||
): AsyncResult<never, TError> {
|
|
||||||
return new AsyncResult(Promise.resolve(Result.failWith(error)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(private r: Promise<Result<TValue, TError>>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
promise(): Promise<Result<TValue, TError>> {
|
|
||||||
return this.r;
|
|
||||||
}
|
|
||||||
|
|
||||||
async unwrapOrThrow(): Promise<TValue> {
|
|
||||||
return (await this.r).unwrapOrThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
async orThrow(): Promise<void> {
|
|
||||||
return (await this.r).orThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
async unwrapErrorOrThrow(): Promise<TError> {
|
|
||||||
return (await this.r).unwrapErrorOrThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a function over the value of the result.
|
|
||||||
*/
|
|
||||||
map<TMappedValue>(
|
|
||||||
fn: (value: TValue) => TMappedValue,
|
|
||||||
): AsyncResult<TMappedValue, TError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.map(fn)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a function over the value of the result and flattens the result.
|
|
||||||
*/
|
|
||||||
flatMap<TMappedValue, TMappedError extends TaggedError>(
|
|
||||||
fn: (value: TValue) => AsyncResult<TMappedValue, TMappedError>,
|
|
||||||
): AsyncResult<TMappedValue, TError | TMappedError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => {
|
|
||||||
if (result.isError()) {
|
|
||||||
return result as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (fn(result.unwrapOrThrow())).promise();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
): AsyncResult<TMappedValue, TError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.tryMap(fn, errMapper)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a function over the error of the result.
|
|
||||||
*/
|
|
||||||
errorMap<TMappedError extends TaggedError>(
|
|
||||||
fn: (error: TError) => TMappedError,
|
|
||||||
): AsyncResult<TValue, TMappedError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.errorMap(fn)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a function if the result is not an error.
|
|
||||||
* The function does not affect the result.
|
|
||||||
*/
|
|
||||||
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.tap(fn)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert<TResultValue, TResultError extends TaggedError>(
|
|
||||||
fn: (value: TValue) => AsyncResult<TResultValue, TResultError>,
|
|
||||||
): AsyncResult<TValue, TError | TResultError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => {
|
|
||||||
if (result.isError()) {
|
|
||||||
return result as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (fn(result.unwrapOrThrow())).promise();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +1 @@
|
|||||||
export * from "./async-result.ts";
|
|
||||||
export * from "./result.ts";
|
export * from "./result.ts";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AsyncResult } from "@fabric/core";
|
import { Effect } from "@fabric/core";
|
||||||
import { describe, expect, test } from "@fabric/testing";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { UnexpectedError } from "../error/unexpected-error.ts";
|
import { UnexpectedError } from "../error/unexpected-error.ts";
|
||||||
import { Run } from "./run.ts";
|
import { Run } from "./run.ts";
|
||||||
@ -6,21 +6,21 @@ import { Run } from "./run.ts";
|
|||||||
describe("Run", () => {
|
describe("Run", () => {
|
||||||
describe("In Sequence", () => {
|
describe("In Sequence", () => {
|
||||||
test("should pipe the results of multiple async functions", async () => {
|
test("should pipe the results of multiple async functions", async () => {
|
||||||
const result = Run.seq(
|
const result = await Run.seq(
|
||||||
() => AsyncResult.succeedWith(1),
|
() => Effect.ok(1),
|
||||||
(x) => AsyncResult.succeedWith(x + 1),
|
(x) => Effect.ok(x + 1),
|
||||||
(x) => AsyncResult.succeedWith(x * 2),
|
(x) => Effect.ok(x * 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await result.unwrapOrThrow()).toEqual(4);
|
expect(result.unwrapOrThrow()).toEqual(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return the first error if one of the functions fails", async () => {
|
test("should return the first error if one of the functions fails", async () => {
|
||||||
const result = await Run.seq(
|
const result = await Run.seq(
|
||||||
() => AsyncResult.succeedWith(1),
|
() => Effect.ok(1),
|
||||||
() => AsyncResult.failWith(new UnexpectedError()),
|
() => Effect.failWith(new UnexpectedError()),
|
||||||
(x) => AsyncResult.succeedWith(x * 2),
|
(x) => Effect.ok(x * 2),
|
||||||
).promise();
|
);
|
||||||
|
|
||||||
expect(result.isError()).toBe(true);
|
expect(result.isError()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// deno-lint-ignore-file no-namespace no-explicit-any
|
// deno-lint-ignore-file no-namespace no-explicit-any
|
||||||
|
import { Effect } from "../effect/index.ts";
|
||||||
import type { TaggedError } from "../error/tagged-error.ts";
|
import type { TaggedError } from "../error/tagged-error.ts";
|
||||||
import type { AsyncResult } from "../result/async-result.ts";
|
import { Result } from "../result/index.ts";
|
||||||
|
|
||||||
export namespace Run {
|
export namespace Run {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -10,10 +11,9 @@ export namespace Run {
|
|||||||
T2,
|
T2,
|
||||||
TE2 extends TaggedError,
|
TE2 extends TaggedError,
|
||||||
>(
|
>(
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
fn1: () => Effect<T1, TE1>,
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
): AsyncResult<T2, TE1 | TE2>;
|
): Promise<Result<T2, TE1 | TE2>>;
|
||||||
// prettier-ignore
|
|
||||||
export function seq<
|
export function seq<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
@ -22,11 +22,10 @@ export namespace Run {
|
|||||||
T3,
|
T3,
|
||||||
TE3 extends TaggedError,
|
TE3 extends TaggedError,
|
||||||
>(
|
>(
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
fn1: () => Effect<T1, TE1>,
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => Effect<T3, TE3>,
|
||||||
): AsyncResult<T3, TE1 | TE2 | TE3>;
|
): Promise<Result<T3, TE1 | TE2 | TE3>>;
|
||||||
// prettier-ignore
|
|
||||||
export function seq<
|
export function seq<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
@ -37,21 +36,21 @@ export namespace Run {
|
|||||||
T4,
|
T4,
|
||||||
TE4 extends TaggedError,
|
TE4 extends TaggedError,
|
||||||
>(
|
>(
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
fn1: () => Effect<T1, TE1>,
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => Effect<T3, TE3>,
|
||||||
fn4: (value: T3) => AsyncResult<T4, TE4>,
|
fn4: (value: T3) => Effect<T4, TE4>,
|
||||||
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
|
): Promise<Result<T4, TE1 | TE2 | TE3 | TE4>>;
|
||||||
export function seq(
|
export function seq(
|
||||||
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
...fns: ((...args: any[]) => Effect<any, any>)[]
|
||||||
): AsyncResult<any, any> {
|
): Promise<Result<any, any>> {
|
||||||
let result = fns[0]!();
|
let result = fns[0]!();
|
||||||
|
|
||||||
for (let i = 1; i < fns.length; i++) {
|
for (let i = 1; i < fns.length; i++) {
|
||||||
result = result.flatMap((value) => fns[i]!(value));
|
result = result.flatMap((value) => fns[i]!(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -61,8 +60,8 @@ export namespace Run {
|
|||||||
T2,
|
T2,
|
||||||
TE2 extends TaggedError,
|
TE2 extends TaggedError,
|
||||||
>(
|
>(
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
fn1: () => Effect<T1, TE1>,
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
): Promise<T2>;
|
): Promise<T2>;
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seqOrThrow<
|
export function seqOrThrow<
|
||||||
@ -73,14 +72,14 @@ export namespace Run {
|
|||||||
T3,
|
T3,
|
||||||
TE3 extends TaggedError,
|
TE3 extends TaggedError,
|
||||||
>(
|
>(
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
fn1: () => Effect<T1, TE1>,
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => Effect<T2, TE2>,
|
||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => Effect<T3, TE3>,
|
||||||
): Promise<T2>;
|
): Promise<T2>;
|
||||||
export function seqOrThrow(
|
export async function seqOrThrow(
|
||||||
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
...fns: ((...args: any[]) => Effect<any, any>)[]
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const result = (seq as any)(...fns);
|
const result = await (seq as any)(...fns);
|
||||||
|
|
||||||
return result.unwrapOrThrow();
|
return result.unwrapOrThrow();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
AsyncResult,
|
Effect,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
PosixDate,
|
PosixDate,
|
||||||
|
UnexpectedError,
|
||||||
UUID,
|
UUID,
|
||||||
VariantFromTag,
|
VariantFromTag,
|
||||||
VariantTag,
|
VariantTag,
|
||||||
@ -16,11 +17,11 @@ export interface EventStore<TEvents extends DomainEvent> {
|
|||||||
*/
|
*/
|
||||||
append<T extends TEvents>(
|
append<T extends TEvents>(
|
||||||
event: T,
|
event: T,
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError>;
|
): Effect<StoredEvent<T>, StoreQueryError | UnexpectedError>;
|
||||||
|
|
||||||
getEventsFromStream(
|
getEventsFromStream(
|
||||||
streamId: UUID,
|
streamId: UUID,
|
||||||
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
|
): Effect<StoredEvent<TEvents>[], StoreQueryError>;
|
||||||
|
|
||||||
subscribe<TEventKey extends TEvents[VariantTag]>(
|
subscribe<TEventKey extends TEvents[VariantTag]>(
|
||||||
events: TEventKey[],
|
events: TEventKey[],
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import type { TaggedVariant, VariantTag } from "@fabric/core";
|
import type { TaggedVariant, UUID, VariantTag } from "@fabric/core";
|
||||||
import type { UUID } from "../../core/types/uuid.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event is a tagged variant with a payload and a timestamp.
|
* An event is a tagged variant with a payload and a timestamp.
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
type VariantFromTag,
|
type VariantFromTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
||||||
import type { FieldDefinition, FieldToType } from "./index.ts";
|
import { FieldDefinition, FieldToType } from "./fields.ts";
|
||||||
|
|
||||||
export type FieldParsers = {
|
export type FieldParsers = {
|
||||||
[K in FieldDefinition["_tag"]]: FieldParser<
|
[K in FieldDefinition["_tag"]]: FieldParser<
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AsyncResult } from "@fabric/core";
|
import { Effect } from "@fabric/core";
|
||||||
import type { StoreQueryError } from "../errors/query-error.ts";
|
import type { StoreQueryError } from "../errors/query-error.ts";
|
||||||
import type { ModelSchemaFromModels } from "./model-schema.ts";
|
import type { ModelSchemaFromModels } from "./model-schema.ts";
|
||||||
import type { Model, ModelToType } from "./model.ts";
|
import type { Model, ModelToType } from "./model.ts";
|
||||||
@ -15,5 +15,5 @@ export interface WritableStateStore<TModel extends Model>
|
|||||||
insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
collection: T,
|
collection: T,
|
||||||
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
||||||
): AsyncResult<void, StoreQueryError>;
|
): Effect<void, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import {
|
import { Effect, type Keyof, type Optional, TaggedError } from "@fabric/core";
|
||||||
type AsyncResult,
|
|
||||||
type Keyof,
|
|
||||||
type Optional,
|
|
||||||
TaggedError,
|
|
||||||
} from "@fabric/core";
|
|
||||||
import type { StoreQueryError } from "../../errors/query-error.ts";
|
import type { StoreQueryError } from "../../errors/query-error.ts";
|
||||||
import type { FilterOptions } from "./filter-options.ts";
|
import type { FilterOptions } from "./filter-options.ts";
|
||||||
import type { OrderByOptions } from "./order-by-options.ts";
|
import type { OrderByOptions } from "./order-by-options.ts";
|
||||||
@ -14,84 +9,84 @@ export interface StoreQuery<T> {
|
|||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): Effect<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): Effect<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
selectOne(): Effect<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): Effect<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
selectOneOrFail(): Effect<T, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
selectOneOrFail<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
): Effect<Pick<T, K>, StoreQueryError | NotFoundError>;
|
||||||
|
|
||||||
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
|
assertNone(): Effect<void, StoreQueryError | AlreadyExistsError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreSortableQuery<T> {
|
export interface StoreSortableQuery<T> {
|
||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): Effect<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): Effect<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
selectOne(): Effect<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): Effect<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
selectOneOrFail(): Effect<T, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
selectOneOrFail<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
): Effect<Pick<T, K>, StoreQueryError | NotFoundError>;
|
||||||
|
|
||||||
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
|
assertNone(): Effect<void, StoreQueryError | AlreadyExistsError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreLimitableQuery<T> {
|
export interface StoreLimitableQuery<T> {
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): Effect<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): Effect<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
selectOne(): Effect<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): Effect<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
selectOneOrFail(): Effect<T, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
selectOneOrFail<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
): Effect<Pick<T, K>, StoreQueryError | NotFoundError>;
|
||||||
|
|
||||||
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
|
assertNone(): Effect<void, StoreQueryError | AlreadyExistsError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectableQuery<T> {
|
export interface SelectableQuery<T> {
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): Effect<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): Effect<Pick<T, K>[], StoreQueryError>;
|
||||||
|
|
||||||
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
selectOne(): Effect<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): Effect<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
selectOneOrFail(): Effect<T, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
selectOneOrFail<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
): Effect<Pick<T, K>, StoreQueryError | NotFoundError>;
|
||||||
|
|
||||||
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
|
assertNone(): Effect<void, StoreQueryError | AlreadyExistsError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreQueryDefinition<K extends string = string> {
|
export interface StoreQueryDefinition<K extends string = string> {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UUID } from "../../core/types/uuid.ts";
|
import type { UUID } from "@fabric/core";
|
||||||
import type { UUIDGenerator } from "./uuid-generator.ts";
|
import type { UUIDGenerator } from "./uuid-generator.ts";
|
||||||
|
|
||||||
export const UUIDGeneratorMock: UUIDGenerator = {
|
export const UUIDGeneratorMock: UUIDGenerator = {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UUID } from "../../core/types/uuid.ts";
|
import type { UUID } from "@fabric/core";
|
||||||
|
|
||||||
export interface UUIDGenerator {
|
export interface UUIDGenerator {
|
||||||
generate(): UUID;
|
generate(): UUID;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AsyncResult, TaggedError } from "@fabric/core";
|
import type { Effect, TaggedError } from "@fabric/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -9,8 +9,8 @@ export type UseCase<
|
|||||||
TOutput,
|
TOutput,
|
||||||
TErrors extends TaggedError<string>,
|
TErrors extends TaggedError<string>,
|
||||||
> = TPayload extends undefined
|
> = TPayload extends undefined
|
||||||
? (dependencies: TDependencies) => AsyncResult<TOutput, TErrors>
|
? (dependencies: TDependencies) => Effect<TOutput, TErrors>
|
||||||
: (
|
: (
|
||||||
dependencies: TDependencies,
|
dependencies: TDependencies,
|
||||||
payload: TPayload,
|
payload: TPayload,
|
||||||
) => AsyncResult<TOutput, TErrors>;
|
) => Effect<TOutput, TErrors>;
|
||||||
|
|||||||
@ -22,11 +22,11 @@ describe("Event Store", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
store = new SQLiteEventStore(":memory:");
|
store = new SQLiteEventStore(":memory:");
|
||||||
await store.migrate().orThrow();
|
await store.migrate().runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await store.close().orThrow();
|
await store.close().runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should append an event", async () => {
|
test("Should append an event", async () => {
|
||||||
@ -39,9 +39,9 @@ describe("Event Store", () => {
|
|||||||
payload: { name: "test" },
|
payload: { name: "test" },
|
||||||
};
|
};
|
||||||
|
|
||||||
await store.append(userCreated).orThrow();
|
await store.append(userCreated).runOrThrow();
|
||||||
|
|
||||||
const events = await store.getEventsFromStream(newUUID).unwrapOrThrow();
|
const events = await store.getEventsFromStream(newUUID).runOrThrow();
|
||||||
|
|
||||||
expect(events).toHaveLength(1);
|
expect(events).toHaveLength(1);
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe("Event Store", () => {
|
|||||||
|
|
||||||
store.subscribe(["UserCreated"], subscriber);
|
store.subscribe(["UserCreated"], subscriber);
|
||||||
|
|
||||||
await store.append(userCreated).orThrow();
|
await store.append(userCreated).runOrThrow();
|
||||||
|
|
||||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
expect(subscriber).toHaveBeenCalledTimes(1);
|
||||||
expect(subscriber).toHaveBeenCalledWith({
|
expect(subscriber).toHaveBeenCalledWith({
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
AsyncResult,
|
Effect,
|
||||||
JSONUtils,
|
JSONUtils,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
PosixDate,
|
PosixDate,
|
||||||
Run,
|
Result,
|
||||||
|
UnexpectedError,
|
||||||
UUID,
|
UUID,
|
||||||
VariantTag,
|
VariantTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
@ -32,8 +33,8 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
this.db = new SQLiteDatabase(dbPath);
|
this.db = new SQLiteDatabase(dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(): AsyncResult<void, StoreQueryError> {
|
migrate(): Effect<void, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
this.db.init();
|
this.db.init();
|
||||||
this.db.run(
|
this.db.run(
|
||||||
@ -54,8 +55,8 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
|
|
||||||
getEventsFromStream(
|
getEventsFromStream(
|
||||||
streamId: UUID,
|
streamId: UUID,
|
||||||
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError> {
|
): Effect<StoredEvent<TEvents>[], StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const events = this.db.allPrepared(
|
const events = this.db.allPrepared(
|
||||||
`SELECT * FROM events WHERE streamId = $id`,
|
`SELECT * FROM events WHERE streamId = $id`,
|
||||||
@ -79,13 +80,13 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
|
|
||||||
append<T extends TEvents>(
|
append<T extends TEvents>(
|
||||||
event: T,
|
event: T,
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError> {
|
): Effect<StoredEvent<T>, StoreQueryError | UnexpectedError> {
|
||||||
return Run.seq(
|
return Effect.seq(
|
||||||
() => this.getLastVersion(event.streamId),
|
() => this.getLastVersion(event.streamId),
|
||||||
(version) =>
|
(version) =>
|
||||||
AsyncResult.from(() => {
|
Effect.from(() => {
|
||||||
this.streamVersions.set(event.streamId, version + 1n);
|
this.streamVersions.set(event.streamId, version + 1n);
|
||||||
return version;
|
return Result.ok(version);
|
||||||
}),
|
}),
|
||||||
(version) => this.storeEvent(event.streamId, version + 1n, event),
|
(version) => this.storeEvent(event.streamId, version + 1n, event),
|
||||||
(storedEvent) =>
|
(storedEvent) =>
|
||||||
@ -93,15 +94,17 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifySubscribers(event: StoredEvent<TEvents>): AsyncResult<void> {
|
private notifySubscribers(
|
||||||
return AsyncResult.from(async () => {
|
event: StoredEvent<TEvents>,
|
||||||
|
): Effect<void, UnexpectedError> {
|
||||||
|
return Effect.tryFrom(async () => {
|
||||||
const subscribers = this.eventSubscribers.get(event[VariantTag]) || [];
|
const subscribers = this.eventSubscribers.get(event[VariantTag]) || [];
|
||||||
await Promise.all(subscribers.map((subscriber) => subscriber(event)));
|
await Promise.all(subscribers.map((subscriber) => subscriber(event)));
|
||||||
});
|
}, (e) => new UnexpectedError(e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLastVersion(streamId: UUID): AsyncResult<bigint, StoreQueryError> {
|
private getLastVersion(streamId: UUID): Effect<bigint, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const { lastVersion } = this.db.onePrepared(
|
const { lastVersion } = this.db.onePrepared(
|
||||||
`SELECT max(version) as lastVersion FROM events WHERE streamId = $id`,
|
`SELECT max(version) as lastVersion FROM events WHERE streamId = $id`,
|
||||||
@ -132,8 +135,8 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): AsyncResult<void, StoreQueryError> {
|
close(): Effect<void, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => this.db.close(),
|
() => this.db.close(),
|
||||||
(error) => new StoreQueryError(error.message),
|
(error) => new StoreQueryError(error.message),
|
||||||
);
|
);
|
||||||
@ -143,8 +146,8 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
|
|||||||
streamId: UUID,
|
streamId: UUID,
|
||||||
version: bigint,
|
version: bigint,
|
||||||
event: T,
|
event: T,
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError> {
|
): Effect<StoredEvent<T>, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const storedEvent: StoredEvent<T> = {
|
const storedEvent: StoredEvent<T> = {
|
||||||
...event,
|
...event,
|
||||||
|
|||||||
@ -20,26 +20,26 @@ describe("QueryBuilder", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stateStore = new SQLiteStateStore(":memory:", models);
|
stateStore = new SQLiteStateStore(":memory:", models);
|
||||||
await stateStore.migrate().unwrapOrThrow();
|
await stateStore.migrate().runOrThrow();
|
||||||
await stateStore.insertInto("test", {
|
await stateStore.insertInto("test", {
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
name: "test1",
|
name: "test1",
|
||||||
}).unwrapOrThrow();
|
}).runOrThrow();
|
||||||
await stateStore.insertInto("test", {
|
await stateStore.insertInto("test", {
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
name: "test2",
|
name: "test2",
|
||||||
}).unwrapOrThrow();
|
}).runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await stateStore.close().unwrapOrThrow();
|
await stateStore.close().runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("select() after a where() should return valid results", async () => {
|
test("select() after a where() should return valid results", async () => {
|
||||||
const result = await stateStore.from("test").where({
|
const result = await stateStore.from("test").where({
|
||||||
name: isLike("test%"),
|
name: isLike("test%"),
|
||||||
})
|
})
|
||||||
.select().unwrapOrThrow();
|
.select().runOrThrow();
|
||||||
expect(result).toEqual([{
|
expect(result).toEqual([{
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test1",
|
name: "test1",
|
||||||
@ -51,7 +51,7 @@ describe("QueryBuilder", () => {
|
|||||||
|
|
||||||
test("selectOneOrFail() should return a single result", async () => {
|
test("selectOneOrFail() should return a single result", async () => {
|
||||||
const result = await stateStore.from("test").where({ name: "test1" })
|
const result = await stateStore.from("test").where({ name: "test1" })
|
||||||
.selectOneOrFail().unwrapOrThrow();
|
.selectOneOrFail().runOrThrow();
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: "test1",
|
name: "test1",
|
||||||
@ -60,14 +60,14 @@ describe("QueryBuilder", () => {
|
|||||||
|
|
||||||
test("selectOneOrFail() should fail if no results are found", async () => {
|
test("selectOneOrFail() should fail if no results are found", async () => {
|
||||||
const error = await stateStore.from("test").where({ name: "not-found" })
|
const error = await stateStore.from("test").where({ name: "not-found" })
|
||||||
.selectOneOrFail().unwrapErrorOrThrow();
|
.selectOneOrFail().failOrThrow();
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(NotFoundError);
|
expect(error).toBeInstanceOf(NotFoundError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selectOne() should return a single result", async () => {
|
test("selectOne() should return a single result", async () => {
|
||||||
const result = await stateStore.from("test")
|
const result = await stateStore.from("test")
|
||||||
.selectOne().unwrapOrThrow();
|
.selectOne().runOrThrow();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
@ -79,7 +79,7 @@ describe("QueryBuilder", () => {
|
|||||||
const result = await stateStore.from("test").where({
|
const result = await stateStore.from("test").where({
|
||||||
name: "not-found",
|
name: "not-found",
|
||||||
})
|
})
|
||||||
.selectOne().unwrapOrThrow();
|
.selectOne().runOrThrow();
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -87,14 +87,14 @@ describe("QueryBuilder", () => {
|
|||||||
test("assertNone() should succeed if no results are found", async () => {
|
test("assertNone() should succeed if no results are found", async () => {
|
||||||
const result = await stateStore.from("test").where({
|
const result = await stateStore.from("test").where({
|
||||||
name: "not-found",
|
name: "not-found",
|
||||||
}).assertNone().unwrapOrThrow();
|
}).assertNone().runOrThrow();
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("assertNone() should fail if results are found", async () => {
|
test("assertNone() should fail if results are found", async () => {
|
||||||
const error = await stateStore.from("test").where({ name: "test1" })
|
const error = await stateStore.from("test").where({ name: "test1" })
|
||||||
.assertNone().unwrapErrorOrThrow();
|
.assertNone().failOrThrow();
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(AlreadyExistsError);
|
expect(error).toBeInstanceOf(AlreadyExistsError);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import { AsyncResult, Keyof, Optional } from "@fabric/core";
|
import { Effect, Keyof, Optional } from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
AlreadyExistsError,
|
AlreadyExistsError,
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
@ -47,12 +47,12 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): Effect<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
): Effect<Pick<T, K>[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
|
select<K extends Keyof<T>>(keys?: K[]): Effect<any, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const [sql, params] = getSelectStatement(
|
const [sql, params] = getSelectStatement(
|
||||||
this.schema[this.query.from]!,
|
this.schema[this.query.from]!,
|
||||||
@ -71,12 +71,12 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
|
selectOne(): Effect<Optional<T>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): Effect<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
|
selectOne<K extends Keyof<T>>(keys?: K[]): Effect<any, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
async () => {
|
async () => {
|
||||||
const [stmt, params] = getSelectStatement(
|
const [stmt, params] = getSelectStatement(
|
||||||
this.schema[this.query.from]!,
|
this.schema[this.query.from]!,
|
||||||
@ -96,14 +96,14 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
selectOneOrFail(): Effect<T, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Extract<keyof T, string>>(
|
selectOneOrFail<K extends Extract<keyof T, string>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
): Effect<Pick<T, K>, StoreQueryError | NotFoundError>;
|
||||||
selectOneOrFail<K extends Extract<keyof T, string>>(
|
selectOneOrFail<K extends Extract<keyof T, string>>(
|
||||||
keys?: K[],
|
keys?: K[],
|
||||||
): AsyncResult<any, StoreQueryError | NotFoundError> {
|
): Effect<any, StoreQueryError | NotFoundError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
async () => {
|
async () => {
|
||||||
const [stmt, params] = getSelectStatement(
|
const [stmt, params] = getSelectStatement(
|
||||||
this.schema[this.query.from]!,
|
this.schema[this.query.from]!,
|
||||||
@ -122,14 +122,14 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
(err) => new StoreQueryError(err.message),
|
(err) => new StoreQueryError(err.message),
|
||||||
).flatMap((result) => {
|
).flatMap((result) => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return AsyncResult.failWith(new NotFoundError());
|
return Effect.failWith(new NotFoundError());
|
||||||
}
|
}
|
||||||
return AsyncResult.ok(result);
|
return Effect.ok(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError> {
|
assertNone(): Effect<void, StoreQueryError | AlreadyExistsError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
async () => {
|
async () => {
|
||||||
const [stmt, params] = getSelectStatement(
|
const [stmt, params] = getSelectStatement(
|
||||||
this.schema[this.query.from]!,
|
this.schema[this.query.from]!,
|
||||||
@ -146,9 +146,9 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
(err) => new StoreQueryError(err.message),
|
(err) => new StoreQueryError(err.message),
|
||||||
).flatMap((result) => {
|
).flatMap((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
return AsyncResult.failWith(new AlreadyExistsError());
|
return Effect.failWith(new AlreadyExistsError());
|
||||||
}
|
}
|
||||||
return AsyncResult.ok();
|
return Effect.ok();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Run } from "@fabric/core";
|
import { Effect, Run } from "@fabric/core";
|
||||||
import { Field, isLike, Model } from "@fabric/domain";
|
import { Field, isLike, Model } from "@fabric/domain";
|
||||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing";
|
import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing";
|
||||||
@ -20,11 +20,11 @@ describe("State Store", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
store = new SQLiteStateStore(":memory:", models);
|
store = new SQLiteStateStore(":memory:", models);
|
||||||
await store.migrate().orThrow();
|
await store.migrate().runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await store.close().orThrow();
|
await store.close().runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should insert a record", async () => {
|
test("should insert a record", async () => {
|
||||||
@ -33,7 +33,7 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
name: "test",
|
name: "test",
|
||||||
}).orThrow();
|
}).runOrThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should select all records", async () => {
|
test("should select all records", async () => {
|
||||||
@ -42,9 +42,9 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
}).orThrow();
|
}).runOrThrow();
|
||||||
|
|
||||||
const result = await store.from("users").select().unwrapOrThrow();
|
const result = await store.from("users").select().runOrThrow();
|
||||||
|
|
||||||
// expectTypeOf(result).toEqualTypeOf<
|
// expectTypeOf(result).toEqualTypeOf<
|
||||||
// {
|
// {
|
||||||
@ -64,7 +64,7 @@ describe("State Store", () => {
|
|||||||
test("should select records with a filter", async () => {
|
test("should select records with a filter", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await Run.seqOrThrow(
|
await Effect.seq(
|
||||||
() =>
|
() =>
|
||||||
store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
@ -80,14 +80,14 @@ describe("State Store", () => {
|
|||||||
name: "anotherName2",
|
name: "anotherName2",
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
}),
|
}),
|
||||||
);
|
).runOrThrow();
|
||||||
|
|
||||||
const result = await store
|
const result = await store
|
||||||
.from("users")
|
.from("users")
|
||||||
.where({
|
.where({
|
||||||
name: isLike("te%"),
|
name: isLike("te%"),
|
||||||
})
|
})
|
||||||
.select().unwrapOrThrow();
|
.select().runOrThrow();
|
||||||
|
|
||||||
// expectTypeOf(result).toEqualTypeOf<
|
// expectTypeOf(result).toEqualTypeOf<
|
||||||
// {
|
// {
|
||||||
@ -107,17 +107,20 @@ describe("State Store", () => {
|
|||||||
test("should update a record", async () => {
|
test("should update a record", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Effect.seq(
|
||||||
name: "test",
|
() =>
|
||||||
id: newUUID,
|
store.insertInto("users", {
|
||||||
}).orThrow();
|
name: "test",
|
||||||
|
id: newUUID,
|
||||||
await store.update("users", newUUID, {
|
}),
|
||||||
name: "updated",
|
() =>
|
||||||
}).orThrow();
|
store.update("users", newUUID, {
|
||||||
|
name: "updated",
|
||||||
|
}),
|
||||||
|
).runOrThrow();
|
||||||
|
|
||||||
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
||||||
.unwrapOrThrow();
|
.runOrThrow();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
@ -128,34 +131,37 @@ describe("State Store", () => {
|
|||||||
test("should delete a record", async () => {
|
test("should delete a record", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Effect.seq(
|
||||||
name: "test",
|
() =>
|
||||||
id: newUUID,
|
store.insertInto("users", {
|
||||||
}).orThrow();
|
name: "test",
|
||||||
|
id: newUUID,
|
||||||
await store.delete("users", newUUID).orThrow();
|
}),
|
||||||
|
() => store.delete("users", newUUID),
|
||||||
|
).runOrThrow();
|
||||||
|
|
||||||
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
||||||
.unwrapOrThrow();
|
.runOrThrow();
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
//test for inserting into a collection with a reference
|
|
||||||
|
|
||||||
test("should insert a record with a reference", async () => {
|
test("should insert a record with a reference", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
const ownerUUID = UUIDGeneratorMock.generate();
|
const ownerUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Run.seqOrThrow(
|
||||||
id: ownerUUID,
|
() =>
|
||||||
name: "test",
|
store.insertInto("users", {
|
||||||
}).orThrow();
|
id: ownerUUID,
|
||||||
|
name: "test",
|
||||||
await store.insertInto("demo", {
|
}),
|
||||||
id: newUUID,
|
() =>
|
||||||
value: 1.0,
|
store.insertInto("demo", {
|
||||||
owner: ownerUUID,
|
id: newUUID,
|
||||||
}).orThrow();
|
value: 1.0,
|
||||||
|
owner: ownerUUID,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AsyncResult, UnexpectedError, UUID } from "@fabric/core";
|
import { Effect, UnexpectedError, UUID } from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
Model,
|
Model,
|
||||||
ModelSchemaFromModels,
|
ModelSchemaFromModels,
|
||||||
@ -37,10 +37,10 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
collection: T,
|
collection: T,
|
||||||
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
||||||
): AsyncResult<void, StoreQueryError> {
|
): Effect<void, StoreQueryError> {
|
||||||
const model = this.schema[collection];
|
const model = this.schema[collection];
|
||||||
|
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
this.db.runPrepared(
|
this.db.runPrepared(
|
||||||
`INSERT INTO ${model.name} (${
|
`INSERT INTO ${model.name} (${
|
||||||
@ -67,10 +67,10 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
collection: T,
|
collection: T,
|
||||||
id: UUID,
|
id: UUID,
|
||||||
record: Partial<ModelToType<ModelSchemaFromModels<TModel>[T]>>,
|
record: Partial<ModelToType<ModelSchemaFromModels<TModel>[T]>>,
|
||||||
): AsyncResult<void, StoreQueryError> {
|
): Effect<void, StoreQueryError> {
|
||||||
const model = this.schema[collection];
|
const model = this.schema[collection];
|
||||||
|
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const params = recordToSQLParamRecord(model, {
|
const params = recordToSQLParamRecord(model, {
|
||||||
...record,
|
...record,
|
||||||
@ -92,10 +92,10 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
delete<T extends keyof ModelSchemaFromModels<TModel>>(
|
delete<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
collection: T,
|
collection: T,
|
||||||
id: UUID,
|
id: UUID,
|
||||||
): AsyncResult<void, StoreQueryError> {
|
): Effect<void, StoreQueryError> {
|
||||||
const model = this.schema[collection];
|
const model = this.schema[collection];
|
||||||
|
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
this.db.runPrepared(
|
this.db.runPrepared(
|
||||||
`DELETE FROM ${model.name} WHERE id = ${keyToParamKey("id")}`,
|
`DELETE FROM ${model.name} WHERE id = ${keyToParamKey("id")}`,
|
||||||
@ -108,8 +108,8 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(): AsyncResult<void, StoreQueryError> {
|
migrate(): Effect<void, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return Effect.tryFrom(
|
||||||
async () => {
|
async () => {
|
||||||
this.db.init();
|
this.db.init();
|
||||||
await this.db.withTransaction(() => {
|
await this.db.withTransaction(() => {
|
||||||
@ -124,7 +124,10 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(): AsyncResult<void, UnexpectedError> {
|
close(): Effect<void, UnexpectedError> {
|
||||||
return AsyncResult.from(() => this.db.close());
|
return Effect.tryFrom(
|
||||||
|
() => this.db.close(),
|
||||||
|
(e) => new UnexpectedError(e.message),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user