Template
1
0

feat: modular domain driven boilerplate

This commit is contained in:
2025-09-22 01:29:55 +02:00
parent 2433f59d1a
commit 9be3230c84
160 changed files with 2468 additions and 1525 deletions

View File

@@ -0,0 +1,66 @@
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { EventRecord, EventStoreFactory } from "../event-store.ts";
import { CodeIdentity } from "../events/code.ts";
export class Code extends AggregateRoot<EventStoreFactory> {
static override readonly name = "code";
identity!: CodeIdentity;
value!: string;
createdAt!: Date;
claimedAt?: Date;
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
get isClaimed(): boolean {
return this.claimedAt !== undefined;
}
// -------------------------------------------------------------------------
// Folder
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "code:created": {
this.value = event.data.value;
this.identity = event.data.identity;
this.createdAt = getDate(event.created);
break;
}
case "code:claimed": {
this.claimedAt = getDate(event.created);
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(identity: CodeIdentity): this {
return this.push({
type: "code:created",
stream: this.id,
data: {
identity,
value: crypto
.getRandomValues(new Uint8Array(5))
.map((v) => v % 10)
.join(""),
},
});
}
claim(): this {
return this.push({
type: "code:claimed",
stream: this.id,
});
}
}

View File

@@ -0,0 +1,211 @@
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { db } from "../database.ts";
import { type EventRecord, eventStore, type EventStoreFactory, projector } from "../event-store.ts";
import type { Avatar } from "../schemas/avatar.ts";
import type { Contact } from "../schemas/contact.ts";
import type { Email } from "../schemas/email.ts";
import type { Name } from "../schemas/name.ts";
import type { Role } from "../schemas/role.ts";
import type { Strategy } from "../schemas/strategies.ts";
export class Identity extends AggregateRoot<EventStoreFactory> {
static override readonly name = "identity";
avatar?: Avatar;
name?: Name;
contact: Contact = {
emails: [],
};
strategies: Strategy[] = [];
roles: Role[] = [];
createdAt!: Date;
updatedAt!: Date;
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "identity:created": {
this.id = event.stream;
this.createdAt = getDate(event.created);
break;
}
case "identity:avatar:added": {
this.avatar = { url: event.data };
this.updatedAt = getDate(event.created);
break;
}
case "identity:name:added": {
this.name = event.data;
this.updatedAt = getDate(event.created);
break;
}
case "identity:email:added": {
this.contact.emails.push(event.data);
this.updatedAt = getDate(event.created);
break;
}
case "identity:role:added": {
this.roles.push(event.data);
this.updatedAt = getDate(event.created);
break;
}
case "identity:strategy:email:added": {
this.strategies.push({ type: "email", value: event.data });
this.updatedAt = getDate(event.created);
break;
}
case "identity:strategy:password:added": {
this.strategies.push({ type: "password", ...event.data });
this.updatedAt = getDate(event.created);
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "identity:created",
meta,
});
}
addAvatar(url: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:avatar:added",
data: url,
meta,
});
}
addName(name: Name, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:name:added",
data: name,
meta,
});
}
addEmail(email: Email, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:email:added",
data: email,
meta,
});
}
addRole(role: Role, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:role:added",
data: role,
meta,
});
}
addEmailStrategy(email: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:strategy:email:added",
data: email,
meta,
});
}
addPasswordStrategy(alias: string, password: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:strategy:password:added",
data: { alias, password },
meta,
});
}
}
/*
|--------------------------------------------------------------------------------
| Utilities
|--------------------------------------------------------------------------------
*/
export async function isEmailClaimed(email: string): Promise<boolean> {
const relations = await eventStore.relations.getByKey(getIdentityEmailRelation(email));
if (relations.length > 0) {
return true;
}
return false;
}
/*
|--------------------------------------------------------------------------------
| Relations
|--------------------------------------------------------------------------------
*/
export function getIdentityEmailRelation(email: string): string {
return `/identities/emails/${email}`;
}
export function getIdentityAliasRelation(alias: string): string {
return `/identities/aliases/${alias}`;
}
/*
|--------------------------------------------------------------------------------
| Projectors
|--------------------------------------------------------------------------------
*/
projector.on("identity:created", async ({ stream: id }) => {
await db.collection("identities").insertOne({
id,
name: {
given: null,
family: null,
},
contact: {
emails: [],
},
strategies: [],
roles: [],
});
});
projector.on("identity:avatar:added", async ({ stream: id, data: url }) => {
await db.collection("identities").updateOne({ id }, { $set: { avatar: { url } } });
});
projector.on("identity:name:added", async ({ stream: id, data: name }) => {
await db.collection("identities").updateOne({ id }, { $set: { name } });
});
projector.on("identity:email:added", async ({ stream: id, data: email }) => {
await db.collection("identities").updateOne({ id }, { $push: { "contact.emails": email } });
});
projector.on("identity:role:added", async ({ stream: id, data: role }) => {
await db.collection("identities").updateOne({ id }, { $push: { roles: role } });
});
projector.on("identity:strategy:email:added", async ({ stream: id, data: email }) => {
await eventStore.relations.insert(getIdentityEmailRelation(email), id);
await db.collection("identities").updateOne({ id }, { $push: { strategies: { type: "email", value: email } } });
});
projector.on("identity:strategy:password:added", async ({ stream: id, data: strategy }) => {
await eventStore.relations.insert(getIdentityAliasRelation(strategy.alias), id);
await db.collection("identities").updateOne({ id }, { $push: { strategies: { type: "password", ...strategy } } });
});