Compare commits
8 Commits
d62b588033
...
4ea00f515b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ea00f515b | |||
| a6a303f256 | |||
| 9a63ba22f1 | |||
| 3afdb5d230 | |||
| b71ecb5de1 | |||
| 4171107227 | |||
| 6b46677be9 | |||
| 559a3f3c22 |
10
package.json
10
package.json
@ -9,18 +9,18 @@
|
|||||||
"apps/**/*"
|
"apps/**/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.10.0",
|
"@eslint/js": "^9.12.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.10.0",
|
"eslint": "^9.12.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.2",
|
"typescript": "^5.6.3",
|
||||||
"typescript-eslint": "^8.6.0",
|
"typescript-eslint": "^8.8.1",
|
||||||
"zx": "^8.1.7"
|
"zx": "^8.1.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --fix --report-unused-disable-directives",
|
"lint": "eslint . --fix --report-unused-disable-directives",
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@ -9,19 +11,15 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/validator": "^13.12.2",
|
"@vitest/coverage-v8": "^2.1.2",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.2"
|
||||||
},
|
},
|
||||||
"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,6 +1,7 @@
|
|||||||
/* 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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +15,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: () => Promise<T>,
|
fn: () => MaybePromise<T>,
|
||||||
errorMapper: (error: any) => TError,
|
errorMapper: (error: any) => TError,
|
||||||
): AsyncResult<T, TError> {
|
): AsyncResult<T, TError> {
|
||||||
try {
|
try {
|
||||||
@ -25,8 +26,8 @@ export namespace AsyncResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function from<T>(
|
export async function from<T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => MaybePromise<T>,
|
||||||
): AsyncResult<T, UnexpectedError> {
|
): AsyncResult<T, never> {
|
||||||
return tryFrom(fn, (error) => new UnexpectedError(error));
|
return tryFrom(fn, (error) => new UnexpectedError(error) as never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,18 @@ 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
packages/fabric/core/src/types/record.ts
Normal file
1
packages/fabric/core/src/types/record.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
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: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@ -10,12 +12,11 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fabric/sqlite-store": "workspace:^",
|
"@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:^",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
"coverage": "vitest run --coverage",
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,31 @@
|
|||||||
import { AsyncResult, PosixDate } from "@fabric/core";
|
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
import { EventsFromStream, EventStream } from "./event-stream.js";
|
import { UUID } from "../types/uuid.js";
|
||||||
|
import { Event, EventFromKey } from "./event.js";
|
||||||
import { StoredEvent } from "./stored-event.js";
|
import { StoredEvent } from "./stored-event.js";
|
||||||
|
|
||||||
export interface EventStore<TEventStream extends EventStream> {
|
export interface EventStore<TEvents extends Event> {
|
||||||
/**
|
/**
|
||||||
* Store a new event in the event store.
|
* Store a new event in the event store.
|
||||||
*/
|
*/
|
||||||
append<
|
append<T extends TEvents>(
|
||||||
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;
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
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,3 +10,8 @@ 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,4 +1,3 @@
|
|||||||
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,19 +1,14 @@
|
|||||||
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,3 +6,4 @@ 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 +1,2 @@
|
|||||||
|
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: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@ -9,11 +11,11 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"@vitest/coverage-v8": "^2.1.2",
|
||||||
"vitest": "^2.1.1"
|
"typescript": "^5.6.3",
|
||||||
|
"vitest": "^2.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^",
|
||||||
@ -22,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
"coverage": "vitest run --coverage",
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
packages/fabric/sqlite-store/src/events/event-store.spec.ts
Normal file
77
packages/fabric/sqlite-store/src/events/event-store.spec.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
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" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
176
packages/fabric/sqlite-store/src/events/event-store.ts
Normal file
176
packages/fabric/sqlite-store/src/events/event-store.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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
packages/fabric/sqlite-store/src/events/index.ts
Normal file
1
packages/fabric/sqlite-store/src/events/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./event-store.js";
|
||||||
@ -79,7 +79,11 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -100,7 +104,11 @@ 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-wrapper.js";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.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-wrapper.js";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.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 dbPath: string,
|
private readonly 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: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"typescript": "^5.6.2",
|
"@vitest/coverage-v8": "^2.1.2",
|
||||||
"vitest": "^2.1.1"
|
"typescript": "^5.6.3",
|
||||||
|
"vitest": "^2.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^",
|
||||||
@ -18,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"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: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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",
|
||||||
@ -9,17 +11,18 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"@vitest/coverage-v8": "^2.1.2",
|
||||||
"vitest": "^2.1.1"
|
"typescript": "^5.6.3",
|
||||||
|
"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: ["**/index.ts"],
|
exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"],
|
||||||
},
|
},
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user