Skip to content

Commit a413c5b

Browse files
feat: enhance generateId() by allowing type specification and export ReadonlyEntity type
1 parent e017177 commit a413c5b

6 files changed

Lines changed: 118 additions & 12 deletions

File tree

.changeset/some-pumas-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ventyd": patch
3+
---
4+
5+
feat: enhance `generateId()` by allowing type specification and export `ReadonlyEntity` type

src/Entity.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export function Entity<$$Schema extends DefaultSchema>(
7777

7878
const entityName = schema[" $$entityName"] as $$EntityName;
7979
const initialEventName = schema[" $$initialEventName"] as $$InitialEventName;
80-
const generateId = schema[" $$generateId"] as () => string;
80+
const generateId = schema[" $$generateId"] as (
81+
type: "eventId" | "entityId",
82+
) => string;
8183

8284
// options
8385
const maxQueuedEvents = options?.maxQueuedEvents ?? 10000; // Default to 10000 events
@@ -119,7 +121,7 @@ export function Entity<$$Schema extends DefaultSchema>(
119121
constructor(args: EntityConstructorArgs<$$Schema>) {
120122
switch (args.type) {
121123
case "create": {
122-
this.entityId = args.entityId ?? generateId();
124+
this.entityId = args.entityId ?? generateId("entityId");
123125
type EventName = InferEventNameFromSchema<$$Schema>;
124126
type EventBody = InferEventBodyFromSchema<$$Schema, EventName>;
125127

@@ -295,7 +297,7 @@ export function Entity<$$Schema extends DefaultSchema>(
295297
},
296298
) {
297299
return {
298-
eventId: options?.eventId ?? generateId(),
300+
eventId: options?.eventId ?? generateId("eventId"),
299301
eventCreatedAt:
300302
options?.eventCreatedAt ?? this[" $$now"]().toISOString(),
301303
eventName,

src/defineSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export function defineSchema<
9696
options: {
9797
schema: SchemaInput<$$EntityName, $$EventType, $$StateType>;
9898
initialEventName: $$InitialEventName;
99-
generateId?: () => string;
99+
generateId?: (type: "eventId" | "entityId") => string;
100100
},
101101
): Schema<$$EntityName, $$EventType, $$StateType, $$InitialEventName> {
102102
const generateId = options.generateId ?? defaultGenerateId;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
InferSchemaFromEntityConstructor,
1818
InferStateFromSchema,
1919
Plugin,
20+
ReadonlyEntity,
2021
Reducer,
2122
Repository,
2223
Schema,

src/types/Schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type Schema<
1313
> = ReturnType<SchemaInput<EntityName, EventType, StateType>> & {
1414
" $$entityName": EntityName;
1515
" $$initialEventName": InitialEventName;
16-
" $$generateId": () => string;
16+
" $$generateId": (type: "eventId" | "entityId") => string;
1717
};
1818

1919
/**

test/entity.spec.ts

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import * as v from "valibot";
55
import { describe, expect, test } from "vitest";
6-
import { defineReducer, defineSchema, Entity } from "../src";
6+
import { defineReducer, defineSchema, Entity, mutation } from "../src";
77
import { valibot } from "../src/valibot"; // Use 'ventyd/valibot' in production
88
import { Order } from "./entities/Order";
99
import { User } from "./entities/User";
@@ -57,7 +57,7 @@ describe("Entity Unit Tests", () => {
5757
state: v.object({ value: v.string() }),
5858
}),
5959
initialEventName: "test:created",
60-
generateId: () => `test-${idCounter++}`,
60+
generateId: (type) => `${type}-${idCounter++}`,
6161
});
6262

6363
const reducer = defineReducer(schema, (_, event) => {
@@ -67,7 +67,7 @@ describe("Entity Unit Tests", () => {
6767
return { value: "" };
6868
});
6969

70-
const TestEntity = Entity(schema, reducer);
70+
class TestEntity extends Entity(schema, reducer) {}
7171

7272
const entity1 = TestEntity.create({
7373
body: { value: "first" },
@@ -78,11 +78,109 @@ describe("Entity Unit Tests", () => {
7878

7979
// generateId is called 3 times per entity:
8080
// 1) entityId, 2) validation eventId (discarded), 3) actual eventId
81-
expect(entity1.entityId).toBe("test-1000");
82-
expect(entity1[" $$queuedEvents"][0]?.eventId).toBe("test-1002");
81+
expect(entity1.entityId).toBe("entityId-1000");
82+
expect(entity1[" $$queuedEvents"][0]?.eventId).toBe("eventId-1002");
8383

84-
expect(entity2.entityId).toBe("test-1003");
85-
expect(entity2[" $$queuedEvents"][0]?.eventId).toBe("test-1005");
84+
expect(entity2.entityId).toBe("entityId-1003");
85+
expect(entity2[" $$queuedEvents"][0]?.eventId).toBe("eventId-1005");
86+
});
87+
88+
test("should generate different IDs for entityId and eventId types", () => {
89+
const generatedIds: Array<{ type: string; id: string }> = [];
90+
91+
const schema = defineSchema("test", {
92+
schema: valibot({
93+
event: {
94+
created: v.object({ value: v.string() }),
95+
},
96+
state: v.object({ value: v.string() }),
97+
}),
98+
initialEventName: "test:created",
99+
generateId: (type) => {
100+
const id = `${type}-${Date.now()}-${Math.random()}`;
101+
generatedIds.push({ type, id });
102+
return id;
103+
},
104+
});
105+
106+
const reducer = defineReducer(schema, (_, event) => {
107+
if (event.eventName === "test:created") {
108+
return { value: event.body.value };
109+
}
110+
return { value: "" };
111+
});
112+
113+
class TestEntity extends Entity(schema, reducer) {}
114+
115+
const entity = TestEntity.create({
116+
body: { value: "test" },
117+
});
118+
119+
// Filter out the validation eventId (discarded)
120+
const entityIdCalls = generatedIds.filter((g) => g.type === "entityId");
121+
const eventIdCalls = generatedIds.filter((g) => g.type === "eventId");
122+
123+
// Should have at least 1 entityId call
124+
expect(entityIdCalls.length).toBeGreaterThanOrEqual(1);
125+
// Should have at least 2 eventId calls (1 for validation, 1 for actual event)
126+
expect(eventIdCalls.length).toBeGreaterThanOrEqual(2);
127+
128+
// Verify entityId starts with "entityId-"
129+
expect(entity.entityId).toMatch(/^entityId-/);
130+
// Verify eventId starts with "eventId-"
131+
expect(entity[" $$queuedEvents"][0]?.eventId).toMatch(/^eventId-/);
132+
});
133+
134+
test("should pass correct type argument to generateId", () => {
135+
const typeCallLog: Array<"entityId" | "eventId"> = [];
136+
137+
const schema = defineSchema("test", {
138+
schema: valibot({
139+
event: {
140+
created: v.object({ value: v.string() }),
141+
updated: v.object({ value: v.string() }),
142+
},
143+
state: v.object({ value: v.string() }),
144+
}),
145+
initialEventName: "test:created",
146+
generateId: (type) => {
147+
typeCallLog.push(type);
148+
return crypto.randomUUID();
149+
},
150+
});
151+
152+
const reducer = defineReducer(schema, (prevState, event) => {
153+
if (event.eventName === "test:created") {
154+
return { value: event.body.value };
155+
}
156+
if (event.eventName === "test:updated") {
157+
return { value: event.body.value };
158+
}
159+
return prevState;
160+
});
161+
162+
class TestEntity extends Entity(schema, reducer) {
163+
updateValue = mutation(this, (dispatch, value: string) => {
164+
dispatch("test:updated", { value });
165+
});
166+
}
167+
168+
const entity = TestEntity.create({
169+
body: { value: "initial" },
170+
});
171+
172+
// Update using mutation method
173+
entity.updateValue("updated");
174+
175+
// Verify that entityId was called once for entity creation
176+
const entityIdCalls = typeCallLog.filter((t) => t === "entityId");
177+
expect(entityIdCalls.length).toBe(1);
178+
179+
// Verify that eventId was called for events
180+
// Should be 3 calls: validation for created, actual created, actual updated
181+
// (validation only happens for initial event, not for regular dispatch)
182+
const eventIdCalls = typeCallLog.filter((t) => t === "eventId");
183+
expect(eventIdCalls.length).toBe(3);
86184
});
87185
});
88186

0 commit comments

Comments
 (0)