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