Migrate all async computations to Effects

This commit is contained in:
Pablo Baleztena 2024-11-13 17:08:55 -03:00
parent 8e4e91c6d7
commit cd472e6600
24 changed files with 282 additions and 349 deletions

View File

@ -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,

View File

@ -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": {

View File

@ -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);
});
}); });

View File

@ -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;

View File

@ -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.

View File

@ -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";

View File

@ -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();
}),
);
}
}

View File

@ -1,2 +1 @@
export * from "./async-result.ts";
export * from "./result.ts"; export * from "./result.ts";

View File

@ -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);
}); });

View File

@ -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();
} }

View File

@ -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[],

View File

@ -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.

View File

@ -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<

View File

@ -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>;
} }

View File

@ -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> {

View File

@ -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 = {

View File

@ -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;

View File

@ -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>;

View File

@ -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({

View File

@ -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,

View File

@ -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);
}); });

View File

@ -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();
}); });
} }
} }

View File

@ -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,
}),
);
}); });
}); });

View File

@ -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),
);
} }
} }