Compare commits

..

No commits in common. "4cc3324b46cd3774ad7b0daec695f148c03af8db" and "b77ba6dc838579adbb57de7c2df2e3d24ab4ef4b" have entirely different histories.

12 changed files with 30 additions and 180 deletions

View File

@ -12,7 +12,9 @@
"deno.enable": true, "deno.enable": true,
"editor.defaultFormatter": "denoland.vscode-deno", "editor.defaultFormatter": "denoland.vscode-deno",
"deno.future": true, "deno.future": true,
"deno.codeLens.implementations": true,
"deno.codeLens.references": true, "deno.codeLens.references": true,
"typescript.referencesCodeLens.enabled": true,
"deno.testing.args": [ "deno.testing.args": [
"--allow-all" "--allow-all"
], ],

View File

@ -7,10 +7,10 @@ import type {
VariantTag, VariantTag,
} from "@fabric/core"; } from "@fabric/core";
import type { StoreQueryError } from "../errors/query-error.ts"; import type { StoreQueryError } from "../errors/query-error.ts";
import type { DomainEvent } from "./event.ts"; import type { Event } from "./event.ts";
import type { StoredEvent } from "./stored-event.ts"; import type { StoredEvent } from "./stored-event.ts";
export interface EventStore<TEvents extends DomainEvent> { export interface EventStore<TEvents extends Event> {
/** /**
* Store a new event in the event store. * Store a new event in the event store.
*/ */
@ -28,7 +28,7 @@ export interface EventStore<TEvents extends DomainEvent> {
): void; ): void;
} }
export type EventSubscriber<TEvents extends DomainEvent = DomainEvent> = ( export type EventSubscriber<TEvents extends Event = Event> = (
event: StoredEvent<TEvents>, event: StoredEvent<TEvents>,
) => MaybePromise<void>; ) => MaybePromise<void>;

View File

@ -1,18 +1,18 @@
// deno-lint-ignore-file no-explicit-any // deno-lint-ignore-file no-explicit-any
import type { TaggedVariant, VariantTag } from "@fabric/core"; import type { VariantTag } from "@fabric/core";
import type { UUID } from "../../core/types/uuid.ts"; import type { UUID } from "../../core/types/uuid.ts";
/** /**
* An event is a tagged variant with a payload and a timestamp. * An event is a tagged variant with a payload and a timestamp.
*/ */
export interface DomainEvent<TTag extends string = string, TPayload = any> export interface Event<TTag extends string = string, TPayload = any> {
extends TaggedVariant<TTag> { readonly [VariantTag]: TTag;
readonly id: UUID; readonly id: UUID;
readonly streamId: UUID; readonly streamId: UUID;
readonly payload: TPayload; readonly payload: TPayload;
} }
export type EventFromKey< export type EventFromKey<
TEvents extends DomainEvent, TEvents extends Event,
TKey extends TEvents[VariantTag], TKey extends TEvents[VariantTag],
> = Extract<TEvents, { [VariantTag]: TKey }>; > = Extract<TEvents, { [VariantTag]: TKey }>;

View File

@ -1,10 +1,10 @@
import type { PosixDate } from "@fabric/core"; import type { PosixDate } from "@fabric/core";
import type { DomainEvent } from "./event.ts"; import type { Event } from "./event.ts";
/** /**
* A stored event is an inmutable event, already stored, with it's version in the stream and timestamp. * A stored event is an inmutable event, already stored, with it's version in the stream and timestamp.
*/ */
export type StoredEvent<TEvent extends DomainEvent> = TEvent & { export type StoredEvent<TEvent extends Event> = TEvent & {
readonly version: bigint; readonly version: bigint;
readonly timestamp: PosixDate; readonly timestamp: PosixDate;
}; };

View File

