feat: version 2 beta

This commit is contained in:
2025-04-25 22:39:47 +00:00
commit 1e58359905
75 changed files with 6899 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
import { assertEquals, assertObjectMatch, assertRejects } from "@std/assert";
import { it } from "@std/testing/bdd";
import { EventInsertionError, EventValidationError } from "../../../libraries/errors.ts";
import { makeId } from "../../../libraries/nanoid.ts";
import type { EventStoreFactory } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".addEvent", (getEventStore) => {
it("should throw a 'EventValidationError' when providing bad event data", async () => {
const { store } = await getEventStore();
await assertRejects(
async () =>
store.pushEvent(
store.event({
type: "user:created",
data: {
name: {
given: "John",
familys: "Doe",
},
email: "john.doe@fixture.none",
},
} as any),
),
EventValidationError,
);
});
it("should throw a 'EventInsertionError' on event insertion error", async () => {
const { store } = await getEventStore();
store.events.insert = async () => {
throw new Error("Fake Insert Error");
};
await assertRejects(
async () =>
store.pushEvent(
store.event({
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: { auditor: "foo" },
}),
),
EventInsertionError,
new EventInsertionError().message,
);
});
it("should insert and project 'user:created' event", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const event = store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: { auditor: "foo" },
});
let projectedResult: string = "";
projector.on("user:created", async (record) => {
projectedResult = `${record.data.name?.given} ${record.data.name?.family} | ${record.data.email}`;
});
await store.pushEvent(event);
assertObjectMatch(await store.events.getByStream(stream).then((rows: any) => rows[0]), event);
assertEquals(projectedResult, "John Doe | john.doe@fixture.none");
});
it("should insert 'user:created' and ignore 'project' error", async () => {
const { store, projector } = await getEventStore({
hooks: {
async onError() {
// ...
},
},
});
const stream = makeId();
const event = store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "admin",
},
});
projector.on("user:created", async () => {
throw new Error();
});
await store.pushEvent(event);
assertObjectMatch(await store.events.getByStream(stream).then((rows: any) => rows[0]), event);
});
it("should insert 'user:created' and add it to 'tenant:xyz' relation", async () => {
const { store, projector } = await getEventStore();
const key = `tenant:${makeId()}`;
projector.on("user:created", async ({ stream }) => {
await store.relations.insert(key, stream);
});
await store.pushEvent(
store.event({
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
);
const res1 = await store.getEventsByRelations([key]);
assertEquals(res1.length, 1);
await store.pushEvent(
store.event({
type: "user:created",
data: {
name: {
given: "Jane",
family: "Doe",
},
email: "jane.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
);
const res2 = await store.getEventsByRelations([key]);
assertEquals(res2.length, 2);
});
it("should insert 'user:email-set' and remove it from 'tenant:xyz' relations", async () => {
const { store, projector } = await getEventStore();
const key = `tenant:${makeId()}`;
projector.on("user:created", async ({ stream }) => {
await store.relations.insert(key, stream);
});
projector.on("user:email-set", async ({ stream }) => {
await store.relations.remove(key, stream);
});
await store.pushEvent(
store.event({
stream: "user-1",
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
);
const res1 = await store.getEventsByRelations([key]);
assertEquals(res1.length, 1);
await store.pushEvent(
store.event({
stream: "user-1",
type: "user:email-set",
data: "jane.doe@fixture.none",
meta: {
auditor: "super",
},
}),
);
const res2 = await store.getEventsByRelations([key]);
assertEquals(res2.length, 0);
});
});

View File

@@ -0,0 +1,108 @@
import { assertEquals, assertObjectMatch, assertRejects } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import { EventValidationError } from "../../../mod.ts";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".addSequence", (getEventStore) => {
it("should insert 'user:created', 'user:name:given-set', and 'user:email-set' in a sequence of events", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const events = [
store.event({
stream,
type: "user:created",
data: {
name: {
given: "Jane",
family: "Doe",
},
email: "jane.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
store.event({
stream,
type: "user:name:given-set",
data: "John",
meta: {
auditor: "admin",
},
}),
store.event({
stream,
type: "user:email-set",
data: "john@doe.com",
meta: {
auditor: "admin",
},
}),
];
await store.pushManyEvents(events);
const records = await store.getEventsByStreams([stream]);
assertEquals(records.length, 3);
records.forEach((record, index) => {
assertObjectMatch(record, events[index]);
});
const state = await store.reduce({ name: "user", stream, reducer: userReducer });
assertEquals(state?.name.given, "John");
assertEquals(state?.email, "john@doe.com");
});
it("should not commit any events when insert fails", async () => {
const { store } = await getEventStore();
const stream = nanoid();
await assertRejects(
async () =>
store.pushManyEvents([
store.event({
stream,
type: "user:created",
data: {
name: {
given: "Jane",
family: "Doe",
},
email: "jane.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
store.event({
stream,
type: "user:name:given-set",
data: {
givens: "John",
},
} as any),
store.event({
stream,
type: "user:email-set",
data: "john@doe.com",
meta: {
auditor: "admin",
},
}),
]),
EventValidationError,
);
const records = await store.getEventsByStreams([stream]);
assertEquals(records.length, 0);
});
});

View File

@@ -0,0 +1,91 @@
import { assertEquals, assertNotEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".createSnapshot", (getEventStore) => {
it("should create a new snapshot", async () => {
const { store } = await getEventStore();
const stream = nanoid();
await store.pushEvent(
store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "super",
},
}),
);
await store.pushEvent(
store.event({
stream,
type: "user:email-set",
data: "jane.doe@fixture.none",
meta: {
auditor: "super",
},
}),
);
await store.pushEvent(
store.event({
stream,
type: "user:deactivated",
meta: {
auditor: "super",
},
}),
);
await store.createSnapshot({ name: "user", stream, reducer: userReducer });
const snapshot = await store.snapshots.getByStream("user", stream);
assertNotEquals(snapshot, undefined);
assertObjectMatch(snapshot!.state, {
name: {
given: "John",
family: "Doe",
},
email: "jane.doe@fixture.none",
active: false,
});
await store.pushEvent(
store.event({
stream,
type: "user:activated",
meta: {
auditor: "super",
},
}),
);
const events = await store.events.getByStream(stream, { cursor: snapshot!.cursor });
assertEquals(events.length, 1);
const state = await store.reduce({ name: "user", stream, reducer: userReducer });
assertObjectMatch(state!, {
name: {
given: "John",
family: "Doe",
},
email: "jane.doe@fixture.none",
active: true,
});
});
});

View File

@@ -0,0 +1,25 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import type { EventStoreFactory } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".makeAggregateReducer", (getEventStore) => {
it("should reduce a user", async () => {
const { store } = await getEventStore();
const userA = await store.aggregate("user").create({ given: "John", family: "Doe" }, "john.doe@fixture.none").setGivenName("Jane").save();
await userA.snapshot();
await userA.setFamilyName("Smith").setEmail("jane.smith@fixture.none", "system").save();
const userB = await store.aggregate("user").getById(userA.id);
if (userB === undefined) {
throw new Error("Expected user to exist");
}
assertEquals(userB.fullName(), "Jane Smith");
assertEquals(userB.email, "jane.smith@fixture.none");
});
});

View File

@@ -0,0 +1,89 @@
import { assertEquals, assertLess } from "@std/assert";
import { it } from "@std/testing/bdd";
import { RelationPayload } from "../../../types/adapter.ts";
import type { EventStoreFactory } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".makeEvent", (getEventStore) => {
it("should make and performantly batch insert a list of events directly", async () => {
const { store } = await getEventStore();
const eventsToInsert = [];
const t0 = performance.now();
let count = 10_000;
while (count--) {
eventsToInsert.push(
store.event({
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "system",
},
}),
);
}
const t1 = performance.now();
assertLess((t1 - t0) / 1000, 5);
const t3 = performance.now();
await store.events.insertMany(eventsToInsert);
const t4 = performance.now();
assertLess((t4 - t3) / 1000, 5);
const events = await store.getEvents();
assertEquals(events.length, 10_000);
});
it("should performantly create and remove event relations", async () => {
const { store } = await getEventStore();
const relations: RelationPayload[] = [];
let count = 10_000;
while (count--) {
const event = store.event({
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "system",
},
});
relations.push({ key: `test:xyz`, stream: event.stream });
}
const t0 = performance.now();
await store.relations.insertMany(relations);
const tr0 = (performance.now() - t0) / 1000;
assertEquals((await store.relations.getByKey(`test:xyz`)).length, 10_000);
assertLess(tr0, 5);
const t1 = performance.now();
await store.relations.removeMany(relations);
const tr1 = (performance.now() - t1) / 1000;
assertEquals((await store.relations.getByKey(`test:xyz`)).length, 0);
assertLess(tr1, 10);
});
});

View File

@@ -0,0 +1,120 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import { makeId } from "../../../libraries/nanoid.ts";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userPostReducer } from "../mocks/user-posts-reducer.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".makeReducer", (getEventStore) => {
it("should create a 'user' reducer and only reduce filtered events", async () => {
const { store } = await getEventStore();
const streamA = nanoid();
const streamB = nanoid();
await store.pushEvent(
store.event({
stream: streamA,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "system",
},
}),
);
await store.pushEvent(
store.event({
stream: streamB,
type: "user:created",
data: {
name: {
given: "Peter",
family: "Parker",
},
email: "peter.parker@fixture.none",
},
meta: {
auditor: "system",
},
}),
);
await store.pushEvent(
store.event({
stream: streamA,
type: "user:name:given-set",
data: "Jane",
meta: {
auditor: "system",
},
}),
);
await store.pushEvent(
store.event({
stream: streamA,
type: "user:email-set",
data: "jane.doe@fixture.none",
meta: {
auditor: "system",
},
}),
);
await store.pushEvent(
store.event({
stream: streamB,
type: "user:email-set",
data: "spiderman@fixture.none",
meta: {
auditor: "system",
},
}),
);
const state = await store.reduce({ name: "user", stream: streamA, reducer: userReducer, filter: { types: ["user:created", "user:email-set"] } });
assertEquals(state?.name, { given: "John", family: "Doe" });
assertEquals(state?.email, "jane.doe@fixture.none");
});
it("should create a 'post:count' reducer and retrieve post correct post count", async () => {
const { store, projector } = await getEventStore();
const auditor = nanoid();
projector.on("post:created", async ({ stream, meta: { auditor } }) => {
await store.relations.insert(`user:${auditor}:posts`, stream);
});
const post1 = makeId();
const post2 = makeId();
const post3 = makeId();
await store.pushEvent(store.event({ stream: post1, type: "post:created", data: { title: "Post #1", body: "Sample #1" }, meta: { auditor } }));
await store.pushEvent(store.event({ stream: post2, type: "post:created", data: { title: "Post #2", body: "Sample #2" }, meta: { auditor } }));
await store.pushEvent(store.event({ stream: post2, type: "post:removed", meta: { auditor } }));
await store.pushEvent(store.event({ stream: post3, type: "post:created", data: { title: "Post #3", body: "Sample #3" }, meta: { auditor } }));
const events = await store.getEventsByRelations([`user:${auditor}:posts`]);
assertEquals(events.length, 4);
const state = await store.reduce({ name: "user", relation: `user:${auditor}:posts`, reducer: userPostReducer });
assertEquals(state?.posts, [
{ id: post1, author: auditor },
{ id: post3, author: auditor },
]);
assertEquals(state?.count, 2);
});
});

