diff --git a/deno.json b/deno.json index f19c5b2..aada6af 100644 --- a/deno.json +++ b/deno.json @@ -10,8 +10,7 @@ "packages/fabric/testing", "packages/fabric/validations", "packages/templates/domain", - "packages/templates/lib", - "apps/syntropy/domain" + "packages/templates/lib" ], "compilerOptions": { "strict": true, diff --git a/deno.lock b/deno.lock index bfd8e9a..fc75e4f 100644 --- a/deno.lock +++ b/deno.lock @@ -127,11 +127,15 @@ }, "workspace": { "members": { + "packages/fabric/core": { + "dependencies": [ + "jsr:@quentinadam/decimal@~0.1.6" + ] + }, "packages/fabric/domain": { "dependencies": [ "jsr:@fabric/core@*", - "jsr:@fabric/validations@*", - "jsr:@quentinadam/decimal@~0.1.6" + "jsr:@fabric/validations@*" ] }, "packages/fabric/sqlite-store": { diff --git a/packages/fabric/core/effect/effect.test.ts b/packages/fabric/core/effect/effect.test.ts index c66a3ef..ed18825 100644 --- a/packages/fabric/core/effect/effect.test.ts +++ b/packages/fabric/core/effect/effect.test.ts @@ -97,7 +97,7 @@ describe("Effect", () => { }); test("Effect.flatMap should skip maps when the Result is an error", async () => { - const mockFn = fn() as () => Effect; + const mockFn = fn() as () => Effect; const effect = Effect.failWith(new UnexpectedError("failure")).flatMap( mockFn, ); @@ -138,4 +138,16 @@ describe("Effect", () => { expectTypeOf().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); + }); }); diff --git a/packages/fabric/core/effect/effect.ts b/packages/fabric/core/effect/effect.ts index 72d031b..9c9c9ad 100644 --- a/packages/fabric/core/effect/effect.ts +++ b/packages/fabric/core/effect/effect.ts @@ -5,19 +5,19 @@ import type { MaybePromise } from "../types/maybe-promise.ts"; import type { MergeTypes } from "../types/merge-types.ts"; export class Effect< - TDeps = void, TValue = any, TError extends TaggedError = never, + TDeps = void, > { static from( fn: (deps: TDeps) => MaybePromise>, - ): Effect { + ): Effect { return new Effect(fn); } static tryFrom( fn: () => MaybePromise, errorMapper: (error: any) => TError, - ): Effect { + ): Effect { return new Effect( async () => { try { @@ -30,13 +30,15 @@ export class Effect< ); } - static ok(value: TValue): Effect { + static ok(): Effect; + static ok(value: TValue): Effect; + static ok(value?: TValue): Effect { return new Effect(() => Result.ok(value)); } static failWith( error: TError, - ): Effect { + ): Effect { return new Effect(() => Result.failWith(error)); } @@ -49,7 +51,7 @@ export class Effect< map( fn: (value: TValue) => MaybePromise, - ): Effect { + ): Effect { return new Effect(async (deps: TDeps) => { const result = await this.fn(deps); if (result.isError()) { @@ -62,8 +64,8 @@ export class Effect< flatMap( fn: ( value: TValue, - ) => Effect, - ): Effect, TNewValue, TError | TNewError> { + ) => Effect, + ): Effect> { return new Effect(async (deps: TDeps & TNewDeps) => { const result = await this.fn(deps); if (result.isError()) { @@ -71,16 +73,72 @@ export class Effect< } return await fn(result.value as TValue).fn(deps); }) as Effect< - MergeTypes, TNewValue, - TError | TNewError + TError | TNewError, + MergeTypes >; } async run(deps: TDeps): Promise> { return await this.fn(deps); } + + async runOrThrow(deps: TDeps): Promise { + return (await this.fn(deps)).unwrapOrThrow(); + } + + async failOrThrow(deps: TDeps): Promise { + return (await this.fn(deps)).unwrapErrorOrThrow(); + } + + static seq< + T1, + TE1 extends TaggedError, + T2, + TE2 extends TaggedError, + >( + fn1: () => Effect, + fn2: (value: T1) => Effect, + ): Effect; + static seq< + T1, + TE1 extends TaggedError, + T2, + TE2 extends TaggedError, + T3, + TE3 extends TaggedError, + >( + fn1: () => Effect, + fn2: (value: T1) => Effect, + fn3: (value: T2) => Effect, + ): Effect; + static seq< + T1, + TE1 extends TaggedError, + T2, + TE2 extends TaggedError, + T3, + TE3 extends TaggedError, + T4, + TE4 extends TaggedError, + >( + fn1: () => Effect, + fn2: (value: T1) => Effect, + fn3: (value: T2) => Effect, + fn4: (value: T3) => Effect, + ): Effect; + static seq( + ...fns: ((...args: any[]) => Effect)[] + ): Effect { + 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 extends - Effect ? TDeps : never; + Effect ? TDeps : never; diff --git a/packages/fabric/core/error/tagged-error.ts b/packages/fabric/core/error/tagged-error.ts index 4f9ee94..137776d 100644 --- a/packages/fabric/core/error/tagged-error.ts +++ b/packages/fabric/core/error/tagged-error.ts @@ -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. diff --git a/packages/fabric/core/index.ts b/packages/fabric/core/index.ts index dfbd794..c7adb44 100644 --- a/packages/fabric/core/index.ts +++ b/packages/fabric/core/index.ts @@ -1,5 +1,6 @@ import Decimal from "decimal"; export * from "./array/index.ts"; +export * from "./effect/index.ts"; export * from "./error/index.ts"; export * from "./record/index.ts"; export * from "./result/index.ts"; diff --git a/packages/fabric/core/result/async-result.ts b/packages/fabric/core/result/async-result.ts deleted file mode 100644 index 7ee7774..0000000 --- a/packages/fabric/core/result/async-result.ts +++ /dev/null @@ -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( - fn: () => MaybePromise, - errorMapper: (error: any) => TError, - ): AsyncResult { - return new AsyncResult( - new Promise>(async (resolve) => { - try { - const value = await fn(); - resolve(Result.ok(value)); - } catch (error) { - resolve(Result.failWith(errorMapper(error))); - } - }), - ); - } - - static from(fn: () => MaybePromise): AsyncResult { - return AsyncResult.tryFrom( - fn, - (error) => new UnexpectedError(error) as never, - ); - } - - static ok(): AsyncResult; - static ok(value: T): AsyncResult; - static ok(value?: any) { - return new AsyncResult(Promise.resolve(Result.ok(value))); - } - - static succeedWith = AsyncResult.ok; - - static failWith( - error: TError, - ): AsyncResult { - return new AsyncResult(Promise.resolve(Result.failWith(error))); - } - - private constructor(private r: Promise>) { - } - - promise(): Promise> { - return this.r; - } - - async unwrapOrThrow(): Promise { - return (await this.r).unwrapOrThrow(); - } - - async orThrow(): Promise { - return (await this.r).orThrow(); - } - - async unwrapErrorOrThrow(): Promise { - return (await this.r).unwrapErrorOrThrow(); - } - - /** - * Map a function over the value of the result. - */ - map( - fn: (value: TValue) => TMappedValue, - ): AsyncResult { - return new AsyncResult( - this.r.then((result) => result.map(fn)), - ); - } - - /** - * Maps a function over the value of the result and flattens the result. - */ - flatMap( - fn: (value: TValue) => AsyncResult, - ): AsyncResult { - 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( - fn: (value: TValue) => TMappedValue, - errMapper: (error: any) => TError, - ): AsyncResult { - return new AsyncResult( - this.r.then((result) => result.tryMap(fn, errMapper)), - ); - } - - /** - * Map a function over the error of the result. - */ - errorMap( - fn: (error: TError) => TMappedError, - ): AsyncResult { - 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 { - return new AsyncResult( - this.r.then((result) => result.tap(fn)), - ); - } - - assert( - fn: (value: TValue) => AsyncResult, - ): AsyncResult { - return new AsyncResult( - this.r.then((result) => { - if (result.isError()) { - return result as any; - } - - return (fn(result.unwrapOrThrow())).promise(); - }), - ); - } -} diff --git a/packages/fabric/core/result/index.ts b/packages/fabric/core/result/index.ts index d7ad8f3..3d8709e 100644 --- a/packages/fabric/core/result/index.ts +++ b/packages/fabric/core/result/index.ts @@ -1,2 +1 @@ -export * from "./async-result.ts"; export * from "./result.ts"; diff --git a/packages/fabric/core/run/run.test.ts b/packages/fabric/core/run/run.test.ts index 9ede254..0de52b1 100644 --- a/packages/fabric/core/run/run.test.ts +++ b/packages/fabric/core/run/run.test.ts @@ -1,4 +1,4 @@ -import { AsyncResult } from "@fabric/core"; +import { Effect } from "@fabric/core"; import { describe, expect, test } from "@fabric/testing"; import { UnexpectedError } from "../error/unexpected-error.ts"; import { Run } from "./run.ts"; @@ -6,21 +6,21 @@ import { Run } from "./run.ts"; describe("Run", () => { describe("In Sequence", () => { test("should pipe the results of multiple async functions", async () => { - const result = Run.seq( - () => AsyncResult.succeedWith(1), - (x) => AsyncResult.succeedWith(x + 1), - (x) => AsyncResult.succeedWith(x * 2), + const result = await Run.seq( + () => Effect.ok(1), + (x) => Effect.ok(x + 1), + (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 () => { const result = await Run.seq( - () => AsyncResult.succeedWith(1), - () => AsyncResult.failWith(new UnexpectedError()), - (x) => AsyncResult.succeedWith(x * 2), - ).promise(); + () => Effect.ok(1), + () => Effect.failWith(new UnexpectedError()), + (x) => Effect.ok(x * 2), + ); expect(result.isError()).toBe(true); }); diff --git a/packages/fabric/core/run/run.ts b/packages/fabric/core/run/run.ts index dfca83c..3475c40 100644 --- a/packages/fabric/core/run/run.ts +++ b/packages/fabric/core/run/run.ts @@ -1,6 +1,7 @@ // 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 { AsyncResult } from "../result/async-result.ts"; +import { Result } from "../result/index.ts"; export namespace Run { // prettier-ignore @@ -10,10 +11,9 @@ export namespace Run { T2, TE2 extends TaggedError, >( - fn1: () => AsyncResult, - fn2: (value: T1) => AsyncResult, - ): AsyncResult; - // prettier-ignore + fn1: () => Effect, + fn2: (value: T1) => Effect, + ): Promise>; export function seq< T1, TE1 extends TaggedError, @@ -22,11 +22,10 @@ export namespace Run { T3, TE3 extends TaggedError, >( - fn1: () => AsyncResult, - fn2: (value: T1) => AsyncResult, - fn3: (value: T2) => AsyncResult, - ): AsyncResult; - // prettier-ignore + fn1: () => Effect, + fn2: (value: T1) => Effect, + fn3: (value: T2) => Effect, + ): Promise>; export function seq< T1, TE1 extends TaggedError, @@ -37,21 +36,21 @@ export namespace Run { T4, TE4 extends TaggedError, >( - fn1: () => AsyncResult, - fn2: (value: T1) => AsyncResult, - fn3: (value: T2) => AsyncResult, - fn4: (value: T3) => AsyncResult, - ): AsyncResult; + fn1: () => Effect, + fn2: (value: T1) => Effect, + fn3: (value: T2) => Effect, + fn4: (value: T3) => Effect, + ): Promise>; export function seq( - ...fns: ((...args: any[]) => AsyncResult)[] - ): AsyncResult { + ...fns: ((...args: any[]) => Effect)[] + ): Promise> { let result = fns[0]!(); for (let i = 1; i < fns.length; i++) { result = result.flatMap((value) => fns[i]!(value)); } - return result; + return result.run(); } // prettier-ignore @@ -61,8 +60,8 @@ export namespace Run { T2, TE2 extends TaggedError, >( - fn1: () => AsyncResult, - fn2: (value: T1) => AsyncResult, + fn1: () => Effect, + fn2: (value: T1) => Effect, ): Promise; // prettier-ignore export function seqOrThrow< @@ -73,14 +72,14 @@ export namespace Run { T3, TE3 extends TaggedError, >( - fn1: () => AsyncResult, - fn2: (value: T1) => AsyncResult, - fn3: (value: T2) => AsyncResult, + fn1: () => Effect, + fn2: (value: T1) => Effect, + fn3: (value: T2) => Effect, ): Promise; - export function seqOrThrow( - ...fns: ((...args: any[]) => AsyncResult)[] + export async function seqOrThrow( + ...fns: ((...args: any[]) => Effect)[] ): Promise { - const result = (seq as any)(...fns); + const result = await (seq as any)(...fns); return result.unwrapOrThrow(); } diff --git a/packages/fabric/domain/events/event-store.ts b/packages/fabric/domain/events/event-store.ts index 3952fd0..9714ad6 100644 --- a/packages/fabric/domain/events/event-store.ts +++ b/packages/fabric/domain/events/event-store.ts @@ -1,7 +1,8 @@ import type { - AsyncResult, + Effect, MaybePromise, PosixDate, + UnexpectedError, UUID, VariantFromTag, VariantTag, @@ -16,11 +17,11 @@ export interface EventStore { */ append( event: T, - ): AsyncResult, StoreQueryError>; + ): Effect, StoreQueryError | UnexpectedError>; getEventsFromStream( streamId: UUID, - ): AsyncResult[], StoreQueryError>; + ): Effect[], StoreQueryError>; subscribe( events: TEventKey[], diff --git a/packages/fabric/domain/events/event.ts b/packages/fabric/domain/events/event.ts index ee2fd53..dda9b06 100644 --- a/packages/fabric/domain/events/event.ts +++ b/packages/fabric/domain/events/event.ts @@ -1,6 +1,5 @@ // deno-lint-ignore-file no-explicit-any -import type { TaggedVariant, VariantTag } from "@fabric/core"; -import type { UUID } from "../../core/types/uuid.ts"; +import type { TaggedVariant, UUID, VariantTag } from "@fabric/core"; /** * An event is a tagged variant with a payload and a timestamp. diff --git a/packages/fabric/domain/models/field-parsers.ts b/packages/fabric/domain/models/field-parsers.ts index e2dfcf2..b6547a2 100644 --- a/packages/fabric/domain/models/field-parsers.ts +++ b/packages/fabric/domain/models/field-parsers.ts @@ -6,7 +6,7 @@ import { type VariantFromTag, } from "@fabric/core"; import { isUUID, parseAndSanitizeString } from "@fabric/validations"; -import type { FieldDefinition, FieldToType } from "./index.ts"; +import { FieldDefinition, FieldToType } from "./fields.ts"; export type FieldParsers = { [K in FieldDefinition["_tag"]]: FieldParser< diff --git a/packages/fabric/domain/models/state-store.ts b/packages/fabric/domain/models/state-store.ts index 8c67d90..2292ccb 100644 --- a/packages/fabric/domain/models/state-store.ts +++ b/packages/fabric/domain/models/state-store.ts @@ -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 { ModelSchemaFromModels } from "./model-schema.ts"; import type { Model, ModelToType } from "./model.ts"; @@ -15,5 +15,5 @@ export interface WritableStateStore insertInto>( collection: T, record: ModelToType[T]>, - ): AsyncResult; + ): Effect; } diff --git a/packages/fabric/domain/models/store-query/store-query.ts b/packages/fabric/domain/models/store-query/store-query.ts index b967648..bb51aa1 100644 --- a/packages/fabric/domain/models/store-query/store-query.ts +++ b/packages/fabric/domain/models/store-query/store-query.ts @@ -1,10 +1,5 @@ // deno-lint-ignore-file no-explicit-any -import { - type AsyncResult, - type Keyof, - type Optional, - TaggedError, -} from "@fabric/core"; +import { Effect, type Keyof, type Optional, TaggedError } from "@fabric/core"; import type { StoreQueryError } from "../../errors/query-error.ts"; import type { FilterOptions } from "./filter-options.ts"; import type { OrderByOptions } from "./order-by-options.ts"; @@ -14,84 +9,84 @@ export interface StoreQuery { orderBy(opts: OrderByOptions): StoreLimitableQuery; limit(limit: number, offset?: number): SelectableQuery; - select(): AsyncResult; + select(): Effect; select>( keys: K[], - ): AsyncResult[], StoreQueryError>; + ): Effect[], StoreQueryError>; - selectOne(): AsyncResult, StoreQueryError>; + selectOne(): Effect, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult>, StoreQueryError>; + ): Effect>, StoreQueryError>; - selectOneOrFail(): AsyncResult; + selectOneOrFail(): Effect; selectOneOrFail>( keys: K[], - ): AsyncResult, StoreQueryError | NotFoundError>; + ): Effect, StoreQueryError | NotFoundError>; - assertNone(): AsyncResult; + assertNone(): Effect; } export interface StoreSortableQuery { orderBy(opts: OrderByOptions): StoreLimitableQuery; limit(limit: number, offset?: number): SelectableQuery; - select(): AsyncResult; + select(): Effect; select>( keys: K[], - ): AsyncResult[], StoreQueryError>; + ): Effect[], StoreQueryError>; - selectOne(): AsyncResult, StoreQueryError>; + selectOne(): Effect, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult>, StoreQueryError>; + ): Effect>, StoreQueryError>; - selectOneOrFail(): AsyncResult; + selectOneOrFail(): Effect; selectOneOrFail>( keys: K[], - ): AsyncResult, StoreQueryError | NotFoundError>; + ): Effect, StoreQueryError | NotFoundError>; - assertNone(): AsyncResult; + assertNone(): Effect; } export interface StoreLimitableQuery { limit(limit: number, offset?: number): SelectableQuery; - select(): AsyncResult; + select(): Effect; select>( keys: K[], - ): AsyncResult[], StoreQueryError>; + ): Effect[], StoreQueryError>; - selectOne(): AsyncResult, StoreQueryError>; + selectOne(): Effect, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult>, StoreQueryError>; + ): Effect>, StoreQueryError>; - selectOneOrFail(): AsyncResult; + selectOneOrFail(): Effect; selectOneOrFail>( keys: K[], - ): AsyncResult, StoreQueryError | NotFoundError>; + ): Effect, StoreQueryError | NotFoundError>; - assertNone(): AsyncResult; + assertNone(): Effect; } export interface SelectableQuery { - select(): AsyncResult; + select(): Effect; select>( keys: K[], - ): AsyncResult[], StoreQueryError>; + ): Effect[], StoreQueryError>; - selectOne(): AsyncResult, StoreQueryError>; + selectOne(): Effect, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult>, StoreQueryError>; + ): Effect>, StoreQueryError>; - selectOneOrFail(): AsyncResult; + selectOneOrFail(): Effect; selectOneOrFail>( keys: K[], - ): AsyncResult, StoreQueryError | NotFoundError>; + ): Effect, StoreQueryError | NotFoundError>; - assertNone(): AsyncResult; + assertNone(): Effect; } export interface StoreQueryDefinition { diff --git a/packages/fabric/domain/services/uuid-generator.mock.ts b/packages/fabric/domain/services/uuid-generator.mock.ts index c4f4843..883337c 100644 --- a/packages/fabric/domain/services/uuid-generator.mock.ts +++ b/packages/fabric/domain/services/uuid-generator.mock.ts @@ -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"; export const UUIDGeneratorMock: UUIDGenerator = { diff --git a/packages/fabric/domain/services/uuid-generator.ts b/packages/fabric/domain/services/uuid-generator.ts index d586ff2..1bbefb6 100644 --- a/packages/fabric/domain/services/uuid-generator.ts +++ b/packages/fabric/domain/services/uuid-generator.ts @@ -1,4 +1,4 @@ -import type { UUID } from "../../core/types/uuid.ts"; +import type { UUID } from "@fabric/core"; export interface UUIDGenerator { generate(): UUID; diff --git a/packages/fabric/domain/use-case/use-case.ts b/packages/fabric/domain/use-case/use-case.ts index eeb8a2c..1df349a 100644 --- a/packages/fabric/domain/use-case/use-case.ts +++ b/packages/fabric/domain/use-case/use-case.ts @@ -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. @@ -9,8 +9,8 @@ export type UseCase< TOutput, TErrors extends TaggedError, > = TPayload extends undefined - ? (dependencies: TDependencies) => AsyncResult + ? (dependencies: TDependencies) => Effect : ( dependencies: TDependencies, payload: TPayload, - ) => AsyncResult; + ) => Effect; diff --git a/packages/fabric/sqlite-store/events/event-store.test.ts b/packages/fabric/sqlite-store/events/event-store.test.ts index 6b4efbc..ecf2fcf 100644 --- a/packages/fabric/sqlite-store/events/event-store.test.ts +++ b/packages/fabric/sqlite-store/events/event-store.test.ts @@ -22,11 +22,11 @@ describe("Event Store", () => { beforeEach(async () => { store = new SQLiteEventStore(":memory:"); - await store.migrate().orThrow(); + await store.migrate().runOrThrow(); }); afterEach(async () => { - await store.close().orThrow(); + await store.close().runOrThrow(); }); test("Should append an event", async () => { @@ -39,9 +39,9 @@ describe("Event Store", () => { 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); @@ -69,7 +69,7 @@ describe("Event Store", () => { store.subscribe(["UserCreated"], subscriber); - await store.append(userCreated).orThrow(); + await store.append(userCreated).runOrThrow(); expect(subscriber).toHaveBeenCalledTimes(1); expect(subscriber).toHaveBeenCalledWith({ diff --git a/packages/fabric/sqlite-store/events/event-store.ts b/packages/fabric/sqlite-store/events/event-store.ts index d9840b7..d686a1d 100644 --- a/packages/fabric/sqlite-store/events/event-store.ts +++ b/packages/fabric/sqlite-store/events/event-store.ts @@ -1,9 +1,10 @@ import { - AsyncResult, + Effect, JSONUtils, MaybePromise, PosixDate, - Run, + Result, + UnexpectedError, UUID, VariantTag, } from "@fabric/core"; @@ -32,8 +33,8 @@ export class SQLiteEventStore this.db = new SQLiteDatabase(dbPath); } - migrate(): AsyncResult { - return AsyncResult.tryFrom( + migrate(): Effect { + return Effect.tryFrom( () => { this.db.init(); this.db.run( @@ -54,8 +55,8 @@ export class SQLiteEventStore getEventsFromStream( streamId: UUID, - ): AsyncResult[], StoreQueryError> { - return AsyncResult.tryFrom( + ): Effect[], StoreQueryError> { + return Effect.tryFrom( () => { const events = this.db.allPrepared( `SELECT * FROM events WHERE streamId = $id`, @@ -79,13 +80,13 @@ export class SQLiteEventStore append( event: T, - ): AsyncResult, StoreQueryError> { - return Run.seq( + ): Effect, StoreQueryError | UnexpectedError> { + return Effect.seq( () => this.getLastVersion(event.streamId), (version) => - AsyncResult.from(() => { + Effect.from(() => { this.streamVersions.set(event.streamId, version + 1n); - return version; + return Result.ok(version); }), (version) => this.storeEvent(event.streamId, version + 1n, event), (storedEvent) => @@ -93,15 +94,17 @@ export class SQLiteEventStore ); } - private notifySubscribers(event: StoredEvent): AsyncResult { - return AsyncResult.from(async () => { + private notifySubscribers( + event: StoredEvent, + ): Effect { + return Effect.tryFrom(async () => { const subscribers = this.eventSubscribers.get(event[VariantTag]) || []; await Promise.all(subscribers.map((subscriber) => subscriber(event))); - }); + }, (e) => new UnexpectedError(e.message)); } - private getLastVersion(streamId: UUID): AsyncResult { - return AsyncResult.tryFrom( + private getLastVersion(streamId: UUID): Effect { + return Effect.tryFrom( () => { const { lastVersion } = this.db.onePrepared( `SELECT max(version) as lastVersion FROM events WHERE streamId = $id`, @@ -132,8 +135,8 @@ export class SQLiteEventStore }); } - close(): AsyncResult { - return AsyncResult.tryFrom( + close(): Effect { + return Effect.tryFrom( () => this.db.close(), (error) => new StoreQueryError(error.message), ); @@ -143,8 +146,8 @@ export class SQLiteEventStore streamId: UUID, version: bigint, event: T, - ): AsyncResult, StoreQueryError> { - return AsyncResult.tryFrom( + ): Effect, StoreQueryError> { + return Effect.tryFrom( () => { const storedEvent: StoredEvent = { ...event, diff --git a/packages/fabric/sqlite-store/state/query-builder.test.ts b/packages/fabric/sqlite-store/state/query-builder.test.ts index 26beafc..55ea3f5 100644 --- a/packages/fabric/sqlite-store/state/query-builder.test.ts +++ b/packages/fabric/sqlite-store/state/query-builder.test.ts @@ -20,26 +20,26 @@ describe("QueryBuilder", () => { beforeEach(async () => { stateStore = new SQLiteStateStore(":memory:", models); - await stateStore.migrate().unwrapOrThrow(); + await stateStore.migrate().runOrThrow(); await stateStore.insertInto("test", { id: UUIDGeneratorMock.generate(), name: "test1", - }).unwrapOrThrow(); + }).runOrThrow(); await stateStore.insertInto("test", { id: UUIDGeneratorMock.generate(), name: "test2", - }).unwrapOrThrow(); + }).runOrThrow(); }); afterEach(async () => { - await stateStore.close().unwrapOrThrow(); + await stateStore.close().runOrThrow(); }); test("select() after a where() should return valid results", async () => { const result = await stateStore.from("test").where({ name: isLike("test%"), }) - .select().unwrapOrThrow(); + .select().runOrThrow(); expect(result).toEqual([{ id: expect.any(String), name: "test1", @@ -51,7 +51,7 @@ describe("QueryBuilder", () => { test("selectOneOrFail() should return a single result", async () => { const result = await stateStore.from("test").where({ name: "test1" }) - .selectOneOrFail().unwrapOrThrow(); + .selectOneOrFail().runOrThrow(); expect(result).toEqual({ id: expect.any(String), name: "test1", @@ -60,14 +60,14 @@ describe("QueryBuilder", () => { test("selectOneOrFail() should fail if no results are found", async () => { const error = await stateStore.from("test").where({ name: "not-found" }) - .selectOneOrFail().unwrapErrorOrThrow(); + .selectOneOrFail().failOrThrow(); expect(error).toBeInstanceOf(NotFoundError); }); test("selectOne() should return a single result", async () => { const result = await stateStore.from("test") - .selectOne().unwrapOrThrow(); + .selectOne().runOrThrow(); expect(result).toEqual({ id: expect.any(String), @@ -79,7 +79,7 @@ describe("QueryBuilder", () => { const result = await stateStore.from("test").where({ name: "not-found", }) - .selectOne().unwrapOrThrow(); + .selectOne().runOrThrow(); expect(result).toBeUndefined(); }); @@ -87,14 +87,14 @@ describe("QueryBuilder", () => { test("assertNone() should succeed if no results are found", async () => { const result = await stateStore.from("test").where({ name: "not-found", - }).assertNone().unwrapOrThrow(); + }).assertNone().runOrThrow(); expect(result).toBeUndefined(); }); test("assertNone() should fail if results are found", async () => { const error = await stateStore.from("test").where({ name: "test1" }) - .assertNone().unwrapErrorOrThrow(); + .assertNone().failOrThrow(); expect(error).toBeInstanceOf(AlreadyExistsError); }); diff --git a/packages/fabric/sqlite-store/state/query-builder.ts b/packages/fabric/sqlite-store/state/query-builder.ts index bfe372b..83ac428 100644 --- a/packages/fabric/sqlite-store/state/query-builder.ts +++ b/packages/fabric/sqlite-store/state/query-builder.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file no-explicit-any -import { AsyncResult, Keyof, Optional } from "@fabric/core"; +import { Effect, Keyof, Optional } from "@fabric/core"; import { AlreadyExistsError, FilterOptions, @@ -47,12 +47,12 @@ export class QueryBuilder implements StoreQuery { }); } - select(): AsyncResult; + select(): Effect; select>( keys: K[], - ): AsyncResult[], StoreQueryError>; - select>(keys?: K[]): AsyncResult { - return AsyncResult.tryFrom( + ): Effect[], StoreQueryError>; + select>(keys?: K[]): Effect { + return Effect.tryFrom( () => { const [sql, params] = getSelectStatement( this.schema[this.query.from]!, @@ -71,12 +71,12 @@ export class QueryBuilder implements StoreQuery { ); } - selectOne(): AsyncResult, StoreQueryError>; + selectOne(): Effect, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult>, StoreQueryError>; - selectOne>(keys?: K[]): AsyncResult { - return AsyncResult.tryFrom( + ): Effect>, StoreQueryError>; + selectOne>(keys?: K[]): Effect { + return Effect.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, @@ -96,14 +96,14 @@ export class QueryBuilder implements StoreQuery { ); } - selectOneOrFail(): AsyncResult; + selectOneOrFail(): Effect; selectOneOrFail>( keys: K[], - ): AsyncResult, StoreQueryError | NotFoundError>; + ): Effect, StoreQueryError | NotFoundError>; selectOneOrFail>( keys?: K[], - ): AsyncResult { - return AsyncResult.tryFrom( + ): Effect { + return Effect.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, @@ -122,14 +122,14 @@ export class QueryBuilder implements StoreQuery { (err) => new StoreQueryError(err.message), ).flatMap((result) => { if (!result) { - return AsyncResult.failWith(new NotFoundError()); + return Effect.failWith(new NotFoundError()); } - return AsyncResult.ok(result); + return Effect.ok(result); }); } - assertNone(): AsyncResult { - return AsyncResult.tryFrom( + assertNone(): Effect { + return Effect.tryFrom( async () => { const [stmt, params] = getSelectStatement( this.schema[this.query.from]!, @@ -146,9 +146,9 @@ export class QueryBuilder implements StoreQuery { (err) => new StoreQueryError(err.message), ).flatMap((result) => { if (result) { - return AsyncResult.failWith(new AlreadyExistsError()); + return Effect.failWith(new AlreadyExistsError()); } - return AsyncResult.ok(); + return Effect.ok(); }); } } diff --git a/packages/fabric/sqlite-store/state/state-store.test.ts b/packages/fabric/sqlite-store/state/state-store.test.ts index 9de0bb3..d70db8b 100644 --- a/packages/fabric/sqlite-store/state/state-store.test.ts +++ b/packages/fabric/sqlite-store/state/state-store.test.ts @@ -1,4 +1,4 @@ -import { Run } from "@fabric/core"; +import { Effect, Run } from "@fabric/core"; import { Field, isLike, Model } from "@fabric/domain"; import { UUIDGeneratorMock } from "@fabric/domain/mocks"; import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing"; @@ -20,11 +20,11 @@ describe("State Store", () => { beforeEach(async () => { store = new SQLiteStateStore(":memory:", models); - await store.migrate().orThrow(); + await store.migrate().runOrThrow(); }); afterEach(async () => { - await store.close().orThrow(); + await store.close().runOrThrow(); }); test("should insert a record", async () => { @@ -33,7 +33,7 @@ describe("State Store", () => { await store.insertInto("users", { id: newUUID, name: "test", - }).orThrow(); + }).runOrThrow(); }); test("should select all records", async () => { @@ -42,9 +42,9 @@ describe("State Store", () => { await store.insertInto("users", { name: "test", id: newUUID, - }).orThrow(); + }).runOrThrow(); - const result = await store.from("users").select().unwrapOrThrow(); + const result = await store.from("users").select().runOrThrow(); // expectTypeOf(result).toEqualTypeOf< // { @@ -64,7 +64,7 @@ describe("State Store", () => { test("should select records with a filter", async () => { const newUUID = UUIDGeneratorMock.generate(); - await Run.seqOrThrow( + await Effect.seq( () => store.insertInto("users", { name: "test", @@ -80,14 +80,14 @@ describe("State Store", () => { name: "anotherName2", id: UUIDGeneratorMock.generate(), }), - ); + ).runOrThrow(); const result = await store .from("users") .where({ name: isLike("te%"), }) - .select().unwrapOrThrow(); + .select().runOrThrow(); // expectTypeOf(result).toEqualTypeOf< // { @@ -107,17 +107,20 @@ describe("State Store", () => { test("should update a record", async () => { const newUUID = UUIDGeneratorMock.generate(); - await store.insertInto("users", { - name: "test", - id: newUUID, - }).orThrow(); - - await store.update("users", newUUID, { - name: "updated", - }).orThrow(); + await Effect.seq( + () => + store.insertInto("users", { + name: "test", + id: newUUID, + }), + () => + store.update("users", newUUID, { + name: "updated", + }), + ).runOrThrow(); const result = await store.from("users").where({ id: newUUID }).selectOne() - .unwrapOrThrow(); + .runOrThrow(); expect(result).toEqual({ id: newUUID, @@ -128,34 +131,37 @@ describe("State Store", () => { test("should delete a record", async () => { const newUUID = UUIDGeneratorMock.generate(); - await store.insertInto("users", { - name: "test", - id: newUUID, - }).orThrow(); - - await store.delete("users", newUUID).orThrow(); + await Effect.seq( + () => + store.insertInto("users", { + name: "test", + id: newUUID, + }), + () => store.delete("users", newUUID), + ).runOrThrow(); const result = await store.from("users").where({ id: newUUID }).selectOne() - .unwrapOrThrow(); + .runOrThrow(); expect(result).toBeUndefined(); }); - //test for inserting into a collection with a reference - test("should insert a record with a reference", async () => { const newUUID = UUIDGeneratorMock.generate(); const ownerUUID = UUIDGeneratorMock.generate(); - await store.insertInto("users", { - id: ownerUUID, - name: "test", - }).orThrow(); - - await store.insertInto("demo", { - id: newUUID, - value: 1.0, - owner: ownerUUID, - }).orThrow(); + await Run.seqOrThrow( + () => + store.insertInto("users", { + id: ownerUUID, + name: "test", + }), + () => + store.insertInto("demo", { + id: newUUID, + value: 1.0, + owner: ownerUUID, + }), + ); }); }); diff --git a/packages/fabric/sqlite-store/state/state-store.ts b/packages/fabric/sqlite-store/state/state-store.ts index 6ba71c3..f54cf08 100644 --- a/packages/fabric/sqlite-store/state/state-store.ts +++ b/packages/fabric/sqlite-store/state/state-store.ts @@ -1,4 +1,4 @@ -import { AsyncResult, UnexpectedError, UUID } from "@fabric/core"; +import { Effect, UnexpectedError, UUID } from "@fabric/core"; import { Model, ModelSchemaFromModels, @@ -37,10 +37,10 @@ export class SQLiteStateStore insertInto>( collection: T, record: ModelToType[T]>, - ): AsyncResult { + ): Effect { const model = this.schema[collection]; - return AsyncResult.tryFrom( + return Effect.tryFrom( () => { this.db.runPrepared( `INSERT INTO ${model.name} (${ @@ -67,10 +67,10 @@ export class SQLiteStateStore collection: T, id: UUID, record: Partial[T]>>, - ): AsyncResult { + ): Effect { const model = this.schema[collection]; - return AsyncResult.tryFrom( + return Effect.tryFrom( () => { const params = recordToSQLParamRecord(model, { ...record, @@ -92,10 +92,10 @@ export class SQLiteStateStore delete>( collection: T, id: UUID, - ): AsyncResult { + ): Effect { const model = this.schema[collection]; - return AsyncResult.tryFrom( + return Effect.tryFrom( () => { this.db.runPrepared( `DELETE FROM ${model.name} WHERE id = ${keyToParamKey("id")}`, @@ -108,8 +108,8 @@ export class SQLiteStateStore ); } - migrate(): AsyncResult { - return AsyncResult.tryFrom( + migrate(): Effect { + return Effect.tryFrom( async () => { this.db.init(); await this.db.withTransaction(() => { @@ -124,7 +124,10 @@ export class SQLiteStateStore ); } - close(): AsyncResult { - return AsyncResult.from(() => this.db.close()); + close(): Effect { + return Effect.tryFrom( + () => this.db.close(), + (e) => new UnexpectedError(e.message), + ); } }