feat: add supertokens
This commit is contained in:
66
modules/workspace/aggregates/workspace-user.ts
Normal file
66
modules/workspace/aggregates/workspace-user.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
|
||||
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
||||
|
||||
import { db } from "../database.ts";
|
||||
import { EventRecord, EventStoreFactory, projector } from "../event-store.ts";
|
||||
|
||||
export class WorkspaceUser extends AggregateRoot<EventStoreFactory> {
|
||||
static override readonly name = "workspace:user";
|
||||
|
||||
workspaceId!: string;
|
||||
identityId!: string;
|
||||
|
||||
createdAt!: Date;
|
||||
updatedAt?: Date;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reducer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
with(event: EventRecord): void {
|
||||
switch (event.type) {
|
||||
case "workspace:user:created": {
|
||||
this.workspaceId = event.data.workspaceId;
|
||||
this.identityId = event.data.identityId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
create(workspaceId: string, identityId: string, meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:user:created",
|
||||
data: {
|
||||
workspaceId,
|
||||
identityId,
|
||||
},
|
||||
meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Projectors
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
projector.on("workspace:user:created", async ({ stream: id, data: { workspaceId, identityId }, meta, created }) => {
|
||||
await db.collection("workspace:users").insertOne({
|
||||
id,
|
||||
workspaceId,
|
||||
identityId,
|
||||
name: {
|
||||
given: "",
|
||||
family: "",
|
||||
},
|
||||
contacts: [],
|
||||
createdAt: getDate(created),
|
||||
createdBy: meta.user.uid ?? "Unknown",
|
||||
});
|
||||
});
|
||||
120
modules/workspace/aggregates/workspace.ts
Normal file
120
modules/workspace/aggregates/workspace.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
|
||||
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
||||
|
||||
import { db } from "../database.ts";
|
||||
import { EventRecord, EventStoreFactory, projector } from "../event-store.ts";
|
||||
|
||||
export class Workspace extends AggregateRoot<EventStoreFactory> {
|
||||
static override readonly name = "workspace";
|
||||
|
||||
ownerId!: string;
|
||||
|
||||
name!: string;
|
||||
description?: string;
|
||||
archived = false;
|
||||
|
||||
createdAt!: Date;
|
||||
updatedAt?: Date;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Reducer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
with(event: EventRecord): void {
|
||||
switch (event.type) {
|
||||
case "workspace:created": {
|
||||
this.id = event.stream;
|
||||
this.ownerId = event.data.ownerId;
|
||||
this.name = event.data.name;
|
||||
this.createdAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "workspace:name:added": {
|
||||
this.name = event.data;
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "workspace:description:added": {
|
||||
this.description = event.data;
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "workspace:archived": {
|
||||
this.archived = true;
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
case "workspace:restored": {
|
||||
this.archived = false;
|
||||
this.updatedAt = getDate(event.created);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Actions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
create(ownerId: string, name: string, meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:created",
|
||||
data: {
|
||||
ownerId,
|
||||
name,
|
||||
},
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
setName(name: string, meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:name:added",
|
||||
data: name,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
setDescription(description: string, meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:description:added",
|
||||
data: description,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
archive(meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:archived",
|
||||
meta,
|
||||
});
|
||||
}
|
||||
|
||||
restore(meta: AuditActor = auditors.system) {
|
||||
return this.push({
|
||||
stream: this.id,
|
||||
type: "workspace:restored",
|
||||
meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Projectors
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
projector.on("workspace:created", async ({ stream: id, data: { ownerId, name }, meta, created }) => {
|
||||
await db.collection("workspaces").insertOne({
|
||||
id,
|
||||
ownerId,
|
||||
name,
|
||||
createdAt: getDate(created),
|
||||
createdBy: meta.user.uid ?? "Unknown",
|
||||
});
|
||||
});
|
||||
33
modules/workspace/cerbos/policies/workspace.yaml
Normal file
33
modules/workspace/cerbos/policies/workspace.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
|
||||
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
|
||||
|
||||
apiVersion: api.cerbos.dev/v1
|
||||
resourcePolicy:
|
||||
resource: workspace
|
||||
version: default
|
||||
rules:
|
||||
|
||||
- actions: ["create"]
|
||||
effect: EFFECT_ALLOW
|
||||
roles: ["super"]
|
||||
|
||||
- actions: ["read"]
|
||||
effect: EFFECT_ALLOW
|
||||
roles: ["super", "admin", "user"]
|
||||
condition:
|
||||
match:
|
||||
expr: R.attr.id in P.attr.workspaceIds
|
||||
|
||||
- actions: ["update"]
|
||||
effect: EFFECT_ALLOW
|
||||
roles: ["super", "admin"]
|
||||
condition:
|
||||
match:
|
||||
expr: R.attr.id in P.attr.workspaceIds
|
||||
|
||||
- actions: ["delete"]
|
||||
effect: EFFECT_ALLOW
|
||||
roles: ["super"]
|
||||
condition:
|
||||
match:
|
||||
expr: R.attr.id in P.attr.workspaceIds
|
||||
54
modules/workspace/cerbos/policies/workspace_user.yaml
Normal file
54
modules/workspace/cerbos/policies/workspace_user.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
|
||||
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
|
||||
|
||||
apiVersion: api.cerbos.dev/v1
|
||||
resourcePolicy:
|
||||
resource: workspace_user
|
||||
version: default
|
||||
rules:
|
||||
|
||||
# Admins can invite new members into their own workspace
|
||||
|
||||
- actions:
|
||||
- invite
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- admin
|
||||
condition:
|
||||
match:
|
||||
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
|
||||
|
||||
# Admins can remove members from their own workspace
|
||||
|
||||
- actions:
|
||||
- remove
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- admin
|
||||
condition:
|
||||
match:
|
||||
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
|
||||
|
||||
# Admins can update member roles in their own workspace
|
||||
|
||||
- actions:
|
||||
- update_role
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- admin
|
||||
condition:
|
||||
match:
|
||||
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
|
||||
|
||||
# Admins and users can list/read members of their own workspace
|
||||
|
||||
- actions:
|
||||
- list
|
||||
- read
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- admin
|
||||
- user
|
||||
condition:
|
||||
match:
|
||||
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
|
||||
22
modules/workspace/cerbos/resources.ts
Normal file
22
modules/workspace/cerbos/resources.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import z from "zod";
|
||||
|
||||
/*
|
||||
export const resources = new ResourceRegistry([
|
||||
{
|
||||
kind: "workspace",
|
||||
actions: [],
|
||||
attr: {
|
||||
workspaceId: z.string(),
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "workspace_user",
|
||||
actions: [],
|
||||
attr: {
|
||||
workspaceId: z.string(),
|
||||
},
|
||||
},
|
||||
] as const);
|
||||
|
||||
export type Resource = typeof resources.$resource;
|
||||
*/
|
||||
0
modules/workspace/client.ts
Normal file
0
modules/workspace/client.ts
Normal file
27
modules/workspace/database.ts
Normal file
27
modules/workspace/database.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getDatabaseAccessor } from "@platform/database/accessor.ts";
|
||||
|
||||
import { parseWorkspace, type Workspace } from "./models/workspace.ts";
|
||||
import { WorkspaceUser } from "./models/workspace-user.ts";
|
||||
|
||||
export const db = getDatabaseAccessor<{
|
||||
workspaces: Workspace;
|
||||
"workspace:users": WorkspaceUser;
|
||||
}>(`workspace:read-store`);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Identity
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve a single workspace by its primary identifier.
|
||||
*
|
||||
* @param id - Unique identity.
|
||||
*/
|
||||
export async function getWorkspaceById(id: string): Promise<Workspace | undefined> {
|
||||
return db
|
||||
.collection("workspaces")
|
||||
.findOne({ id })
|
||||
.then((document) => parseWorkspace(document));
|
||||
}
|
||||
54
modules/workspace/event-store.ts
Normal file
54
modules/workspace/event-store.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { container } from "@platform/database/container.ts";
|
||||
import { EventFactory, EventStore, Prettify, Projector } from "@valkyr/event-store";
|
||||
import { MongoAdapter } from "@valkyr/event-store/mongo";
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Event Factory
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const eventFactory = new EventFactory([
|
||||
...(await import("./events/workspace.ts")).default,
|
||||
...(await import("./events/workspace-user.ts")).default,
|
||||
]);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Event Store
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const eventStore = new EventStore({
|
||||
adapter: new MongoAdapter(() => container.get("mongo"), `workspace:event-store`),
|
||||
events: eventFactory,
|
||||
snapshot: "auto",
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Projector
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const projector = new Projector<EventStoreFactory>();
|
||||
|
||||
eventStore.onEventsInserted(async (records, { batch }) => {
|
||||
if (batch !== undefined) {
|
||||
await projector.pushMany(batch, records);
|
||||
} else {
|
||||
for (const record of records) {
|
||||
await projector.push(record, { hydrated: false, outdated: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Events
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type EventStoreFactory = typeof eventFactory;
|
||||
|
||||
export type EventRecord = Prettify<EventStoreFactory["$events"][number]["$record"]>;
|
||||
23
modules/workspace/events/workspace-user.ts
Normal file
23
modules/workspace/events/workspace-user.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
|
||||
import { event } from "@valkyr/event-store";
|
||||
import z from "zod";
|
||||
|
||||
import { AvatarSchema } from "../value-objects/avatar.ts";
|
||||
import { ContactSchema } from "../value-objects/contact.ts";
|
||||
import { NameSchema } from "../value-objects/name.ts";
|
||||
|
||||
export default [
|
||||
event
|
||||
.type("workspace:user:created")
|
||||
.data(
|
||||
z.strictObject({
|
||||
workspaceId: z.string(),
|
||||
identityId: z.string(),
|
||||
}),
|
||||
)
|
||||
.meta(AuditActorSchema),
|
||||
event.type("workspace:user:name-set").data(NameSchema).meta(AuditActorSchema),
|
||||
event.type("workspace:user:avatar-set").data(AvatarSchema).meta(AuditActorSchema),
|
||||
event.type("workspace:user:contacts-added").data(z.array(ContactSchema)).meta(AuditActorSchema),
|
||||
event.type("workspace:user:contacts-removed").data(z.array(z.string())).meta(AuditActorSchema),
|
||||
];
|
||||
19
modules/workspace/events/workspace.ts
Normal file
19
modules/workspace/events/workspace.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
|
||||
import { event } from "@valkyr/event-store";
|
||||
import z from "zod";
|
||||
|
||||
export default [
|
||||
event
|
||||
.type("workspace:created")
|
||||
.data(
|
||||
z.strictObject({
|
||||
ownerId: z.uuid(),
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.meta(AuditActorSchema),
|
||||
event.type("workspace:name:added").data(z.string()).meta(AuditActorSchema),
|
||||
event.type("workspace:description:added").data(z.string()).meta(AuditActorSchema),
|
||||
event.type("workspace:archived").meta(AuditActorSchema),
|
||||
event.type("workspace:restored").meta(AuditActorSchema),
|
||||
];
|
||||
38
modules/workspace/models/workspace-user.ts
Normal file
38
modules/workspace/models/workspace-user.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { makeDocumentParser } from "@platform/database/utilities.ts";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AvatarSchema } from "../value-objects/avatar.ts";
|
||||
import { ContactSchema } from "../value-objects/contact.ts";
|
||||
import { NameSchema } from "../value-objects/name.ts";
|
||||
|
||||
export const WorkspaceUserSchema = z.object({
|
||||
id: z.uuid(),
|
||||
|
||||
workspaceId: z.uuid(),
|
||||
identityId: z.string(),
|
||||
|
||||
name: NameSchema.optional(),
|
||||
avatar: AvatarSchema.optional(),
|
||||
contacts: z.array(ContactSchema).default([]),
|
||||
|
||||
createdAt: z.coerce.date(),
|
||||
createdBy: z.string(),
|
||||
updatedAt: z.coerce.date().optional(),
|
||||
updatedBy: z.string().optional(),
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Parsers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const parseWorkspaceUser = makeDocumentParser(WorkspaceUserSchema);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type WorkspaceUser = z.infer<typeof WorkspaceUserSchema>;
|
||||
32
modules/workspace/models/workspace.ts
Normal file
32
modules/workspace/models/workspace.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { makeDocumentParser } from "@platform/database/utilities.ts";
|
||||
import { z } from "zod";
|
||||
|
||||
export const WorkspaceSchema = z.object({
|
||||
id: z.uuid(),
|
||||
|
||||
ownerId: z.uuid(),
|
||||
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
|
||||
createdAt: z.coerce.date(),
|
||||
createdBy: z.string(),
|
||||
updatedAt: z.coerce.date().optional(),
|
||||
updatedBy: z.string().optional(),
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Parsers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export const parseWorkspace = makeDocumentParser(WorkspaceSchema);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type Workspace = z.infer<typeof WorkspaceSchema>;
|
||||
19
modules/workspace/package.json
Normal file
19
modules/workspace/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@modules/workspace",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./client.ts": "./client.ts",
|
||||
"./server.ts": "./server.ts"
|
||||
},
|
||||
"types": "types.d.ts",
|
||||
"dependencies": {
|
||||
"@platform/database": "workspace:*",
|
||||
"@platform/relay": "workspace:*",
|
||||
"@platform/spec": "workspace:*",
|
||||
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
|
||||
"cookie": "1.0.2",
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
}
|
||||
20
modules/workspace/routes/workspaces/create/handle.ts
Normal file
20
modules/workspace/routes/workspaces/create/handle.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ForbiddenError } from "@platform/relay";
|
||||
|
||||
import { Workspace } from "../../../aggregates/workspace.ts";
|
||||
import { eventStore } from "../../../event-store.ts";
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async ({ body: { name } }, { access, principal }) => {
|
||||
const decision = await access.isAllowed({ kind: "workspace", id: "1", attr: {} }, "create");
|
||||
if (decision === false) {
|
||||
return new ForbiddenError("You do not have permission to create workspaces.");
|
||||
}
|
||||
const workspace = await eventStore.aggregate.from(Workspace).create(principal.id, name).save();
|
||||
return {
|
||||
id: workspace.id,
|
||||
ownerId: workspace.ownerId,
|
||||
name: workspace.name,
|
||||
createdAt: workspace.createdAt,
|
||||
createdBy: principal.id,
|
||||
};
|
||||
});
|
||||
14
modules/workspace/routes/workspaces/create/spec.ts
Normal file
14
modules/workspace/routes/workspaces/create/spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ForbiddenError, InternalServerError, route, UnauthorizedError, ValidationError } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
import { WorkspaceSchema } from "../../../models/workspace.ts";
|
||||
|
||||
export default route
|
||||
.post("/api/v1/workspace")
|
||||
.body(
|
||||
z.strictObject({
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.errors([UnauthorizedError, ForbiddenError, ValidationError, InternalServerError])
|
||||
.response(WorkspaceSchema);
|
||||
30
modules/workspace/server.ts
Normal file
30
modules/workspace/server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { idIndex } from "@platform/database/id.ts";
|
||||
import { register as registerReadStore } from "@platform/database/registrar.ts";
|
||||
import { register as registerEventStore } from "@valkyr/event-store/mongo";
|
||||
|
||||
import { db } from "./database.ts";
|
||||
import { eventStore } from "./event-store.ts";
|
||||
|
||||
export default {
|
||||
routes: [(await import("./routes/workspaces/create/handle.ts")).default],
|
||||
|
||||
bootstrap: async (): Promise<void> => {
|
||||
await registerReadStore(db.db, [
|
||||
{
|
||||
name: "workspaces",
|
||||
indexes: [
|
||||
idIndex,
|
||||
// [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "workspace:users",
|
||||
indexes: [
|
||||
idIndex,
|
||||
// [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
|
||||
],
|
||||
},
|
||||
]);
|
||||
await registerEventStore(eventStore.db.db, console.info);
|
||||
},
|
||||
};
|
||||
7
modules/workspace/value-objects/avatar.ts
Normal file
7
modules/workspace/value-objects/avatar.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
export const AvatarSchema = z.object({
|
||||
url: z.string().describe("A valid URL pointing to the user's avatar image."),
|
||||
});
|
||||
|
||||
export type Avatar = z.infer<typeof AvatarSchema>;
|
||||
13
modules/workspace/value-objects/contact.ts
Normal file
13
modules/workspace/value-objects/contact.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import z from "zod";
|
||||
|
||||
import { EmailSchema } from "./email.ts";
|
||||
|
||||
export const ContactSchema = z.union([
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("email"),
|
||||
email: EmailSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type Contact = z.infer<typeof ContactSchema>;
|
||||
11
modules/workspace/value-objects/email.ts
Normal file
11
modules/workspace/value-objects/email.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import z from "zod";
|
||||
|
||||
export const EmailSchema = z.object({
|
||||
type: z.enum(["personal", "work"]).describe("The context of the email address, e.g., personal or work."),
|
||||
value: z.email().describe("A valid email address string."),
|
||||
primary: z.boolean().describe("Indicates if this is the primary email."),
|
||||
verified: z.boolean().describe("True if the email address has been verified."),
|
||||
label: z.string().optional().describe("Optional display label for the email address."),
|
||||
});
|
||||
|
||||
export type Email = z.infer<typeof EmailSchema>;
|
||||
8
modules/workspace/value-objects/name.ts
Normal file
8
modules/workspace/value-objects/name.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const NameSchema = z.object({
|
||||
family: z.string().nullable().describe("Family name, also known as last name or surname."),
|
||||
given: z.string().nullable().describe("Given name, also known as first name."),
|
||||
});
|
||||
|
||||
export type Name = z.infer<typeof NameSchema>;
|
||||
Reference in New Issue
Block a user