View File

@@ -0,0 +1,94 @@
import { assertEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { makeId } from "../../../libraries/nanoid.ts";
import type { EventStoreFactory } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>("projector.once", (getEventStore) => {
it("should handle successfull projection", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const event = store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "foo",
},
});
let emailId: string | Error | undefined;
projector.once(
"user:created",
async () => {
return { id: "fake-email-id" };
},
{
async onError({ error }) {
emailId = error as Error;
},
async onSuccess({ data }) {
emailId = data.id;
},
},
);
await store.pushEvent(event);
assertObjectMatch(await store.events.getByStream(stream).then((rows: any) => rows[0]), event);
assertEquals(emailId, "fake-email-id");
});
it("should handle failed projection", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const event = store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "foo",
},
});
let emailId: string | undefined;
projector.once(
"user:created",
async () => {
fakeEmail();
},
{
async onError({ error }) {
emailId = (error as Error).message;
},
async onSuccess() {},
},
);
await store.pushEvent(event);
assertObjectMatch(await store.events.getByStream(stream).then((rows: any) => rows[0]), event);
assertEquals(emailId, "Failed to send email!");
});
});
function fakeEmail() {
throw new Error("Failed to send email!");
}

View File

@@ -0,0 +1,34 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { EventStoreFactory } from "../../mocks/events.ts";
import { describe } from "../../utilities/describe.ts";
export default describe<EventStoreFactory>("relations", (getEventStore) => {
it("should create a new relation", async () => {
const { store } = await getEventStore();
const key = "sample";
const stream = nanoid();
await store.relations.insert(key, stream);
assertEquals(await store.relations.getByKey(key), [stream]);
});
it("should ignore duplicate relations", async () => {
const { store } = await getEventStore();
const key = "sample";
const stream = nanoid();
await store.relations.insertMany([
{ key, stream },
{ key, stream },
]);
await store.relations.insert(key, stream);
assertEquals(await store.relations.getByKey(key), [stream]);
});
});

