Compare commits
6 Commits
b77ba6dc83
...
4cc3324b46
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cc3324b46 | |||
| ce1f0a04f3 | |||
| 60c7bacfb5 | |||
| ae61c03bb9 | |||
| e92de85fe8 | |||
| 55f6d788db |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -12,9 +12,7 @@
|
|||||||
"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"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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 { Event } from "./event.ts";
|
import type { DomainEvent } from "./event.ts";
|
||||||
import type { StoredEvent } from "./stored-event.ts";
|
import type { StoredEvent } from "./stored-event.ts";
|
||||||
|
|
||||||
export interface EventStore<TEvents extends Event> {
|
export interface EventStore<TEvents extends DomainEvent> {
|
||||||
/**
|
/**
|
||||||
* 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 Event> {
|
|||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSubscriber<TEvents extends Event = Event> = (
|
export type EventSubscriber<TEvents extends DomainEvent = DomainEvent> = (
|
||||||
event: StoredEvent<TEvents>,
|
event: StoredEvent<TEvents>,
|
||||||
) => MaybePromise<void>;
|
) => MaybePromise<void>;
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import type { VariantTag } from "@fabric/core";
|
import type { TaggedVariant, 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 Event<TTag extends string = string, TPayload = any> {
|
export interface DomainEvent<TTag extends string = string, TPayload = any>
|
||||||
readonly [VariantTag]: TTag;
|
extends TaggedVariant<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 Event,
|
TEvents extends DomainEvent,
|
||||||
TKey extends TEvents[VariantTag],
|
TKey extends TEvents[VariantTag],
|
||||||
> = Extract<TEvents, { [VariantTag]: TKey }>;
|
> = Extract<TEvents, { [VariantTag]: TKey }>;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import type { PosixDate } from "@fabric/core";
|
import type { PosixDate } from "@fabric/core";
|
||||||
import type { Event } from "./event.ts";
|
import type { DomainEvent } 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 Event> = TEvent & {
|
export type StoredEvent<TEvent extends DomainEvent> = TEvent & {
|
||||||
readonly version: bigint;
|
readonly version: bigint;
|
||||||
readonly timestamp: PosixDate;
|
readonly timestamp: PosixDate;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -28,6 +28,8 @@ 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> {
|
||||||
@ -48,6 +50,8 @@ 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> {
|
||||||
@ -67,6 +71,8 @@ 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> {
|
||||||
@ -84,6 +90,8 @@ 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> {
|
||||||
@ -100,3 +108,9 @@ export class NotFoundError extends TaggedError<"NotFoundError"> {
|
|||||||
super("NotFoundError");
|
super("NotFoundError");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AlreadyExistsError extends TaggedError<"AlreadyExistsError"> {
|
||||||
|
constructor() {
|
||||||
|
super("AlreadyExistsError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import type { VariantTag } from "@fabric/core";
|
import type { VariantTag } from "@fabric/core";
|
||||||
import type { Event } from "../events/event.ts";
|
import type { DomainEvent } 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 Event,
|
TEvents extends DomainEvent,
|
||||||
> {
|
> {
|
||||||
model: TModel;
|
model: TModel;
|
||||||
events: TEvents[VariantTag][];
|
events: TEvents[VariantTag][];
|
||||||
|
|||||||
@ -1,18 +1,24 @@
|
|||||||
// 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 Event = any,
|
TEvent extends DomainEvent = any,
|
||||||
TErrors extends TaggedError<string> = any,
|
TErrors extends TaggedError<string> = any,
|
||||||
> = BasicCommandDefinition<TDependencies, TPayload, TEvent, TErrors>;
|
> = BasicCommandDefinition<
|
||||||
|
TDependencies,
|
||||||
|
TPayload,
|
||||||
|
TEvent,
|
||||||
|
TErrors
|
||||||
|
>;
|
||||||
|
|
||||||
interface BasicCommandDefinition<
|
interface BasicCommandDefinition<
|
||||||
TDependencies,
|
TDependencies,
|
||||||
TPayload,
|
TPayload,
|
||||||
TEvent extends Event,
|
TEvent extends DomainEvent,
|
||||||
TErrors extends TaggedError<string>,
|
TErrors extends TaggedError<string>,
|
||||||
> {
|
> {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,7 +7,12 @@ export type Query<
|
|||||||
TPayload = any,
|
TPayload = any,
|
||||||
TOutput = any,
|
TOutput = any,
|
||||||
TErrors extends TaggedError<string> = any,
|
TErrors extends TaggedError<string> = any,
|
||||||
> = BasicQueryDefinition<TDependencies, TPayload, TOutput, TErrors>;
|
> = BasicQueryDefinition<
|
||||||
|
TDependencies,
|
||||||
|
TPayload,
|
||||||
|
TOutput,
|
||||||
|
TErrors
|
||||||
|
>;
|
||||||
|
|
||||||
interface BasicQueryDefinition<
|
interface BasicQueryDefinition<
|
||||||
TDependencies,
|
TDependencies,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PosixDate } from "@fabric/core";
|
import { PosixDate } from "@fabric/core";
|
||||||
import { Event } from "@fabric/domain";
|
import { DomainEvent } 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 = Event<"UserCreated", { name: string }>;
|
type UserCreated = DomainEvent<"UserCreated", { name: string }>;
|
||||||
type UserUpdated = Event<"UserUpdated", { name: string }>;
|
type UserUpdated = DomainEvent<"UserUpdated", { name: string }>;
|
||||||
type UserDeleted = Event<"UserDeleted", void>;
|
type UserDeleted = DomainEvent<"UserDeleted", void>;
|
||||||
|
|
||||||
type UserEvents = UserCreated | UserUpdated | UserDeleted;
|
type UserEvents = UserCreated | UserUpdated | UserDeleted;
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
VariantTag,
|
VariantTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
Event,
|
DomainEvent,
|
||||||
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 Event>
|
export class SQLiteEventStore<TEvents extends DomainEvent>
|
||||||
implements EventStore<TEvents> {
|
implements EventStore<TEvents> {
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export class SQLiteEventStore<TEvents extends Event>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeEvent<T extends Event>(
|
private storeEvent<T extends DomainEvent>(
|
||||||
streamId: UUID,
|
streamId: UUID,
|
||||||
version: bigint,
|
version: bigint,
|
||||||
event: T,
|
event: T,
|
||||||
|
|||||||
101
packages/fabric/sqlite-store/state/query-builder.test.ts
Normal file
101
packages/fabric/sqlite-store/state/query-builder.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
// 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,
|
||||||
@ -112,18 +113,43 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const result = await this.db.onePrepared(
|
return await this.db.onePrepared(
|
||||||
stmt,
|
stmt,
|
||||||
params,
|
params,
|
||||||
transformRow(this.schema[this.query.from]!),
|
transformRow(this.schema[this.query.from]!),
|
||||||
);
|
);
|
||||||
if (!result) {
|
|
||||||
throw new NotFoundError();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
(err) => new StoreQueryError(err.message),
|
(err) => new StoreQueryError(err.message),
|
||||||
|
).flatMap((result) => {
|
||||||
|
if (!result) {
|
||||||
|
return AsyncResult.failWith(new NotFoundError());
|
||||||
|
}
|
||||||
|
return AsyncResult.ok(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),
|
||||||
|
).flatMap((result) => {
|
||||||
|
if (result) {
|
||||||
|
return AsyncResult.failWith(new AlreadyExistsError());
|
||||||
|
}
|
||||||
|
return AsyncResult.ok();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user