Compare commits
No commits in common. "4ea00f515b05eddaf620af5712d24424ce6276be" and "d62b58803323953c90c49025d2c7f46517c8c9cb" have entirely different histories.
4ea00f515b
...
d62b588033
10
package.json
10
package.json
@ -9,18 +9,18 @@
|
|||||||
"apps/**/*"
|
"apps/**/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.12.0",
|
"@eslint/js": "^9.10.0",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.10.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.2",
|
||||||
"typescript-eslint": "^8.8.1",
|
"typescript-eslint": "^8.6.0",
|
||||||
"zx": "^8.1.9"
|
"zx": "^8.1.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --fix --report-unused-disable-directives",
|
"lint": "eslint . --fix --report-unused-disable-directives",
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@fabric/core",
|
"name": "@fabric/core",
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@ -11,15 +9,19 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"@types/validator": "^13.12.2",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.1.2"
|
"vitest": "^2.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"dependencies": {
|
||||||
|
"validator": "^13.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { TaggedError } from "../error/tagged-error.js";
|
import { TaggedError } from "../error/tagged-error.js";
|
||||||
import { UnexpectedError } from "../error/unexpected-error.js";
|
import { UnexpectedError } from "../error/unexpected-error.js";
|
||||||
import { MaybePromise } from "../types/maybe-promise.js";
|
|
||||||
import { Result } from "./result.js";
|
import { Result } from "./result.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,7 +14,7 @@ export type AsyncResult<
|
|||||||
|
|
||||||
export namespace AsyncResult {
|
export namespace AsyncResult {
|
||||||
export async function tryFrom<T, TError extends TaggedError>(
|
export async function tryFrom<T, TError extends TaggedError>(
|
||||||
fn: () => MaybePromise<T>,
|
fn: () => Promise<T>,
|
||||||
errorMapper: (error: any) => TError,
|
errorMapper: (error: any) => TError,
|
||||||
): AsyncResult<T, TError> {
|
): AsyncResult<T, TError> {
|
||||||
try {
|
try {
|
||||||
@ -26,8 +25,8 @@ export namespace AsyncResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function from<T>(
|
export async function from<T>(
|
||||||
fn: () => MaybePromise<T>,
|
fn: () => Promise<T>,
|
||||||
): AsyncResult<T, never> {
|
): AsyncResult<T, UnexpectedError> {
|
||||||
return tryFrom(fn, (error) => new UnexpectedError(error) as never);
|
return tryFrom(fn, (error) => new UnexpectedError(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,18 +21,6 @@ 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>,
|
||||||
): AsyncResult<T3, TE1 | TE2 | TE3>;
|
): AsyncResult<T3, TE1 | TE2 | TE3>;
|
||||||
// prettier-ignore
|
|
||||||
export async function seq<
|
|
||||||
T1, TE1 extends TaggedError,
|
|
||||||
T2, TE2 extends TaggedError,
|
|
||||||
T3, TE3 extends TaggedError,
|
|
||||||
T4, TE4 extends TaggedError,
|
|
||||||
>(
|
|
||||||
fn1: () => AsyncResult<T1, TE1>,
|
|
||||||
fn2: (value: T1) => AsyncResult<T2, TE2>,
|
|
||||||
fn3: (value: T2) => AsyncResult<T3, TE3>,
|
|
||||||
fn4: (value: T3) => AsyncResult<T4, TE4>,
|
|
||||||
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
|
|
||||||
export async function seq(
|
export async function seq(
|
||||||
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
|
||||||
): AsyncResult<any, any> {
|
): AsyncResult<any, any> {
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export type EmptyRecord = Record<string, never>;
|
|
||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
exclude: ["**/index.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@fabric/domain",
|
"name": "@fabric/domain",
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@ -12,11 +10,12 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"@fabric/sqlite-store": "workspace:^",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.1.2"
|
"vitest": "^2.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^",
|
||||||
@ -24,7 +23,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,21 @@
|
|||||||
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
|
import { AsyncResult, PosixDate } from "@fabric/core";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
import { UUID } from "../types/uuid.js";
|
import { EventsFromStream, EventStream } from "./event-stream.js";
|
||||||
import { Event, EventFromKey } from "./event.js";
|
|
||||||
import { StoredEvent } from "./stored-event.js";
|
import { StoredEvent } from "./stored-event.js";
|
||||||
|
|
||||||
export interface EventStore<TEvents extends Event> {
|
export interface EventStore<TEventStream extends EventStream> {
|
||||||
/**
|
/**
|
||||||
* Store a new event in the event store.
|
* Store a new event in the event store.
|
||||||
*/
|
*/
|
||||||
append<T extends TEvents>(
|
append<
|
||||||
|
TStreamKey extends TEventStream["name"],
|
||||||
|
T extends EventsFromStream<TEventStream, TStreamKey>,
|
||||||
|
>(
|
||||||
|
streamName: TStreamKey,
|
||||||
event: T,
|
event: T,
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError>;
|
): AsyncResult<StoredEvent<T>, StoreQueryError>;
|
||||||
|
|
||||||
getEventsFromStream(
|
|
||||||
streamId: UUID,
|
|
||||||
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
|
|
||||||
|
|
||||||
subscribe<TEventKey extends TEvents["type"]>(
|
|
||||||
events: TEventKey[],
|
|
||||||
subscriber: EventSubscriber<EventFromKey<TEvents, TEventKey>>,
|
|
||||||
): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventSubscriber<TEvents extends Event = Event> = (
|
|
||||||
event: StoredEvent<TEvents>,
|
|
||||||
) => MaybePromise<void>;
|
|
||||||
|
|
||||||
export interface EventFilterOptions {
|
export interface EventFilterOptions {
|
||||||
fromDate?: PosixDate;
|
fromDate?: PosixDate;
|
||||||
toDate?: PosixDate;
|
toDate?: PosixDate;
|
||||||
|
|||||||
16
packages/fabric/domain/src/events/event-stream.ts
Normal file
16
packages/fabric/domain/src/events/event-stream.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { AsyncResult } from "@fabric/core";
|
||||||
|
import { UUID } from "../types/uuid.js";
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
import { StoredEvent } from "./stored-event.js";
|
||||||
|
|
||||||
|
export interface EventStream<
|
||||||
|
TName extends string = string,
|
||||||
|
TEvent extends Event = Event,
|
||||||
|
> {
|
||||||
|
id: UUID;
|
||||||
|
name: TName;
|
||||||
|
append<T extends TEvent>(event: Event): AsyncResult<StoredEvent<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventsFromStream<T extends EventStream, TKey extends string> =
|
||||||
|
T extends EventStream<TKey, infer TEvent> ? TEvent : never;
|
||||||
@ -10,8 +10,3 @@ export interface Event<TTag extends string = string, TPayload = any> {
|
|||||||
readonly streamId: UUID;
|
readonly streamId: UUID;
|
||||||
readonly payload: TPayload;
|
readonly payload: TPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventFromKey<
|
|
||||||
TEvents extends Event,
|
|
||||||
TKey extends TEvents["type"],
|
|
||||||
> = Extract<TEvents, { type: TKey }>;
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./event-store.js";
|
export * from "./event-store.js";
|
||||||
|
export * from "./event-stream.js";
|
||||||
export * from "./event.js";
|
export * from "./event.js";
|
||||||
export * from "./stored-event.js";
|
export * from "./stored-event.js";
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
import { isRecord } from "@fabric/core";
|
import { isRecord } from "@fabric/core";
|
||||||
|
import validator from "validator";
|
||||||
import { InMemoryFile } from "./in-memory-file.js";
|
import { InMemoryFile } from "./in-memory-file.js";
|
||||||
|
|
||||||
|
const { isBase64, isMimeType } = validator;
|
||||||
|
|
||||||
export function isInMemoryFile(value: unknown): value is InMemoryFile {
|
export function isInMemoryFile(value: unknown): value is InMemoryFile {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
isRecord(value) &&
|
isRecord(value) &&
|
||||||
"data" in value &&
|
"data" in value &&
|
||||||
typeof value.data === "string" &&
|
typeof value.data === "string" &&
|
||||||
|
isBase64(value.data.split(",")[1]) &&
|
||||||
"mimeType" in value &&
|
"mimeType" in value &&
|
||||||
typeof value.mimeType === "string" &&
|
typeof value.mimeType === "string" &&
|
||||||
|
isMimeType(value.mimeType) &&
|
||||||
"name" in value &&
|
"name" in value &&
|
||||||
typeof value.name === "string" &&
|
typeof value.name === "string" &&
|
||||||
"sizeInBytes" in value &&
|
"sizeInBytes" in value &&
|
||||||
|
|||||||
@ -6,4 +6,3 @@ export * from "./security/index.js";
|
|||||||
export * from "./services/index.js";
|
export * from "./services/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./use-case/index.js";
|
export * from "./use-case/index.js";
|
||||||
export * from "./utils/index.js";
|
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./json-utils.js";
|
|
||||||
export * from "./sort-by-dependencies.js";
|
export * from "./sort-by-dependencies.js";
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
exclude: ["**/index.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@fabric/sqlite-store",
|
"name": "@fabric/sqlite-store",
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@ -11,11 +9,11 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"typescript": "^5.6.2",
|
||||||
"typescript": "^5.6.3",
|
"vitest": "^2.1.1"
|
||||||
"vitest": "^2.1.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^",
|
||||||
@ -24,7 +22,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
import { PosixDate, Run } from "@fabric/core";
|
|
||||||
import { Event } from "@fabric/domain";
|
|
||||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vitest } from "vitest";
|
|
||||||
import { SQLiteEventStore } from "./event-store.js";
|
|
||||||
|
|
||||||
describe("Event Store", () => {
|
|
||||||
type UserCreated = Event<"UserCreated", { name: string }>;
|
|
||||||
type UserUpdated = Event<"UserUpdated", { name: string }>;
|
|
||||||
type UserDeleted = Event<"UserDeleted", void>;
|
|
||||||
|
|
||||||
type UserEvents = UserCreated | UserUpdated | UserDeleted;
|
|
||||||
|
|
||||||
let store: SQLiteEventStore<UserEvents>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
store = new SQLiteEventStore(":memory:");
|
|
||||||
await Run.UNSAFE(() => store.migrate());
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await Run.UNSAFE(() => store.close());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should append an event", async () => {
|
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
|
||||||
|
|
||||||
const userCreated: UserCreated = {
|
|
||||||
type: "UserCreated",
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
payload: { name: "test" },
|
|
||||||
};
|
|
||||||
|
|
||||||
await Run.UNSAFE(() => store.append(userCreated));
|
|
||||||
|
|
||||||
const events = await Run.UNSAFE(() => store.getEventsFromStream(newUUID));
|
|
||||||
|
|
||||||
expect(events).toHaveLength(1);
|
|
||||||
|
|
||||||
expect(events[0]).toEqual({
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
type: "UserCreated",
|
|
||||||
version: BigInt(1),
|
|
||||||
timestamp: expect.any(PosixDate),
|
|
||||||
payload: { name: "test" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should notify subscribers on append", async () => {
|
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
|
||||||
|
|
||||||
const userCreated: UserCreated = {
|
|
||||||
type: "UserCreated",
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
payload: { name: "test" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscriber = vitest.fn();
|
|
||||||
|
|
||||||
store.subscribe(["UserCreated"], subscriber);
|
|
||||||
|
|
||||||
await Run.UNSAFE(() => store.append(userCreated));
|
|
||||||
|
|
||||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
|
||||||
expect(subscriber).toHaveBeenCalledWith({
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
type: "UserCreated",
|
|
||||||
version: BigInt(1),
|
|
||||||
timestamp: expect.any(PosixDate),
|
|
||||||
payload: { name: "test" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
import { AsyncResult, MaybePromise, PosixDate, Run } from "@fabric/core";
|
|
||||||
import {
|
|
||||||
Event,
|
|
||||||
EventFromKey,
|
|
||||||
EventStore,
|
|
||||||
EventSubscriber,
|
|
||||||
JSONUtils,
|
|
||||||
StoredEvent,
|
|
||||||
StoreQueryError,
|
|
||||||
UUID,
|
|
||||||
} from "@fabric/domain";
|
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.js";
|
|
||||||
|
|
||||||
export class SQLiteEventStore<TEvents extends Event>
|
|
||||||
implements EventStore<TEvents>
|
|
||||||
{
|
|
||||||
private db: SQLiteDatabase;
|
|
||||||
|
|
||||||
private streamVersions = new Map<UUID, bigint>();
|
|
||||||
|
|
||||||
private eventSubscribers = new Map<
|
|
||||||
TEvents["type"],
|
|
||||||
EventSubscriber<TEvents>[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(private readonly dbPath: string) {
|
|
||||||
this.db = new SQLiteDatabase(dbPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrate(): AsyncResult<void, StoreQueryError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
async () => {
|
|
||||||
await this.db.init();
|
|
||||||
await this.db.run(
|
|
||||||
`CREATE TABLE IF NOT EXISTS events (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
streamId TEXT NOT NULL,
|
|
||||||
version INTEGER NOT NULL,
|
|
||||||
timestamp NUMERIC NOT NULL,
|
|
||||||
payload TEXT NOT NULL,
|
|
||||||
UNIQUE(streamId, version)
|
|
||||||
)`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(error) => new StoreQueryError(error.message, { error }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEventsFromStream(
|
|
||||||
streamId: UUID,
|
|
||||||
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
async () => {
|
|
||||||
const events = await this.db.allPrepared(
|
|
||||||
`SELECT * FROM events WHERE streamId = $id`,
|
|
||||||
{
|
|
||||||
$id: streamId,
|
|
||||||
},
|
|
||||||
(event) => ({
|
|
||||||
id: event.id,
|
|
||||||
streamId: event.streamId,
|
|
||||||
type: event.type,
|
|
||||||
version: BigInt(event.version),
|
|
||||||
timestamp: new PosixDate(event.timestamp),
|
|
||||||
payload: JSONUtils.parse(event.payload),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
(error) => new StoreQueryError(error.message, { error }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async append<T extends TEvents>(
|
|
||||||
event: T,
|
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError> {
|
|
||||||
return Run.seq(
|
|
||||||
() => this.getLastVersion(event.streamId),
|
|
||||||
(version) =>
|
|
||||||
AsyncResult.from(() => {
|
|
||||||
this.streamVersions.set(event.streamId, version + 1n);
|
|
||||||
return version;
|
|
||||||
}),
|
|
||||||
(version) => this.storeEvent(event.streamId, version + 1n, event),
|
|
||||||
(storedEvent) =>
|
|
||||||
AsyncResult.from(async () => {
|
|
||||||
await this.notifySubscribers(storedEvent);
|
|
||||||
return storedEvent;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async notifySubscribers(
|
|
||||||
event: StoredEvent<TEvents>,
|
|
||||||
): AsyncResult<void> {
|
|
||||||
return AsyncResult.from(async () => {
|
|
||||||
const subscribers = this.eventSubscribers.get(event.type) || [];
|
|
||||||
await Promise.all(subscribers.map((subscriber) => subscriber(event)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getLastVersion(
|
|
||||||
streamId: UUID,
|
|
||||||
): AsyncResult<bigint, StoreQueryError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
async () => {
|
|
||||||
const { lastVersion } = await this.db.onePrepared(
|
|
||||||
`SELECT max(version) as lastVersion FROM events WHERE streamId = $id`,
|
|
||||||
{
|
|
||||||
$id: streamId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return !lastVersion ? 0n : BigInt(lastVersion);
|
|
||||||
},
|
|
||||||
(error) =>
|
|
||||||
new StoreQueryError(`Error getting last version:${error.message}`, {
|
|
||||||
error,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe<TEventKey extends TEvents["type"]>(
|
|
||||||
events: TEventKey[],
|
|
||||||
subscriber: (
|
|
||||||
event: StoredEvent<EventFromKey<TEvents, TEventKey>>,
|
|
||||||
) => MaybePromise<void>,
|
|
||||||
): void {
|
|
||||||
events.forEach((event) => {
|
|
||||||
const subscribers = this.eventSubscribers.get(event) || [];
|
|
||||||
const newSubscribers = [
|
|
||||||
...subscribers,
|
|
||||||
subscriber,
|
|
||||||
] as EventSubscriber<TEvents>[];
|
|
||||||
this.eventSubscribers.set(event, newSubscribers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
close(): AsyncResult<void, StoreQueryError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
() => this.db.close(),
|
|
||||||
(error) => new StoreQueryError(error.message, { error }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private storeEvent<T extends Event>(
|
|
||||||
streamId: UUID,
|
|
||||||
version: bigint,
|
|
||||||
event: T,
|
|
||||||
): AsyncResult<StoredEvent<T>, StoreQueryError> {
|
|
||||||
return AsyncResult.tryFrom(
|
|
||||||
async () => {
|
|
||||||
const storedEvent: StoredEvent<T> = {
|
|
||||||
...event,
|
|
||||||
version: version,
|
|
||||||
timestamp: new PosixDate(),
|
|
||||||
};
|
|
||||||
await this.db.runPrepared(
|
|
||||||
`INSERT INTO events (id, streamId, type, version, timestamp, payload)
|
|
||||||
VALUES ($id, $streamId, $type, $version, $timestamp, $payload)`,
|
|
||||||
{
|
|
||||||
$id: storedEvent.id,
|
|
||||||
$streamId: streamId,
|
|
||||||
$type: storedEvent.type,
|
|
||||||
$version: storedEvent.version.toString(),
|
|
||||||
$timestamp: storedEvent.timestamp.timestamp,
|
|
||||||
$payload: JSON.stringify(storedEvent.payload),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return storedEvent;
|
|
||||||
},
|
|
||||||
(error) => new StoreQueryError("Error appending event", { error }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./event-store.js";
|
|
||||||
@ -79,11 +79,7 @@ export class SQLiteDatabase {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
try {
|
|
||||||
resolve(transformer ? rows.map(transformer) : rows);
|
resolve(transformer ? rows.map(transformer) : rows);
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -104,11 +100,7 @@ export class SQLiteDatabase {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
try {
|
|
||||||
resolve(transformer ? rows.map(transformer)[0] : rows[0]);
|
resolve(transformer ? rows.map(transformer)[0] : rows[0]);
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.js";
|
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.js";
|
||||||
import { transformRow } from "../sqlite/sql-to-value.js";
|
import { transformRow } from "../sqlite/sql-to-value.js";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.js";
|
import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js";
|
||||||
|
|
||||||
export class QueryBuilder<T> implements StoreQuery<T> {
|
export class QueryBuilder<T> implements StoreQuery<T> {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
recordToSQLParams,
|
recordToSQLParams,
|
||||||
recordToSQLSet,
|
recordToSQLSet,
|
||||||
} from "../sqlite/record-utils.js";
|
} from "../sqlite/record-utils.js";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.js";
|
import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js";
|
||||||
import { QueryBuilder } from "./query-builder.js";
|
import { QueryBuilder } from "./query-builder.js";
|
||||||
|
|
||||||
export class SQLiteStateStore<TModel extends Model>
|
export class SQLiteStateStore<TModel extends Model>
|
||||||
@ -26,7 +26,7 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dbPath: string,
|
private dbPath: string,
|
||||||
models: TModel[],
|
models: TModel[],
|
||||||
) {
|
) {
|
||||||
this.schema = models.reduce((acc, model: TModel) => {
|
this.schema = models.reduce((acc, model: TModel) => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
exclude: ["**/index.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@ulthar/template-domain",
|
"name": "@ulthar/template-domain",
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"typescript": "^5.6.2",
|
||||||
"typescript": "^5.6.3",
|
"vitest": "^2.1.1"
|
||||||
"vitest": "^2.1.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^",
|
||||||
@ -20,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
exclude: ["**/index.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@ulthar/lib-template",
|
"name": "@ulthar/lib-template",
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@ -11,18 +9,17 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.2",
|
"typescript": "^5.6.2",
|
||||||
"typescript": "^5.6.3",
|
"vitest": "^2.1.1"
|
||||||
"vitest": "^2.1.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^"
|
"@fabric/core": "workspace:^"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
exclude: ["**/index.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user