feat: refactor account
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { AggregateRoot, getDate, makeAggregateReducer } from "@valkyr/event-store";
|
||||
import { Avatar, Contact, Email, Name, Phone, Strategy } from "relay/schemas";
|
||||
import { Strategy } from "@spec/modules/account/strategies.ts";
|
||||
import { Avatar, Contact, Email, Name } from "@spec/shared";
|
||||
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
||||
|
||||
import { db, toAccountDriver } from "~libraries/read-store/mod.ts";
|
||||
import { db } from "~libraries/read-store/mod.ts";
|
||||
|
||||
import { eventStore } from "../event-store.ts";
|
||||
import { AccountCreatedData } from "../events/account.ts";
|
||||
import { Auditor } from "../events/auditor.ts";
|
||||
import { EventStoreFactory } from "../events/mod.ts";
|
||||
import { projector } from "../projector.ts";
|
||||
@@ -12,69 +12,22 @@ import { projector } from "../projector.ts";
|
||||
export class Account extends AggregateRoot<EventStoreFactory> {
|
||||
static override readonly name = "account";
|
||||
|
||||
id!: string;
|
||||
organizationId?: string;
|
||||
|
||||
type!: "admin" | "consultant" | "organization";
|
||||
|
||||
avatar?: Avatar;
|
||||
name?: Name;
|
||||
contact: Contact = {
|
||||
emails: [],
|
||||
phones: [],
|
||||
};
|
||||
strategies: Strategy[] = [];
|
||||
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Factories
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static #reducer = makeAggregateReducer(Account);
|
||||
|
||||
static create(data: AccountCreatedData, meta: Auditor): Account {
|
||||
return new Account().push({
|
||||
type: "account:created",
|
||||
data,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
static async getById(stream: string): Promise<Account | undefined> {
|
||||
return this.$store.reduce({ name: "account", stream, reducer: this.#reducer });
|
||||
}
|
||||
|
||||
static async getByEmail(email: string): Promise<Account | undefined> {
|
||||
return this.$store.reduce({ name: "account", relation: Account.emailRelation(email), reducer: this.#reducer });
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Relations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static emailRelation(email: string): `account:email:${string}` {
|
||||
return `account:email:${email}`;
|
||||
}
|
||||
|
||||
static passwordRelation(alias: string): `account:password:${string}` {
|
||||
return `account:password:${alias}`;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reducer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
with(event: EventStoreFactory["$events"][number]["$record"]): void {
|
||||
switch (event.type) {
|
||||
case "account:created": {
|
||||
this.id = event.stream;
|
||||
this.organizationId = event.data.type === "organization" ? event.data.organizationId : undefined;
|
||||
this.type = event.data.type;
|
||||
this.createdAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "account:avatar:added": {
|
||||
this.avatar = { url: event.data };
|
||||
this.updatedAt = getDate(event.created);
|
||||
@@ -90,11 +43,6 @@ export class Account extends AggregateRoot<EventStoreFactory> {
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "account:phone:added": {
|
||||
this.contact.phones.push(event.data);
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "strategy:email:added": {
|
||||
this.strategies.push({ type: "email", value: event.data });
|
||||
this.updatedAt = getDate(event.created);
|
||||
@@ -139,15 +87,6 @@ export class Account extends AggregateRoot<EventStoreFactory> {
|
||||
});
|
||||
}
|
||||
|
||||
addPhone(phone: Phone, meta: Auditor): this {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "account:phone:added",
|
||||
data: phone,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
addRole(roleId: string, meta: Auditor): this {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
@@ -174,95 +113,40 @@ export class Account extends AggregateRoot<EventStoreFactory> {
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
toSession(): Session {
|
||||
if (this.type === "organization") {
|
||||
if (this.organizationId === undefined) {
|
||||
throw new Error("Account .toSession failed, no organization id present");
|
||||
}
|
||||
return {
|
||||
type: this.type,
|
||||
accountId: this.id,
|
||||
organizationId: this.organizationId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: this.type,
|
||||
accountId: this.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
type Session =
|
||||
| {
|
||||
type: "organization";
|
||||
accountId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
| {
|
||||
type: "admin" | "consultant";
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Projectors
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
projector.on("account:created", async ({ stream, data }) => {
|
||||
const schema: any = {
|
||||
id: stream,
|
||||
type: data.type,
|
||||
contact: {
|
||||
emails: [],
|
||||
phones: [],
|
||||
},
|
||||
strategies: [],
|
||||
roles: [],
|
||||
};
|
||||
if (data.type === "organization") {
|
||||
schema.organizationId = data.organizationId;
|
||||
}
|
||||
await db.collection("accounts").insertOne(toAccountDriver(schema));
|
||||
});
|
||||
|
||||
projector.on("account:avatar:added", async ({ stream: id, data: url }) => {
|
||||
await db.collection("accounts").updateOne({ id }, { $set: { avatar: { url } } });
|
||||
await db.collection("accounts").updateOne({ id }, { $set: { avatar: { url } } }, { upsert: true });
|
||||
});
|
||||
|
||||
projector.on("account:name:added", async ({ stream: id, data: name }) => {
|
||||
await db.collection("accounts").updateOne({ id }, { $set: { name } });
|
||||
await db.collection("accounts").updateOne({ id }, { $set: { name } }, { upsert: true });
|
||||
});
|
||||
|
||||
projector.on("account:email:added", async ({ stream: id, data: email }) => {
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { "contact.emails": email } });
|
||||
});
|
||||
|
||||
projector.on("account:phone:added", async ({ stream: id, data: phone }) => {
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { "contact.phones": phone } });
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { "contact.emails": email } }, { upsert: true });
|
||||
});
|
||||
|
||||
projector.on("account:role:added", async ({ stream: id, data: roleId }) => {
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { roles: roleId } });
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { roles: roleId } }, { upsert: true });
|
||||
});
|
||||
|
||||
projector.on("strategy:email:added", async ({ stream: id, data: email }) => {
|
||||
await eventStore.relations.insert(Account.emailRelation(email), id);
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { strategies: { type: "email", value: email } } });
|
||||
await eventStore.relations.insert(`account:email:${email}`, id);
|
||||
await db
|
||||
.collection("accounts")
|
||||
.updateOne({ id }, { $push: { strategies: { type: "email", value: email } } }, { upsert: true });
|
||||
});
|
||||
|
||||
projector.on("strategy:password:added", async ({ stream: id, data: strategy }) => {
|
||||
await eventStore.relations.insert(Account.passwordRelation(strategy.alias), id);
|
||||
await db.collection("accounts").updateOne({ id }, { $push: { strategies: { type: "password", ...strategy } } });
|
||||
await eventStore.relations.insert(`account:alias:${strategy.alias}`, id);
|
||||
await db
|
||||
.collection("accounts")
|
||||
.updateOne({ id }, { $push: { strategies: { type: "password", ...strategy } } }, { upsert: true });
|
||||
});
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { AggregateFactory } from "@valkyr/event-store";
|
||||
|
||||
import { Account } from "./account.ts";
|
||||
import { Code } from "./code.ts";
|
||||
import { Organization } from "./organization.ts";
|
||||
import { Role } from "./role.ts";
|
||||
|
||||
export const aggregates = new AggregateFactory([Account, Code, Organization, Role]);
|
||||
@@ -1,29 +1,12 @@
|
||||
import { EmailSchema, NameSchema } from "@spec/shared";
|
||||
import { event } from "@valkyr/event-store";
|
||||
import { email, name, phone } from "relay/schemas";
|
||||
import z from "zod";
|
||||
|
||||
import { auditor } from "./auditor.ts";
|
||||
|
||||
const created = z.discriminatedUnion([
|
||||
z.object({
|
||||
type: z.literal("admin"),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("consultant"),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("organization"),
|
||||
organizationId: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export default [
|
||||
event.type("account:created").data(created).meta(auditor),
|
||||
event.type("account:avatar:added").data(z.string()).meta(auditor),
|
||||
event.type("account:name:added").data(name).meta(auditor),
|
||||
event.type("account:email:added").data(email).meta(auditor),
|
||||
event.type("account:phone:added").data(phone).meta(auditor),
|
||||
event.type("account:name:added").data(NameSchema).meta(auditor),
|
||||
event.type("account:email:added").data(EmailSchema).meta(auditor),
|
||||
event.type("account:role:added").data(z.string()).meta(auditor),
|
||||
];
|
||||
|
||||
export type AccountCreatedData = z.infer<typeof created>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import z from "zod";
|
||||
|
||||
import { auditor } from "./auditor.ts";
|
||||
|
||||
const created = z.object({
|
||||
const CreatedSchema = z.object({
|
||||
name: z.string(),
|
||||
permissions: z.array(
|
||||
z.object({
|
||||
@@ -13,7 +13,7 @@ const created = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
const operation = z.discriminatedUnion([
|
||||
const OperationSchema = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("grant"),
|
||||
resource: z.string(),
|
||||
@@ -27,11 +27,11 @@ const operation = z.discriminatedUnion([
|
||||
]);
|
||||
|
||||
export default [
|
||||
event.type("role:created").data(created).meta(auditor),
|
||||
event.type("role:created").data(CreatedSchema).meta(auditor),
|
||||
event.type("role:name-set").data(z.string()).meta(auditor),
|
||||
event.type("role:permissions-set").data(z.array(operation)).meta(auditor),
|
||||
event.type("role:permissions-set").data(z.array(OperationSchema)).meta(auditor),
|
||||
];
|
||||
|
||||
export type RoleCreatedData = z.infer<typeof created>;
|
||||
export type RoleCreatedData = z.infer<typeof CreatedSchema>;
|
||||
|
||||
export type RolePermissionOperation = z.infer<typeof operation>;
|
||||
export type RolePermissionOperation = z.infer<typeof OperationSchema>;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { db, takeOne } from "../database.ts";
|
||||
import { type AccountSchema, fromAccountDriver } from "./schema.ts";
|
||||
|
||||
export async function getAccountById(id: string): Promise<AccountSchema | undefined> {
|
||||
return db.collection("accounts").find({ id }).toArray().then(fromAccountDriver).then(takeOne);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const account = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.object({
|
||||
given: z.string(),
|
||||
family: z.string(),
|
||||
}),
|
||||
email: z.email(),
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Parsers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const select = account;
|
||||
const insert = account;
|
||||
|
||||
export function toAccountDriver(documents: unknown): AccountInsert {
|
||||
return insert.parse(documents);
|
||||
}
|
||||
|
||||
export function fromAccountDriver(documents: unknown[]): AccountSchema[] {
|
||||
return documents.map((document) => select.parse(document));
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type AccountSchema = z.infer<typeof select>;
|
||||
export type AccountInsert = z.infer<typeof insert>;
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AccountDocument } from "@spec/modules/account/account.ts";
|
||||
|
||||
import { config } from "~config";
|
||||
import { getDatabaseAccessor } from "~libraries/database/accessor.ts";
|
||||
|
||||
import { AccountInsert } from "./account/schema.ts";
|
||||
|
||||
export const db = getDatabaseAccessor<{
|
||||
accounts: AccountInsert;
|
||||
accounts: AccountDocument;
|
||||
}>(`${config.name}:read-store`);
|
||||
|
||||
export function takeOne<TDocument>(documents: TDocument[]): TDocument | undefined {
|
||||
|
||||
35
api/libraries/read-store/methods.ts
Normal file
35
api/libraries/read-store/methods.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { type Account, parseAccount } from "@spec/modules/account/account.ts";
|
||||
|
||||
import { db, takeOne } from "./database.ts";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Accounts
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve a single account by its primary identifier.
|
||||
*
|
||||
* @param id - Account identifier.
|
||||
*/
|
||||
export async function getAccountById(id: string): Promise<Account | undefined> {
|
||||
return db
|
||||
.collection("accounts")
|
||||
.aggregate([
|
||||
{
|
||||
$match: { id },
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "roles",
|
||||
localField: "roles",
|
||||
foreignField: "id",
|
||||
as: "roles",
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray()
|
||||
.then(parseAccount)
|
||||
.then(takeOne);
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./account/methods.ts";
|
||||
export * from "./account/schema.ts";
|
||||
export * from "./database.ts";
|
||||
export * from "./methods.ts";
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@std/fs": "npm:@jsr/std__fs@1",
|
||||
"@std/path": "npm:@jsr/std__path@1",
|
||||
"@valkyr/auth": "npm:@jsr/valkyr__auth@2",
|
||||
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.0-beta.5",
|
||||
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.0-beta.6",
|
||||
"@valkyr/inverse": "npm:@jsr/valkyr__inverse@1",
|
||||
"cookie": "1",
|
||||
"mongodb": "6",
|
||||
|
||||
Reference in New Issue
Block a user