@ -28,8 +28,6 @@ export interface StoreQuery<T> {
selectOneOrFail<K extends Keyof<T>>( selectOneOrFail<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>; ): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
} }
export interface StoreSortableQuery<T> { export interface StoreSortableQuery<T> {
@ -50,8 +48,6 @@ export interface StoreSortableQuery<T> {
selectOneOrFail<K extends Keyof<T>>( selectOneOrFail<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>; ): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
} }
export interface StoreLimitableQuery<T> { export interface StoreLimitableQuery<T> {
@ -71,8 +67,6 @@ export interface StoreLimitableQuery<T> {
selectOneOrFail<K extends Keyof<T>>( selectOneOrFail<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>; ): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
} }
export interface SelectableQuery<T> { export interface SelectableQuery<T> {
@ -90,8 +84,6 @@ export interface SelectableQuery<T> {
selectOneOrFail<K extends Keyof<T>>( selectOneOrFail<K extends Keyof<T>>(
keys: K[], keys: K[],
): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>; ): AsyncResult<Pick<T, K>, StoreQueryError | NotFoundError>;
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError>;
} }
export interface StoreQueryDefinition<K extends string = string> { export interface StoreQueryDefinition<K extends string = string> {
@ -108,9 +100,3 @@ export class NotFoundError extends TaggedError<"NotFoundError"> {
super("NotFoundError"); super("NotFoundError");
} }
} }
export class AlreadyExistsError extends TaggedError<"AlreadyExistsError"> {
constructor() {
super("AlreadyExistsError");
}
}

View File

@ -1,11 +1,11 @@
import type { VariantTag } from "@fabric/core"; import type { VariantTag } from "@fabric/core";
import type { DomainEvent } from "../events/event.ts"; import type { Event } from "../events/event.ts";
import type { StoredEvent } from "../events/stored-event.ts"; import type { StoredEvent } from "../events/stored-event.ts";
import type { AggregateModel, ModelToType } from "../models/model.ts"; import type { AggregateModel, ModelToType } from "../models/model.ts";
export interface Projection< export interface Projection<
TModel extends AggregateModel, TModel extends AggregateModel,
TEvents extends DomainEvent, TEvents extends Event,
> { > {
model: TModel; model: TModel;
events: TEvents[VariantTag][]; events: TEvents[VariantTag][];

View File

@ -1,24 +1,18 @@
// deno-lint-ignore-file no-explicit-any // deno-lint-ignore-file no-explicit-any
import type { TaggedError } from "@fabric/core"; import type { TaggedError } from "@fabric/core";
import type { DomainEvent } from "../events/event.ts";
import type { UseCase } from "./use-case.ts"; import type { UseCase } from "./use-case.ts";
export type Command< export type Command<
TDependencies = any, TDependencies = any,
TPayload = any, TPayload = any,
TEvent extends DomainEvent = any, TEvent extends Event = any,
TErrors extends TaggedError<string> = any, TErrors extends TaggedError<string> = any,
> = BasicCommandDefinition< > = BasicCommandDefinition<TDependencies, TPayload, TEvent, TErrors>;
TDependencies,
TPayload,
TEvent,
TErrors
>;
interface BasicCommandDefinition< interface BasicCommandDefinition<
TDependencies, TDependencies,
TPayload, TPayload,
TEvent extends DomainEvent, TEvent extends Event,
TErrors extends TaggedError<string>, TErrors extends TaggedError<string>,
> { > {
/** /**

View File

@ -7,12 +7,7 @@ export type Query<
TPayload = any, TPayload = any,
TOutput = any, TOutput = any,
TErrors extends TaggedError<string> = any, TErrors extends TaggedError<string> = any,
> = BasicQueryDefinition< > = BasicQueryDefinition<TDependencies, TPayload, TOutput, TErrors>;
TDependencies,
TPayload,
TOutput,
TErrors
>;
interface BasicQueryDefinition< interface BasicQueryDefinition<
TDependencies, TDependencies,

View File

@ -1,5 +1,5 @@
import { PosixDate } from "@fabric/core"; import { PosixDate } from "@fabric/core";
import { DomainEvent } from "@fabric/domain"; import { Event } from "@fabric/domain";
import { UUIDGeneratorMock } from "@fabric/domain/mocks"; import { UUIDGeneratorMock } from "@fabric/domain/mocks";
import { import {
afterEach, afterEach,
@ -12,9 +12,9 @@ import {
import { SQLiteEventStore } from "./event-store.ts"; import { SQLiteEventStore } from "./event-store.ts";
describe("Event Store", () => { describe("Event Store", () => {
type UserCreated = DomainEvent<"UserCreated", { name: string }>; type UserCreated = Event<"UserCreated", { name: string }>;
type UserUpdated = DomainEvent<"UserUpdated", { name: string }>; type UserUpdated = Event<"UserUpdated", { name: string }>;
type UserDeleted = DomainEvent<"UserDeleted", void>; type UserDeleted = Event<"UserDeleted", void>;
type UserEvents = UserCreated | UserUpdated | UserDeleted; type UserEvents = UserCreated | UserUpdated | UserDeleted;

View File

@ -8,7 +8,7 @@ import {
VariantTag, VariantTag,
} from "@fabric/core"; } from "@fabric/core";
import { import {
DomainEvent, Event,
EventFromKey, EventFromKey,
EventStore, EventStore,
EventSubscriber, EventSubscriber,
@ -17,7 +17,7 @@ import {
} from "@fabric/domain"; } from "@fabric/domain";
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts"; import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
export class SQLiteEventStore<TEvents extends DomainEvent> export class SQLiteEventStore<TEvents extends Event>
implements EventStore<TEvents> { implements EventStore<TEvents> {
private db: SQLiteDatabase; private db: SQLiteDatabase;
@ -139,7 +139,7 @@ export class SQLiteEventStore<TEvents extends DomainEvent>
); );
} }
private storeEvent<T extends DomainEvent>( private storeEvent<T extends Event>(
streamId: UUID, streamId: UUID,
version: bigint, version: bigint,
event: T, event: T,

View File

@ -1,101 +0,0 @@
import {
AlreadyExistsError,
Field,
isLike,
Model,
NotFoundError,
} from "@fabric/domain";
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
import { afterEach, beforeEach, describe, expect, test } from "@fabric/testing";
import { SQLiteStateStore } from "./state-store.ts";
describe("QueryBuilder", () => {
const models = [
Model.entityFrom("test", {
name: Field.string(),
}),
];
let stateStore = new SQLiteStateStore(":memory:", models);
beforeEach(async () => {
stateStore = new SQLiteStateStore(":memory:", models);
await stateStore.migrate().unwrapOrThrow();
await stateStore.insertInto("test", {
id: UUIDGeneratorMock.generate(),
name: "test1",
}).unwrapOrThrow();
await stateStore.insertInto("test", {
id: UUIDGeneratorMock.generate(),
name: "test2",
}).unwrapOrThrow();
});
afterEach(async () => {
await stateStore.close().unwrapOrThrow();
});
test("select() after a where() should return valid results", async () => {
const result = await stateStore.from("test").where({
name: isLike("test%"),
})
.select().unwrapOrThrow();
expect(result).toEqual([{
id: expect.any(String),
name: "test1",
}, {
id: expect.any(String),
name: "test2",
}]);
});
test("selectOneOrFail() should return a single result", async () => {
const result = await stateStore.from("test").where({ name: "test1" })
.selectOneOrFail().unwrapOrThrow();
expect(result).toEqual({
id: expect.any(String),
name: "test1",
});
});
test("selectOneOrFail() should fail if no results are found", async () => {
const error = await stateStore.from("test").where({ name: "not-found" })
.selectOneOrFail().unwrapErrorOrThrow();
expect(error).toBeInstanceOf(NotFoundError);
});
test("selectOne() should return a single result", async () => {
const result = await stateStore.from("test")
.selectOne().unwrapOrThrow();
expect(result).toEqual({
id: expect.any(String),
name: "test1",
});
});
test("selectOne() should return undefined if no results are found", async () => {
const result = await stateStore.from("test").where({
name: "not-found",
})
.selectOne().unwrapOrThrow();
expect(result).toBeUndefined();
});
test("assertNone() should succeed if no results are found", async () => {
const result = await stateStore.from("test").where({
name: "not-found",
}).assertNone().unwrapOrThrow();
expect(result).toBeUndefined();
});
test("assertNone() should fail if results are found", async () => {
const error = await stateStore.from("test").where({ name: "test1" })
.assertNone().unwrapErrorOrThrow();
expect(error).toBeInstanceOf(AlreadyExistsError);
});
});

View File

@ -1,7 +1,6 @@
// deno-lint-ignore-file no-explicit-any // deno-lint-ignore-file no-explicit-any
import { AsyncResult, Keyof, Optional } from "@fabric/core"; import { AsyncResult, Keyof, Optional } from "@fabric/core";
import { import {
AlreadyExistsError,
FilterOptions, FilterOptions,
Model, Model,
type ModelSchema, type ModelSchema,
@ -113,43 +112,18 @@ export class QueryBuilder<T> implements StoreQuery<T> {
limit: 1, limit: 1,
}, },
); );
return await this.db.onePrepared( const result = await this.db.onePrepared(
stmt, stmt,
params, params,
transformRow(this.schema[this.query.from]!), transformRow(this.schema[this.query.from]!),
); );
},
(err) => new StoreQueryError(err.message),
).flatMap((result) => {
if (!result) { if (!result) {
return AsyncResult.failWith(new NotFoundError()); throw new NotFoundError();
} }
return AsyncResult.ok(result); return result;
});
}
assertNone(): AsyncResult<void, StoreQueryError | AlreadyExistsError> {
return AsyncResult.tryFrom(
async () => {
const [stmt, params] = getSelectStatement(
this.schema[this.query.from]!,
{
...this.query,
limit: 1,
},
);
return await this.db.onePrepared(
stmt,
params,
);
}, },
(err) => new StoreQueryError(err.message), (err) => new StoreQueryError(err.message),
).flatMap((result) => { );
if (result) {
return AsyncResult.failWith(new AlreadyExistsError());
}
return AsyncResult.ok();
});
} }
} }