Compare commits

..

No commits in common. "8c6f043f865b9894e545d9e79a7c5294b3397e99" and "4ea00f515b05eddaf620af5712d24424ce6276be" have entirely different histories.

20 changed files with 93 additions and 116 deletions

View File

@ -3,15 +3,14 @@ import { TaggedVariant, VariantTag } from "../variant/index.js";
/** /**
* A TaggedError is a tagged variant with an error message. * A TaggedError is a tagged variant with an error message.
*/ */
export abstract class TaggedError<Tag extends string = string> export class TaggedError<Tag extends string = string>
extends Error extends Error
implements TaggedVariant<Tag> implements TaggedVariant<Tag>
{ {
readonly [VariantTag]: Tag; readonly [VariantTag]: Tag;
constructor(tag: Tag, message?: string) { constructor(tag: Tag) {
super(message); super();
this[VariantTag] = tag; this[VariantTag] = tag;
this.name = tag;
} }
} }

View File

@ -8,7 +8,12 @@ import { TaggedError } from "./tagged-error.js";
* we must be prepared to handle. * we must be prepared to handle.
*/ */
export class UnexpectedError extends TaggedError<"UnexpectedError"> { export class UnexpectedError extends TaggedError<"UnexpectedError"> {
constructor(message?: string) { constructor(readonly context: Record<string, unknown> = {}) {
super("UnexpectedError", message); super("UnexpectedError");
this.message = "An unexpected error occurred";
}
toString() {
return `UnexpectedError: ${this.message}\n${JSON.stringify(this.context, null, 2)}`;
} }
} }

View File

@ -5,5 +5,4 @@ export * from "./result/index.js";
export * from "./run/index.js"; export * from "./run/index.js";
export * from "./time/index.js"; export * from "./time/index.js";
export * from "./types/index.js"; export * from "./types/index.js";
export * from "./utils/index.js";
export * from "./variant/index.js"; export * from "./variant/index.js";

View File

@ -3,4 +3,3 @@ export * from "./fn.js";
export * from "./keyof.js"; export * from "./keyof.js";
export * from "./maybe-promise.js"; export * from "./maybe-promise.js";
export * from "./optional.js"; export * from "./optional.js";
export * from "./record.js";

View File

@ -1,8 +0,0 @@
import { UnexpectedError } from "../error/unexpected-error.js";
export function ensureValue<T>(value?: T): T {
if (!value) {
throw new UnexpectedError("Value is undefined");
}
return value;
}

View File

@ -1 +0,0 @@
export * from "./ensure-value.js";

View File