View File

@@ -0,0 +1,42 @@
import { assertEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".pushAggregate", (getEventStore) => {
it("should successfully commit pending aggregate events to the event store", async () => {
const { store } = await getEventStore();
const user = store
.aggregate("user")
.create({ given: "Jane", family: "Doe" }, "jane.doe@fixture.none")
.setGivenName("John")
.setEmail("john.doe@fixture.none", "admin");
assertEquals(user.toPending().length, 3);
await store.pushAggregate(user);
assertEquals(user.toPending().length, 0);
const records = await store.getEventsByStreams([user.id]);
assertEquals(records.length, 3);
assertObjectMatch(records[0], { stream: user.id, data: { name: { given: "Jane", family: "Doe" }, email: "jane.doe@fixture.none" } });
assertObjectMatch(records[1], { stream: user.id, data: "John" });
assertObjectMatch(records[2], { stream: user.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
const state = await store.reduce({ name: "user", stream: user.id, reducer: userReducer });
assertObjectMatch(state!, {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
});
});
});

View File

@@ -0,0 +1,62 @@
import { assertEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".pushManyAggregates", (getEventStore) => {
it("should successfully commit pending aggregates events to the event store", async () => {
const { store } = await getEventStore();
const userA = store
.aggregate("user")
.create({ given: "Jane", family: "Doe" }, "jane.doe@fixture.none")
.setGivenName("John")
.setEmail("john.doe@fixture.none", "admin");
const userB = store
.aggregate("user")
.create({ given: "Peter", family: "Doe" }, "peter.doe@fixture.none")
.setGivenName("Barry")
.setEmail("barry.doe@fixture.none", "admin");
assertEquals(userA.toPending().length, 3);
assertEquals(userB.toPending().length, 3);
await store.pushManyAggregates([userA, userB]);
assertEquals(userA.toPending().length, 0);
assertEquals(userB.toPending().length, 0);
const records = await store.getEventsByStreams([userA.id, userB.id]);
assertEquals(records.length, 6);
assertObjectMatch(records[0], { stream: userA.id, data: { name: { given: "Jane", family: "Doe" }, email: "jane.doe@fixture.none" } });
assertObjectMatch(records[1], { stream: userA.id, data: "John" });
assertObjectMatch(records[2], { stream: userA.id, data: "john.doe@fixture.none", meta: { auditor: "admin" } });
assertObjectMatch(records[3], { stream: userB.id, data: { name: { given: "Peter", family: "Doe" }, email: "peter.doe@fixture.none" } });
assertObjectMatch(records[4], { stream: userB.id, data: "Barry" });
assertObjectMatch(records[5], { stream: userB.id, data: "barry.doe@fixture.none", meta: { auditor: "admin" } });
const stateA = await store.reduce({ name: "user", stream: userA.id, reducer: userReducer });
const stateB = await store.reduce({ name: "user", stream: userB.id, reducer: userReducer });
assertObjectMatch(stateA!, {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
});
assertObjectMatch(stateB!, {
name: {
given: "Barry",
family: "Doe",
},
email: "barry.doe@fixture.none",
});
});
});

View File

@@ -0,0 +1,103 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { EventStoreFactory } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".reduce", (getEventStore) => {
it("should return reduced state", async () => {
const { store } = await getEventStore();
const stream = nanoid();
await store.pushEvent(
store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "super",
},
}),
);
await store.pushEvent(
store.event({
stream,
type: "user:email-set",
data: "jane.doe@fixture.none",
meta: {
auditor: "super",
},
}),
);
const state = await store.reduce({ name: "user", stream, reducer: userReducer });
assertEquals(state, {
name: { given: "John", family: "Doe" },
email: "jane.doe@fixture.none",
active: true,
posts: { list: [], count: 0 },
});
});
it("should return snapshot if it exists and no new events were found", async () => {
const { store } = await getEventStore();
const stream = nanoid();
await store.pushEvent(
store.event({
stream,
type: "user:created",
data: {
name: {
given: "John",
family: "Doe",
},
email: "john.doe@fixture.none",
},
meta: {
auditor: "super",
},
}),
);
await store.pushEvent(
store.event({
stream,
type: "user:email-set",
data: "jane.doe@fixture.none",
meta: {
auditor: "super",
},
}),
);
await store.createSnapshot({ name: "user", stream, reducer: userReducer });
const state = await store.reduce({ name: "user", stream, reducer: userReducer });
assertEquals(state, {
name: { given: "John", family: "Doe" },
email: "jane.doe@fixture.none",
active: true,
posts: { list: [], count: 0 },
});
});
it("should return undefined if stream does not have events", async () => {
const stream = nanoid();
const { store } = await getEventStore();
const state = await store.reduce({ name: "user", stream, reducer: userReducer });
assertEquals(state, undefined);
});
});

