Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/evil-trees-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ventyd": minor
---

feat: move `generateId()` option from schema declaration to `Entity()`
10 changes: 7 additions & 3 deletions src/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import type { ReadonlyEntity } from "./types/ReadonlyEntity";
* @typeParam Schema - The schema defining the entity's structure and event types
* @param schema - The schema instance created with `defineSchema()`
* @param reducer - The reducer function created with `defineReducer()`
* @param options - Optional configuration for the entity class
* @param options.maxQueuedEvents - Maximum number of uncommitted events allowed before flushing (default: 10000)
* @param options.generateId - Custom ID generator function. Receives `"entityId"` or `"eventId"` as the type argument. Defaults to `crypto.randomUUID()`
* @returns A constructor function for creating entity instances
*
* @remarks
Expand Down Expand Up @@ -63,11 +66,14 @@ import type { ReadonlyEntity } from "./types/ReadonlyEntity";
* console.log(user.state); // { email: "...", nickname: "...", bio: "..." }
* ```
*/
const defaultGenerateId = () => crypto.randomUUID();

export function Entity<$$Schema extends DefaultSchema>(
schema: $$Schema,
reducer: Reducer<$$Schema>,
options?: {
maxQueuedEvents?: number;
generateId?: (type: "eventId" | "entityId") => string;
},
): EntityConstructor<$$Schema> {
type $$EntityName = InferEntityNameFromSchema<$$Schema>;
Expand All @@ -77,9 +83,7 @@ export function Entity<$$Schema extends DefaultSchema>(

const entityName = schema[" $$entityName"] as $$EntityName;
const initialEventName = schema[" $$initialEventName"] as $$InitialEventName;
const generateId = schema[" $$generateId"] as (
type: "eventId" | "entityId",
) => string;
const generateId = options?.generateId ?? defaultGenerateId;

// options
const maxQueuedEvents = options?.maxQueuedEvents ?? 10000; // Default to 10000 events
Expand Down
11 changes: 1 addition & 10 deletions src/defineSchema.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import type { Schema, SchemaInput } from "./types";
import type { BaseEventType } from "./types/BaseEventType";

const defaultGenerateId = () => crypto.randomUUID();

/**
* Defines a complete schema for an event-sourced entity with pluggable validation.
*
* @param entityName - The canonical name for this entity type (e.g., "user", "order")
* @param options - Schema configuration options
* @param options.schema - Schema provider function (e.g., `valibot()`) that provides validation
* @param options.initialEventName - The fully-qualified event name that creates new entities (e.g., "user:created")
* @param options.generateId - Optional custom ID generator function (defaults to crypto.randomUUID)
*
* @returns A fully-typed schema object for use with Entity and Repository
*
Expand Down Expand Up @@ -61,9 +58,7 @@ const defaultGenerateId = () => crypto.randomUUID();
* })
* }),
* // Specify which event creates new entities (use fully-qualified name)
* initialEventName: "user:created",
* // Optional: Custom ID generator
* generateId: () => `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
* initialEventName: "user:created"
* });
* ```
*
Expand Down Expand Up @@ -97,7 +92,6 @@ export function defineSchema<
options: {
schema: SchemaInput<$$EntityName, $$EventType, $$StateType, $$Extension>;
initialEventName: $$InitialEventName;
generateId?: (type: "eventId" | "entityId") => string;
},
): Schema<
$$EntityName,
Expand All @@ -106,8 +100,6 @@ export function defineSchema<
$$InitialEventName,
$$Extension
> {
const generateId = options.generateId ?? defaultGenerateId;

const schemaInput = options.schema({
entityName,
});
Expand All @@ -116,6 +108,5 @@ export function defineSchema<
...schemaInput,
" $$entityName": entityName,
" $$initialEventName": options.initialEventName,
" $$generateId": generateId,
};
}
1 change: 0 additions & 1 deletion src/types/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export type Schema<
> = ReturnType<SchemaInput<EntityName, EventType, StateType, Extension>> & {
" $$entityName": EntityName;
" $$initialEventName": InitialEventName;
" $$generateId": (type: "eventId" | "entityId") => string;
};

/**
Expand Down
31 changes: 17 additions & 14 deletions test/entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("Entity Unit Tests", () => {
expect(user[" $$queuedEvents"][0]?.entityId).toBe(customId);
});

test("should use custom ID generator from schema", () => {
test("should use custom ID generator from entity options", () => {
let idCounter = 1000;

const schema = defineSchema("test", {
Expand All @@ -57,7 +57,6 @@ describe("Entity Unit Tests", () => {
state: v.object({ value: v.string() }),
}),
initialEventName: "test:created",
generateId: (type) => `${type}-${idCounter++}`,
});

const reducer = defineReducer(schema, (_, event) => {
Expand All @@ -67,7 +66,9 @@ describe("Entity Unit Tests", () => {
return { value: "" };
});

class TestEntity extends Entity(schema, reducer) {}
class TestEntity extends Entity(schema, reducer, {
generateId: (type) => `${type}-${idCounter++}`,
}) {}

const entity1 = TestEntity.create({
body: { value: "first" },
Expand Down Expand Up @@ -96,11 +97,6 @@ describe("Entity Unit Tests", () => {
state: v.object({ value: v.string() }),
}),
initialEventName: "test:created",
generateId: (type) => {
const id = `${type}-${Date.now()}-${Math.random()}`;
generatedIds.push({ type, id });
return id;
},
});

const reducer = defineReducer(schema, (_, event) => {
Expand All @@ -110,7 +106,13 @@ describe("Entity Unit Tests", () => {
return { value: "" };
});

class TestEntity extends Entity(schema, reducer) {}
class TestEntity extends Entity(schema, reducer, {
generateId: (type) => {
const id = `${type}-${Date.now()}-${Math.random()}`;
generatedIds.push({ type, id });
return id;
},
}) {}

const entity = TestEntity.create({
body: { value: "test" },
Expand Down Expand Up @@ -143,10 +145,6 @@ describe("Entity Unit Tests", () => {
state: v.object({ value: v.string() }),
}),
initialEventName: "test:created",
generateId: (type) => {
typeCallLog.push(type);
return crypto.randomUUID();
},
});

const reducer = defineReducer(schema, (prevState, event) => {
Expand All @@ -159,7 +157,12 @@ describe("Entity Unit Tests", () => {
return prevState;
});

class TestEntity extends Entity(schema, reducer) {
class TestEntity extends Entity(schema, reducer, {
generateId: (type) => {
typeCallLog.push(type);
return crypto.randomUUID();
},
}) {
updateValue = mutation(this, (dispatch, value: string) => {
dispatch("test:updated", { value });
});
Expand Down
Loading