@ -1,21 +1,17 @@
import { Fn } from "../types/fn.js"; import { Fn } from "../types/fn.js";
import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js"; import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js";
export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = { export type VariantMatcher<TVariant extends TaggedVariant<string>> = {
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>; [K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>>;
}; };
export function match<const TVariant extends TaggedVariant<string>>( export function match<const TVariant extends TaggedVariant<string>>(
v: TVariant, v: TVariant,
) { ) {
return { return {
case< case<const TMatcher extends VariantMatcher<TVariant>>(
const TReturnType, cases: TMatcher,
const TMatcher extends VariantMatcher< ): ReturnType<TMatcher[TVariant[VariantTag]]> {
TVariant,
TReturnType
> = VariantMatcher<TVariant, TReturnType>,
>(cases: TMatcher): TReturnType {
if (!(v[VariantTag] in cases)) { if (!(v[VariantTag] in cases)) {
throw new Error("Non-exhaustive pattern match"); throw new Error("Non-exhaustive pattern match");
} }

View File

@ -7,7 +7,7 @@ export interface TaggedVariant<TTag extends string> {
export type VariantFromTag< export type VariantFromTag<
TVariant extends TaggedVariant<string>, TVariant extends TaggedVariant<string>,
TTag extends TVariant[VariantTag], TTag extends TVariant[typeof VariantTag],
> = Extract<TVariant, { [VariantTag]: TTag }>; > = Extract<TVariant, { [VariantTag]: TTag }>;
export namespace Variant { export namespace Variant {

View File

@ -1,7 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "@fabric/core"; import { TaggedError } from "@fabric/core";
export class StoreQueryError extends TaggedError<"StoreQueryError"> { export class StoreQueryError extends TaggedError<"StoreQueryError"> {
constructor(public message: string) { constructor(
super("StoreQueryError", message); public message: string,
public context: any,
) {
super("StoreQueryError");
} }
} }

View File

@ -1,13 +1,7 @@
import { import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
AsyncResult,
MaybePromise,
PosixDate,
VariantFromTag,
VariantTag,
} from "@fabric/core";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
import { Event } from "./event.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<TEvents extends Event> {
@ -22,9 +16,9 @@ export interface EventStore<TEvents extends Event> {
streamId: UUID, streamId: UUID,
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>; ): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
subscribe<TEventKey extends TEvents[VariantTag]>( subscribe<TEventKey extends TEvents["type"]>(
events: TEventKey[], events: TEventKey[],
subscriber: EventSubscriber<VariantFromTag<TEvents, TEventKey>>, subscriber: EventSubscriber<EventFromKey<TEvents, TEventKey>>,
): void; ): void;
} }

View File

@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { VariantTag } from "@fabric/core";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
/** /**
* 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 Event<TTag extends string = string, TPayload = any> {
readonly [VariantTag]: TTag; readonly type: TTag;
readonly id: UUID; readonly id: UUID;
readonly streamId: UUID; readonly streamId: UUID;
readonly payload: TPayload; readonly payload: TPayload;
@ -14,5 +13,5 @@ export interface Event<TTag extends string = string, TPayload = any> {
export type EventFromKey< export type EventFromKey<
TEvents extends Event, TEvents extends Event,
TKey extends TEvents[VariantTag], TKey extends TEvents["type"],
> = Extract<TEvents, { [VariantTag]: TKey }>; > = Extract<TEvents, { type: TKey }>;

View File

@ -19,7 +19,6 @@ export const DefaultModelFields = {
isUnsigned: true, isUnsigned: true,
hasArbitraryPrecision: true, hasArbitraryPrecision: true,
}), }),
deletedAt: Field.timestamp({ isOptional: true }),
}; };
export interface Model< export interface Model<

View File

@ -1,13 +0,0 @@
import { VariantTag } from "@fabric/core";
import { Event } from "../events/event.js";
import { StoredEvent } from "../events/stored-event.js";
import { Model, ModelToType } from "../models/model.js";
export interface Projection<TModel extends Model, TEvents extends Event> {
model: TModel;
events: TEvents[VariantTag][];
projection: (
event: StoredEvent<TEvents>,
model?: ModelToType<TModel>,
) => ModelToType<TModel>;
}

View File

@ -26,7 +26,7 @@ describe("Event Store", () => {
const newUUID = UUIDGeneratorMock.generate(); const newUUID = UUIDGeneratorMock.generate();
const userCreated: UserCreated = { const userCreated: UserCreated = {
_tag: "UserCreated", type: "UserCreated",
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
payload: { name: "test" }, payload: { name: "test" },
@ -41,7 +41,7 @@ describe("Event Store", () => {
expect(events[0]).toEqual({ expect(events[0]).toEqual({
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
_tag: "UserCreated", type: "UserCreated",
version: BigInt(1), version: BigInt(1),
timestamp: expect.any(PosixDate), timestamp: expect.any(PosixDate),
payload: { name: "test" }, payload: { name: "test" },
@ -52,7 +52,7 @@ describe("Event Store", () => {
const newUUID = UUIDGeneratorMock.generate(); const newUUID = UUIDGeneratorMock.generate();
const userCreated: UserCreated = { const userCreated: UserCreated = {
_tag: "UserCreated", type: "UserCreated",
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
payload: { name: "test" }, payload: { name: "test" },
@ -68,7 +68,7 @@ describe("Event Store", () => {
expect(subscriber).toHaveBeenCalledWith({ expect(subscriber).toHaveBeenCalledWith({
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
_tag: "UserCreated", type: "UserCreated",
version: BigInt(1), version: BigInt(1),
timestamp: expect.any(PosixDate), timestamp: expect.any(PosixDate),
payload: { name: "test" }, payload: { name: "test" },

View File

@ -1,10 +1,4 @@
import { import { AsyncResult, MaybePromise, PosixDate, Run } from "@fabric/core";
AsyncResult,
MaybePromise,
PosixDate,
Run,
VariantTag,
} from "@fabric/core";
import { import {
Event, Event,
EventFromKey, EventFromKey,
@ -25,7 +19,7 @@ export class SQLiteEventStore<TEvents extends Event>
private streamVersions = new Map<UUID, bigint>(); private streamVersions = new Map<UUID, bigint>();
private eventSubscribers = new Map< private eventSubscribers = new Map<
TEvents[VariantTag], TEvents["type"],
EventSubscriber<TEvents>[] EventSubscriber<TEvents>[]
>(); >();
@ -40,7 +34,7 @@ export class SQLiteEventStore<TEvents extends Event>
await this.db.run( await this.db.run(
`CREATE TABLE IF NOT EXISTS events ( `CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
_tag TEXT NOT NULL, type TEXT NOT NULL,
streamId TEXT NOT NULL, streamId TEXT NOT NULL,
version INTEGER NOT NULL, version INTEGER NOT NULL,
timestamp NUMERIC NOT NULL, timestamp NUMERIC NOT NULL,
@ -49,7 +43,7 @@ export class SQLiteEventStore<TEvents extends Event>
)`, )`,
); );
}, },
(error) => new StoreQueryError(error.message), (error) => new StoreQueryError(error.message, { error }),
); );
} }
@ -63,18 +57,18 @@ export class SQLiteEventStore<TEvents extends Event>
{ {
$id: streamId, $id: streamId,
}, },
(e) => ({ (event) => ({
id: e.id, id: event.id,
streamId: e.streamId, streamId: event.streamId,
_tag: e._tag, type: event.type,
version: BigInt(e.version), version: BigInt(event.version),
timestamp: new PosixDate(e.timestamp), timestamp: new PosixDate(event.timestamp),
payload: JSONUtils.parse(e.payload), payload: JSONUtils.parse(event.payload),
}), }),
); );
return events; return events;
}, },
(error) => new StoreQueryError(error.message), (error) => new StoreQueryError(error.message, { error }),
); );
} }
@ -101,7 +95,7 @@ export class SQLiteEventStore<TEvents extends Event>
event: StoredEvent<TEvents>, event: StoredEvent<TEvents>,
): AsyncResult<void> { ): AsyncResult<void> {
return AsyncResult.from(async () => { return AsyncResult.from(async () => {
const subscribers = this.eventSubscribers.get(event[VariantTag]) || []; const subscribers = this.eventSubscribers.get(event.type) || [];
await Promise.all(subscribers.map((subscriber) => subscriber(event))); await Promise.all(subscribers.map((subscriber) => subscriber(event)));
}); });
} }
@ -120,17 +114,20 @@ export class SQLiteEventStore<TEvents extends Event>
return !lastVersion ? 0n : BigInt(lastVersion); return !lastVersion ? 0n : BigInt(lastVersion);
}, },
(error) => new StoreQueryError(error.message), (error) =>
new StoreQueryError(`Error getting last version:${error.message}`, {
error,
}),
); );
} }
subscribe<TEventKey extends TEvents[VariantTag]>( subscribe<TEventKey extends TEvents["type"]>(
eventNames: TEventKey[], events: TEventKey[],
subscriber: ( subscriber: (
event: StoredEvent<EventFromKey<TEvents, TEventKey>>, event: StoredEvent<EventFromKey<TEvents, TEventKey>>,
) => MaybePromise<void>, ) => MaybePromise<void>,
): void { ): void {
eventNames.forEach((event) => { events.forEach((event) => {
const subscribers = this.eventSubscribers.get(event) || []; const subscribers = this.eventSubscribers.get(event) || [];
const newSubscribers = [ const newSubscribers = [
...subscribers, ...subscribers,
@ -143,7 +140,7 @@ export class SQLiteEventStore<TEvents extends Event>
close(): AsyncResult<void, StoreQueryError> { close(): AsyncResult<void, StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
() => this.db.close(), () => this.db.close(),
(error) => new StoreQueryError(error.message), (error) => new StoreQueryError(error.message, { error }),
); );
} }
@ -160,12 +157,12 @@ export class SQLiteEventStore<TEvents extends Event>
timestamp: new PosixDate(), timestamp: new PosixDate(),
}; };
await this.db.runPrepared( await this.db.runPrepared(
`INSERT INTO events (id, streamId, _tag, version, timestamp, payload) `INSERT INTO events (id, streamId, type, version, timestamp, payload)
VALUES ($id, $streamId, $_tag, $version, $timestamp, $payload)`, VALUES ($id, $streamId, $type, $version, $timestamp, $payload)`,
{ {
$id: storedEvent.id, $id: storedEvent.id,
$streamId: streamId, $streamId: streamId,
$_tag: storedEvent[VariantTag], $type: storedEvent.type,
$version: storedEvent.version.toString(), $version: storedEvent.version.toString(),
$timestamp: storedEvent.timestamp.timestamp, $timestamp: storedEvent.timestamp.timestamp,
$payload: JSON.stringify(storedEvent.payload), $payload: JSON.stringify(storedEvent.payload),
@ -173,7 +170,7 @@ export class SQLiteEventStore<TEvents extends Event>
); );
return storedEvent; return storedEvent;
}, },
(error) => new StoreQueryError(error.message), (error) => new StoreQueryError("Error appending event", { error }),
); );
} }
} }

View File

@ -14,9 +14,6 @@ export function transformRow(model: Collection) {
} }
function valueFromSQL(field: FieldDefinition, value: any): any { function valueFromSQL(field: FieldDefinition, value: any): any {
if (value === null) {
return null;
}
const r = FieldSQLInsertMap[field[VariantTag]]; const r = FieldSQLInsertMap[field[VariantTag]];
return r(field as any, value); return r(field as any, value);
} }

View File

@ -25,9 +25,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
}; };
export function fieldValueToSQL(field: FieldDefinition, value: any) { export function fieldValueToSQL(field: FieldDefinition, value: any) {
if (value === null) {
return null;
}
const r = FieldSQLInsertMap[field[VariantTag]] as any; const r = FieldSQLInsertMap[field[VariantTag]] as any;
return r(field as any, value); return r(field as any, value);
} }

View File

@ -62,7 +62,11 @@ export class QueryBuilder<T> implements StoreQuery<T> {
transformRow(this.schema[this.query.from]), transformRow(this.schema[this.query.from]),
); );
}, },
(err) => new StoreQueryError(err.message), (err) =>
new StoreQueryError(err.message, {
err,
query: this.query,
}),
); );
} }
@ -87,7 +91,11 @@ export class QueryBuilder<T> implements StoreQuery<T> {
transformRow(this.schema[this.query.from]), transformRow(this.schema[this.query.from]),
); );
}, },
(err) => new StoreQueryError(err.message), (err) =>
new StoreQueryError(err.message, {
err,
query: this.query,
}),
); );
} }
} }

View File

@ -1,4 +1,4 @@
import { PosixDate, Run } from "@fabric/core"; import { Run } from "@fabric/core";
import { defineModel, Field, isLike, UUID } from "@fabric/domain"; import { defineModel, Field, isLike, UUID } from "@fabric/domain";
import { UUIDGeneratorMock } from "@fabric/domain/mocks"; import { UUIDGeneratorMock } from "@fabric/domain/mocks";
import { import {
@ -42,7 +42,6 @@ describe("State Store", () => {
name: "test", name: "test",
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
}); });
@ -56,7 +55,6 @@ describe("State Store", () => {
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
@ -68,7 +66,6 @@ describe("State Store", () => {
streamId: UUID; streamId: UUID;
streamVersion: bigint; streamVersion: bigint;
name: string; name: string;
deletedAt: PosixDate | null;
}[] }[]
>(); >();
@ -78,7 +75,6 @@ describe("State Store", () => {
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
name: "test", name: "test",
deletedAt: null,
}, },
]); ]);
}); });
@ -93,7 +89,6 @@ describe("State Store", () => {
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
() => () =>
store.insertInto("users", { store.insertInto("users", {
@ -101,7 +96,6 @@ describe("State Store", () => {
id: UUIDGeneratorMock.generate(), id: UUIDGeneratorMock.generate(),
streamId: UUIDGeneratorMock.generate(), streamId: UUIDGeneratorMock.generate(),
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
() => () =>
store.insertInto("users", { store.insertInto("users", {
@ -109,7 +103,6 @@ describe("State Store", () => {
id: UUIDGeneratorMock.generate(), id: UUIDGeneratorMock.generate(),
streamId: UUIDGeneratorMock.generate(), streamId: UUIDGeneratorMock.generate(),
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
@ -128,7 +121,6 @@ describe("State Store", () => {
streamId: UUID; streamId: UUID;
streamVersion: bigint; streamVersion: bigint;
name: string; name: string;
deletedAt: PosixDate | null;
}[] }[]
>(); >();
@ -138,7 +130,6 @@ describe("State Store", () => {
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
name: "test", name: "test",
deletedAt: null,
}, },
]); ]);
}); });
@ -152,7 +143,6 @@ describe("State Store", () => {
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
@ -171,7 +161,6 @@ describe("State Store", () => {
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
name: "updated", name: "updated",
deletedAt: null,
}); });
}); });
@ -184,7 +173,6 @@ describe("State Store", () => {
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
@ -209,7 +197,6 @@ describe("State Store", () => {
name: "test", name: "test",
streamId: ownerUUID, streamId: ownerUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
@ -220,7 +207,6 @@ describe("State Store", () => {
owner: ownerUUID, owner: ownerUUID,
streamId: newUUID, streamId: newUUID,
streamVersion: 1n, streamVersion: 1n,
deletedAt: null,
}), }),
); );
}); });

View File

@ -52,7 +52,12 @@ export class SQLiteStateStore<TModel extends Model>
recordToSQLParams(model, record), recordToSQLParams(model, record),
); );
}, },
(error) => new StoreQueryError(error.message), (error) =>
new StoreQueryError(error.message, {
error,
collectionName: model.name,
record,
}),
); );
} }
@ -82,7 +87,12 @@ export class SQLiteStateStore<TModel extends Model>
params, params,
); );
}, },
(error) => new StoreQueryError(error.message), (error) =>
new StoreQueryError(error.message, {
error,
collectionName: model.name,
record,
}),
); );
} }
@ -99,7 +109,12 @@ export class SQLiteStateStore<TModel extends Model>
{ $id: id }, { $id: id },
); );
}, },
(error) => new StoreQueryError(error.message), (error) =>
new StoreQueryError(error.message, {
error,
collectionName: model.name,
id,
}),
); );
} }
@ -115,7 +130,11 @@ export class SQLiteStateStore<TModel extends Model>
} }
}); });
}, },
(error) => new StoreQueryError(error.message), (error) =>
new StoreQueryError(error.message, {
error,
schema: this.schema,
}),
); );
} }