feat: set beta version

This commit is contained in:
2025-04-25 22:44:38 +00:00
parent 1e58359905
commit 56d9037e33
25 changed files with 87 additions and 78 deletions

134
tests/mocks/aggregates.ts Normal file
View File

@@ -0,0 +1,134 @@
import { AggregateRoot } from "../../libraries/aggregate.ts";
import { AggregateFactory } from "../../libraries/aggregate-factory.ts";
import { makeId } from "../../libraries/nanoid.ts";
import { makeAggregateReducer } from "../../libraries/reducer.ts";
import { EventStoreFactory } from "./events.ts";
export class User extends AggregateRoot<EventStoreFactory> {
static override readonly name = "user";
id: string = "";
name: Name = {
given: "",
family: "",
};
email: string = "";
active: boolean = true;
posts: UserPosts = {
list: [],
count: 0,
};
// -------------------------------------------------------------------------
// Factories
// -------------------------------------------------------------------------
static reducer = makeAggregateReducer(User);
static create(name: Name, email: string): User {
const user = new User();
user.push({
type: "user:created",
stream: makeId(),
data: { name, email },
meta: { auditor: "foo" },
});
return user;
}
static async getById(userId: string): Promise<User | undefined> {
return this.$store.reduce({ name: "user", stream: userId, reducer: this.reducer });
}
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
with(event: EventStoreFactory["$events"][number]["$record"]) {
switch (event.type) {
case "user:created": {
this.id = event.stream;
this.name.given = event.data.name?.given ?? "";
this.name.family = event.data.name?.family ?? "";
this.email = event.data.email;
break;
}
case "user:name:given-set": {
this.name.given = event.data;
break;
}
case "user:name:family-set": {
this.name.family = event.data;
break;
}
case "user:email-set": {
this.email = event.data;
break;
}
case "user:activated": {
this.active = true;
break;
}
case "user:deactivated": {
this.active = false;
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
setGivenName(given: string): this {
return this.push({
type: "user:name:given-set",
stream: this.id,
data: given,
meta: { auditor: "foo" },
});
}
setFamilyName(family: string): this {
return this.push({
type: "user:name:family-set",
stream: this.id,
data: family,
meta: { auditor: "foo" },
});
}
setEmail(email: string, auditor: string): this {
return this.push({
type: "user:email-set",
stream: this.id,
data: email,
meta: { auditor },
});
}
async snapshot(): Promise<this> {
await this.$store.createSnapshot({ name: "user", stream: this.id, reducer: User.reducer });
return this;
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
fullName(): string {
return `${this.name.given} ${this.name.family}`;
}
}
export const aggregates = new AggregateFactory([User]);
type Name = {
given: string;
family: string;
};
type UserPosts = {
list: string[];
count: number;
};

19
tests/mocks/errors.ts Normal file
View File

@@ -0,0 +1,19 @@
export abstract class ServiceError<TData = unknown> extends Error {
constructor(message: string, readonly status: number, readonly data?: TData) {
super(message);
}
toJSON() {
return {
status: this.status,
message: this.message,
data: this.data,
};
}
}
export class CustomServiceError<TData = unknown> extends ServiceError<TData> {
constructor(message = "Custom Error", data?: TData) {
super(message, 400, data);
}
}

32
tests/mocks/events.ts Normal file
View File

@@ -0,0 +1,32 @@
import z from "zod";
import { event } from "../../libraries/event.ts";
import { EventFactory } from "../../libraries/event-factory.ts";
export const auditor = z.strictObject({ auditor: z.string() });
export const events = new EventFactory([
event
.type("user:created")
.data(
z.strictObject({
name: z
.union([z.strictObject({ given: z.string(), family: z.string().optional() }), z.strictObject({ given: z.string().optional(), family: z.string() })])
.optional(),
email: z.string(),
}),
)
.meta(auditor),
event.type("user:name:given-set").data(z.string()).meta(auditor),
event.type("user:name:family-set").data(z.string()).meta(auditor),
event.type("user:email-set").data(z.email()).meta(auditor),
event.type("user:activated").meta(auditor),
event.type("user:deactivated").meta(auditor),
event
.type("post:created")
.data(z.strictObject({ title: z.string(), body: z.string() }))
.meta(auditor),
event.type("post:removed").meta(auditor),
]);
export type EventStoreFactory = typeof events;

View File

@@ -0,0 +1,32 @@
import { makeReducer } from "../../libraries/reducer.ts";
import { EventStoreFactory } from "./events.ts";
export const userPostReducer = makeReducer<EventStoreFactory, UserPostState>(
(state, event) => {
switch (event.type) {
case "post:created": {
state.posts.push({ id: event.stream, author: event.meta.auditor });
state.count += 1;
break;
}
case "post:removed": {
state.posts = state.posts.filter(({ id }) => id !== event.stream);
state.count -= 1;
break;
}
}
return state;
},
() => ({
posts: [],
count: 0,
}),
);
type UserPostState = {
posts: {
id: string;
author: string;
}[];
count: number;
};

View File

@@ -0,0 +1,61 @@
import { makeReducer } from "../../libraries/reducer.ts";
import { EventStoreFactory } from "./events.ts";
export const userReducer = makeReducer<EventStoreFactory, UserState>(
(state, event) => {
switch (event.type) {
case "user:created": {
state.name.given = event.data.name?.given ?? "";
state.name.family = event.data.name?.family ?? "";
state.email = event.data.email;
break;
}
case "user:name:given-set": {
state.name.given = event.data;
break;
}
case "user:name:family-set": {
state.name.family = event.data;
break;
}
case "user:email-set": {
state.email = event.data;
break;
}
case "user:activated": {
state.active = true;
break;
}
case "user:deactivated": {
state.active = false;
break;
}
}
return state;
},
() => ({
name: {
given: "",
family: "",
},
email: "",
active: true,
posts: {
list: [],
count: 0,
},
}),
);
type UserState = {
name: {
given: string;
family: string;
};
email: string;
active: boolean;
posts: {
list: string[];
count: number;
};
};