Compare commits
No commits in common. "4574b9871b04ba278d6d50fa0c57017b09937565" and "4950730d9ed1757cce9b0919e5cd927bd3606b7f" have entirely different histories.
4574b9871b
...
4950730d9e
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@ -1,17 +1,24 @@
|
|||||||
{
|
{
|
||||||
"cSpell.enabled": true,
|
"cSpell.enabled": true,
|
||||||
"cSpell.language": "en,es",
|
"cSpell.language": "en,es",
|
||||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.autoSave": "off",
|
"files.autoSave": "off",
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
|
"javascript.preferences.importModuleSpecifierEnding": "js",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "always",
|
"source.fixAll": "always",
|
||||||
"source.organizeImports": "always"
|
"source.organizeImports": "always"
|
||||||
},
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.yarn": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"packages/frontend/{android,ios}": true
|
||||||
|
},
|
||||||
|
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||||
"cSpell.words": ["autodocs", "Syntropy"],
|
"cSpell.words": ["autodocs", "Syntropy"],
|
||||||
"deno.enable": true,
|
"typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"],
|
||||||
"deno.lint": true,
|
"[typescript]": {
|
||||||
"deno.unstable": [],
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
"deno.suggest.imports.autoDiscover": true
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// deno-lint-ignore-file no-namespace no-explicit-any no-async-promise-executor
|
// deno-lint-ignore-file no-namespace no-explicit-any
|
||||||
import { UnexpectedError } from "@fabric/core";
|
|
||||||
import type { TaggedError } from "../error/tagged-error.ts";
|
import type { TaggedError } from "../error/tagged-error.ts";
|
||||||
|
import { UnexpectedError } from "../error/unexpected-error.ts";
|
||||||
import type { MaybePromise } from "../types/maybe-promise.ts";
|
import type { MaybePromise } from "../types/maybe-promise.ts";
|
||||||
import { Result } from "./result.ts";
|
import { Result } from "./result.ts";
|
||||||
|
|
||||||
@ -8,123 +8,24 @@ import { Result } from "./result.ts";
|
|||||||
* An AsyncResult represents the result of an asynchronous operation that can
|
* An AsyncResult represents the result of an asynchronous operation that can
|
||||||
* resolve to a value of type `TValue` or an error of type `TError`.
|
* resolve to a value of type `TValue` or an error of type `TError`.
|
||||||
*/
|
*/
|
||||||
export class AsyncResult<
|
export type AsyncResult<
|
||||||
TValue = any,
|
TValue = any,
|
||||||
TError extends TaggedError = never,
|
TError extends TaggedError = never,
|
||||||
> {
|
> = Promise<Result<TValue, TError>>;
|
||||||
static tryFrom<T, TError extends TaggedError>(
|
|
||||||
|
export namespace AsyncResult {
|
||||||
|
export async function tryFrom<T, TError extends TaggedError>(
|
||||||
fn: () => MaybePromise<T>,
|
fn: () => MaybePromise<T>,
|
||||||
errorMapper: (error: any) => TError,
|
errorMapper: (error: any) => TError,
|
||||||
): AsyncResult<T, TError> {
|
): AsyncResult<T, TError> {
|
||||||
return new AsyncResult(
|
try {
|
||||||
new Promise<Result<T, TError>>(async (resolve) => {
|
return Result.succeedWith(await fn());
|
||||||
try {
|
} catch (error) {
|
||||||
const value = await fn();
|
return Result.failWith(errorMapper(error));
|
||||||
resolve(Result.ok(value));
|
}
|
||||||
} catch (error) {
|
|
||||||
resolve(Result.failWith(errorMapper(error)));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
|
export function from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
|
||||||
return AsyncResult.tryFrom(
|
return tryFrom(fn, (error) => new UnexpectedError(error) as never);
|
||||||
fn,
|
|
||||||
(error) => new UnexpectedError(error) as never,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ok<T>(value: T): AsyncResult<T, never> {
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
mapError<TMappedError extends TaggedError>(
|
|
||||||
fn: (error: TError) => TMappedError,
|
|
||||||
): AsyncResult<TValue, TMappedError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.mapError(fn)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Taps a function if the result is a success.
|
|
||||||
* This is useful for side effects that do not modify the result.
|
|
||||||
*/
|
|
||||||
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => result.tap(fn)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
import { AsyncResult } from "@fabric/core";
|
// deno-lint-ignore-file require-await
|
||||||
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 { Result } from "../result/result.ts";
|
||||||
import { Run } from "./run.ts";
|
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),
|
async () => Result.succeedWith(1),
|
||||||
(x) => AsyncResult.succeedWith(x + 1),
|
async (x) => Result.succeedWith(x + 1),
|
||||||
(x) => AsyncResult.succeedWith(x * 2),
|
async (x) => Result.succeedWith(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),
|
async () => Result.succeedWith(1),
|
||||||
() => AsyncResult.failWith(new UnexpectedError()),
|
async () => Result.failWith(new UnexpectedError()),
|
||||||
(x) => AsyncResult.succeedWith(x * 2),
|
async (x) => Result.succeedWith(x * 2),
|
||||||
).promise();
|
);
|
||||||
|
|
||||||
expect(result.isError()).toBe(true);
|
expect(result.isError()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { AsyncResult } from "../result/async-result.ts";
|
|||||||
|
|
||||||
export namespace Run {
|
export namespace Run {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seq<
|
export async function seq<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
T2,
|
T2,
|
||||||
@ -14,7 +14,7 @@ export namespace Run {
|
|||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
||||||
): AsyncResult<T2, TE1 | TE2>;
|
): AsyncResult<T2, TE1 | TE2>;
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seq<
|
export async function seq<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
T2,
|
T2,
|
||||||
@ -27,7 +27,7 @@ export namespace Run {
|
|||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
||||||
): AsyncResult<T3, TE1 | TE2 | TE3>;
|
): AsyncResult<T3, TE1 | TE2 | TE3>;
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seq<
|
export async function seq<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
T2,
|
T2,
|
||||||
@ -42,20 +42,24 @@ export namespace Run {
|
|||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
||||||
fn4: (value: T3) => AsyncResult<T4, TE4>,
|
fn4: (value: T3) => AsyncResult<T4, TE4>,
|
||||||
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
|
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
|
||||||
export function seq(
|
export async function seq(
|
||||||
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
||||||
): AsyncResult<any, any> {
|
): AsyncResult<any, any> {
|
||||||
let result = fns[0]!();
|
let result = await 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));
|
if (result.isError()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await fns[i]!(result.unwrapOrThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seqOrThrow<
|
export async function seqUNSAFE<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
T2,
|
T2,
|
||||||
@ -65,7 +69,7 @@ export namespace Run {
|
|||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
||||||
): Promise<T2>;
|
): Promise<T2>;
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export function seqOrThrow<
|
export async function seqUNSAFE<
|
||||||
T1,
|
T1,
|
||||||
TE1 extends TaggedError,
|
TE1 extends TaggedError,
|
||||||
T2,
|
T2,
|
||||||
@ -77,11 +81,21 @@ export namespace Run {
|
|||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
||||||
): Promise<T2>;
|
): Promise<T2>;
|
||||||
export function seqOrThrow(
|
export async function seqUNSAFE(
|
||||||
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const result = (seq as any)(...fns);
|
const result = await (seq as any)(...fns);
|
||||||
|
|
||||||
|
if (result.isError()) {
|
||||||
|
throw result.unwrapOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
return result.unwrapOrThrow();
|
return result.unwrapOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function UNSAFE<T, TError extends TaggedError>(
|
||||||
|
fn: () => AsyncResult<T, TError>,
|
||||||
|
): Promise<T> {
|
||||||
|
return (await fn()).unwrapOrThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export * from "./fields/index.ts";
|
export * from "./fields/index.ts";
|
||||||
export * from "./model-schema.ts";
|
export * from "./model-schema.ts";
|
||||||
export * from "./model.ts";
|
export * from "./model.ts";
|
||||||
|
export * from "./query/index.ts";
|
||||||
export * from "./state-store.ts";
|
export * from "./state-store.ts";
|
||||||
export * from "./store-query/index.ts";
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./filter-options.ts";
|
export * from "./filter-options.ts";
|
||||||
export * from "./order-by-options.ts";
|
export * from "./order-by-options.ts";
|
||||||
export * from "./store-query.ts";
|
export * from "./query.ts";
|
||||||
@ -1,10 +1,5 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import {
|
import type { AsyncResult, Keyof, Optional } 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";
|
||||||
@ -23,11 +18,6 @@ export interface StoreQuery<T> {
|
|||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreSortableQuery<T> {
|
export interface StoreSortableQuery<T> {
|
||||||
@ -43,11 +33,6 @@ export interface StoreSortableQuery<T> {
|
|||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreLimitableQuery<T> {
|
export interface StoreLimitableQuery<T> {
|
||||||
@ -62,11 +47,6 @@ export interface StoreLimitableQuery<T> {
|
|||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectableQuery<T> {
|
export interface SelectableQuery<T> {
|
||||||
@ -79,14 +59,9 @@ export interface SelectableQuery<T> {
|
|||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Keyof<T>>(
|
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreQueryDefinition<K extends string = string> {
|
export interface QueryDefinition<K extends string = string> {
|
||||||
from: K;
|
from: K;
|
||||||
where?: FilterOptions<any>;
|
where?: FilterOptions<any>;
|
||||||
orderBy?: OrderByOptions<any>;
|
orderBy?: OrderByOptions<any>;
|
||||||
@ -94,9 +69,3 @@ export interface StoreQueryDefinition<K extends string = string> {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
keys?: string[];
|
keys?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotFoundError extends TaggedError<"NotFoundError"> {
|
|
||||||
constructor() {
|
|
||||||
super("NotFoundError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ import type { AsyncResult } 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";
|
||||||
import type { StoreQuery } from "./store-query/store-query.ts";
|
import type { StoreQuery } from "./query/query.ts";
|
||||||
|
|
||||||
export interface ReadonlyStateStore<TModel extends Model> {
|
export interface ReadonlyStateStore<TModel extends Model> {
|
||||||
from<T extends keyof ModelSchemaFromModels<TModel>>(
|
from<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
|
||||||
import type { TaggedError } from "@fabric/core";
|
|
||||||
import type { UseCase } from "./use-case.ts";
|
|
||||||
|
|
||||||
export type Command<
|
|
||||||
TDependencies = any,
|
|
||||||
TPayload = any,
|
|
||||||
TEvent extends Event = any,
|
|
||||||
TErrors extends TaggedError<string> = any,
|
|
||||||
> = BasicCommandDefinition<TDependencies, TPayload, TEvent, TErrors>;
|
|
||||||
|
|
||||||
interface BasicCommandDefinition<
|
|
||||||
TDependencies,
|
|
||||||
TPayload,
|
|
||||||
TEvent extends Event,
|
|
||||||
TErrors extends TaggedError<string>,
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* The use case name.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the use case requires authentication or not.
|
|
||||||
*/
|
|
||||||
isAuthRequired: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permissions required to execute the use case.
|
|
||||||
*/
|
|
||||||
permissions?: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The use case function.
|
|
||||||
*/
|
|
||||||
useCase: UseCase<TDependencies, TPayload, TEvent, TErrors>;
|
|
||||||
}
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./command.ts";
|
export * from "./use-case-definition.ts";
|
||||||
export * from "./query.ts";
|
|
||||||
export * from "./use-case.ts";
|
export * from "./use-case.ts";
|
||||||
|
|||||||
@ -2,14 +2,14 @@
|
|||||||
import type { TaggedError } from "@fabric/core";
|
import type { TaggedError } from "@fabric/core";
|
||||||
import type { UseCase } from "./use-case.ts";
|
import type { UseCase } from "./use-case.ts";
|
||||||
|
|
||||||
export type Query<
|
export type UseCaseDefinition<
|
||||||
TDependencies = any,
|
TDependencies = any,
|
||||||
TPayload = any,
|
TPayload = any,
|
||||||
TOutput = any,
|
TOutput = any,
|
||||||
TErrors extends TaggedError<string> = any,
|
TErrors extends TaggedError<string> = any,
|
||||||
> = BasicQueryDefinition<TDependencies, TPayload, TOutput, TErrors>;
|
> = BasicUseCaseDefinition<TDependencies, TPayload, TOutput, TErrors>;
|
||||||
|
|
||||||
interface BasicQueryDefinition<
|
interface BasicUseCaseDefinition<
|
||||||
TDependencies,
|
TDependencies,
|
||||||
TPayload,
|
TPayload,
|
||||||
TOutput,
|
TOutput,
|
||||||
@ -25,11 +25,6 @@ interface BasicQueryDefinition<
|
|||||||
*/
|
*/
|
||||||
isAuthRequired: boolean;
|
isAuthRequired: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* Permissions required to execute the use case.
|
|
||||||
*/
|
|
||||||
permissions?: string[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The use case function.
|
* The use case function.
|
||||||
*/
|
*/
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { PosixDate } from "@fabric/core";
|
import { PosixDate, Run } from "@fabric/core";
|
||||||
import { Event } from "@fabric/domain";
|
import { Event } from "@fabric/domain";
|
||||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
||||||
import {
|
import {
|
||||||
@ -22,11 +22,11 @@ describe("Event Store", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
store = new SQLiteEventStore(":memory:");
|
store = new SQLiteEventStore(":memory:");
|
||||||
await store.migrate().orThrow();
|
await Run.UNSAFE(() => store.migrate());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await store.close().orThrow();
|
await Run.UNSAFE(() => store.close());
|
||||||
});
|
});
|
||||||
|
|
||||||
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 Run.UNSAFE(() => store.append(userCreated));
|
||||||
|
|
||||||
const events = await store.getEventsFromStream(newUUID).unwrapOrThrow();
|
const events = await Run.UNSAFE(() => store.getEventsFromStream(newUUID));
|
||||||
|
|
||||||
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 Run.UNSAFE(() => store.append(userCreated));
|
||||||
|
|
||||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
expect(subscriber).toHaveBeenCalledTimes(1);
|
||||||
expect(subscriber).toHaveBeenCalledWith({
|
expect(subscriber).toHaveBeenCalledWith({
|
||||||
|
|||||||
@ -89,7 +89,10 @@ export class SQLiteEventStore<TEvents extends Event>
|
|||||||
}),
|
}),
|
||||||
(version) => this.storeEvent(event.streamId, version + 1n, event),
|
(version) => this.storeEvent(event.streamId, version + 1n, event),
|
||||||
(storedEvent) =>
|
(storedEvent) =>
|
||||||
this.notifySubscribers(storedEvent).map(() => storedEvent),
|
AsyncResult.from(async () => {
|
||||||
|
await this.notifySubscribers(storedEvent);
|
||||||
|
return storedEvent;
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,14 +5,13 @@ import {
|
|||||||
FilterOptions,
|
FilterOptions,
|
||||||
ModelSchema,
|
ModelSchema,
|
||||||
OrderByOptions,
|
OrderByOptions,
|
||||||
|
QueryDefinition,
|
||||||
SelectableQuery,
|
SelectableQuery,
|
||||||
StoreLimitableQuery,
|
StoreLimitableQuery,
|
||||||
StoreQuery,
|
StoreQuery,
|
||||||
StoreQueryDefinition,
|
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
StoreSortableQuery,
|
StoreSortableQuery,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { NotFoundError } from "../../domain/models/store-query/store-query.ts";
|
|
||||||
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.ts";
|
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.ts";
|
||||||
import { transformRow } from "../sqlite/sql-to-value.ts";
|
import { transformRow } from "../sqlite/sql-to-value.ts";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
||||||
@ -21,7 +20,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
constructor(
|
constructor(
|
||||||
private db: SQLiteDatabase,
|
private db: SQLiteDatabase,
|
||||||
private schema: ModelSchema,
|
private schema: ModelSchema,
|
||||||
private query: StoreQueryDefinition,
|
private query: QueryDefinition,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
||||||
@ -94,42 +93,11 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
(err) => new StoreQueryError(err.message),
|
(err) => new StoreQueryError(err.message),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOneOrFail(): AsyncResult<T, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Extract<keyof T, string>>(
|
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
|
|
||||||
selectOneOrFail<K extends Extract<keyof T, string>>(
|
|
||||||
keys?: K[],
|
|
||||||
): AsyncResult<any, StoreQueryError | NotFoundError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
async () => {
|
|
||||||
const [stmt, params] = getSelectStatement(
|
|
||||||
this.schema[this.query.from]!,
|
|
||||||
{
|
|
||||||
...this.query,
|
|
||||||
keys: keys!,
|
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await this.db.onePrepared(
|
|
||||||
stmt,
|
|
||||||
params,
|
|
||||||
transformRow(this.schema[this.query.from]!),
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
throw new NotFoundError();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
(err) => new StoreQueryError(err.message),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectStatement(
|
export function getSelectStatement(
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
query: StoreQueryDefinition,
|
query: QueryDefinition,
|
||||||
): [string, Record<string, any>] {
|
): [string, Record<string, any>] {
|
||||||
const selectFields = query.keys ? query.keys.join(", ") : "*";
|
const selectFields = query.keys ? query.keys.join(", ") : "*";
|
||||||
|
|
||||||
|
|||||||
@ -26,37 +26,41 @@ describe("State Store", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
store = new SQLiteStateStore(":memory:", models);
|
store = new SQLiteStateStore(":memory:", models);
|
||||||
await store.migrate().orThrow();
|
await Run.UNSAFE(() => store.migrate());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await store.close().orThrow();
|
await Run.UNSAFE(() => store.close());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should insert a record", async () => {
|
test("should insert a record", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Run.UNSAFE(() =>
|
||||||
id: newUUID,
|
store.insertInto("users", {
|
||||||
name: "test",
|
id: newUUID,
|
||||||
streamId: newUUID,
|
name: "test",
|
||||||
streamVersion: 1n,
|
streamId: newUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should select all records", async () => {
|
test("should select all records", async () => {
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Run.UNSAFE(() =>
|
||||||
name: "test",
|
store.insertInto("users", {
|
||||||
id: newUUID,
|
name: "test",
|
||||||
streamId: newUUID,
|
id: newUUID,
|
||||||
streamVersion: 1n,
|
streamId: newUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const result = await store.from("users").select().unwrapOrThrow();
|
const result = await Run.UNSAFE(() => store.from("users").select());
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
{
|
||||||
@ -82,7 +86,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 Run.seqUNSAFE(
|
||||||
() =>
|
() =>
|
||||||
store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
@ -109,12 +113,14 @@ describe("State Store", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await store
|
const result = await Run.UNSAFE(() =>
|
||||||
.from("users")
|
store
|
||||||
.where({
|
.from("users")
|
||||||
name: isLike("te%"),
|
.where({
|
||||||
})
|
name: isLike("te%"),
|
||||||
.select().unwrapOrThrow();
|
})
|
||||||
|
.select()
|
||||||
|
);
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
{
|
||||||
@ -140,20 +146,25 @@ 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 Run.UNSAFE(() =>
|
||||||
name: "test",
|
store.insertInto("users", {
|
||||||
id: newUUID,
|
name: "test",
|
||||||
streamId: newUUID,
|
id: newUUID,
|
||||||
streamVersion: 1n,
|
streamId: newUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await store.update("users", newUUID, {
|
await Run.UNSAFE(() =>
|
||||||
name: "updated",
|
store.update("users", newUUID, {
|
||||||
}).orThrow();
|
name: "updated",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
const result = await Run.UNSAFE(() =>
|
||||||
.unwrapOrThrow();
|
store.from("users").where({ id: newUUID }).selectOne()
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
@ -167,18 +178,21 @@ 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 Run.UNSAFE(() =>
|
||||||
name: "test",
|
store.insertInto("users", {
|
||||||
id: newUUID,
|
name: "test",
|
||||||
streamId: newUUID,
|
id: newUUID,
|
||||||
streamVersion: 1n,
|
streamId: newUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await store.delete("users", newUUID).orThrow();
|
await Run.UNSAFE(() => store.delete("users", newUUID));
|
||||||
|
|
||||||
const result = await store.from("users").where({ id: newUUID }).selectOne()
|
const result = await Run.UNSAFE(() =>
|
||||||
.unwrapOrThrow();
|
store.from("users").where({ id: newUUID }).selectOne()
|
||||||
|
);
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -189,21 +203,25 @@ describe("State Store", () => {
|
|||||||
const newUUID = UUIDGeneratorMock.generate();
|
const newUUID = UUIDGeneratorMock.generate();
|
||||||
const ownerUUID = UUIDGeneratorMock.generate();
|
const ownerUUID = UUIDGeneratorMock.generate();
|
||||||
|
|
||||||
await store.insertInto("users", {
|
await Run.UNSAFE(() =>
|
||||||
id: ownerUUID,
|
store.insertInto("users", {
|
||||||
name: "test",
|
id: ownerUUID,
|
||||||
streamId: ownerUUID,
|
name: "test",
|
||||||
streamVersion: 1n,
|
streamId: ownerUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await store.insertInto("demo", {
|
await Run.UNSAFE(() =>
|
||||||
id: newUUID,
|
store.insertInto("demo", {
|
||||||
value: 1.0,
|
id: newUUID,
|
||||||
owner: ownerUUID,
|
value: 1.0,
|
||||||
streamId: newUUID,
|
owner: ownerUUID,
|
||||||
streamVersion: 1n,
|
streamId: newUUID,
|
||||||
deletedAt: null,
|
streamVersion: 1n,
|
||||||
}).orThrow();
|
deletedAt: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Query } from "@fabric/domain";
|
import { UseCaseDefinition } from "@fabric/domain";
|
||||||
|
|
||||||
export const UseCases = [] as const satisfies Query[];
|
export const UseCases = [] as const satisfies UseCaseDefinition[];
|
||||||
|
|
||||||
export type UseCases = typeof UseCases;
|
export type UseCases = typeof UseCases;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user