View File

@@ -0,0 +1,94 @@
import { assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import { EventStoreFactory } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
export default describe<EventStoreFactory>(".replayEvents", (getEventStore) => {
it("should replay events", async () => {
const { store, projector } = await getEventStore();
const stream = nanoid();
const record: Record<string, any> = {};
projector.on("user:created", async ({ stream, data: { name, email } }) => {
record[stream] = {
name,
email,
};
});
projector.on("user:name:given-set", async ({ stream, data }) => {
record[stream].name.given = data;
});
projector.on("user:email-set", async ({ stream, data }) => {
record[stream].email = data;
});
await store.pushManyEvents([
store.event({
stream,
type: "user:created",
data: {
name: {
given: "Jane",
family: "Doe",
},
email: "jane.doe@fixture.none",
},
meta: {
auditor: "admin",
},
}),
store.event({
stream,
type: "user:name:given-set",
data: "John",
meta: {
auditor: "admin",
},
}),
store.event({
stream,
type: "user:email-set",
data: "john@doe.com",
meta: {
auditor: "admin",
},
}),
]);
assertObjectMatch(record, {
[stream]: {
name: {
given: "John",
family: "Doe",
},
email: "john@doe.com",
},
});
delete record[stream];
const promises = [];
const records = await store.getEventsByStreams([stream]);
for (const record of records) {
promises.push(projector.push(record, { hydrated: true, outdated: false }));
}
await Promise.all(promises);
assertObjectMatch(record, {
[stream]: {
name: {
given: "John",
family: "Doe",
},
email: "john@doe.com",
},
});
});
});