refactor!: make a factory a function instead of an object with create - #20
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors the entity “factory” API so Entity.factory(...) / factoryAsync(...) return a callable factory function instead of an object with a single .create(...) method, reducing ceremony while preserving the same create-path semantics in this DDD entity builder.
Changes:
- Change
EntityFactory/AsyncEntityFactorytypes from{ create(...) }to(input) => Result | AsyncResult. - Update
Entity.factory/factoryAsyncimplementations to return a function directly. - Migrate docs, type tests, runtime tests, and add a changeset describing the breaking API shift.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates public documentation examples and the “entry points” table for callable factories. |
| packages/entity/src/types.ts | Redefines EntityFactory and AsyncEntityFactory as function types. |
| packages/entity/src/entity.ts | Returns factory functions directly from factory / factoryAsync. |
| packages/entity/src/entity.test-d.ts | Updates type-level tests to call factories as functions (no .create). |
| packages/entity/src/crud.spec.ts | Updates runtime tests to call factories as functions (sync + async). |
| packages/entity/README.md | Updates package-level README examples and API table for callable factories. |
| CLAUDE.md | Updates repo guidance to describe the callable factory usage. |
| .changeset/factory-returns-a-function.md | Adds a changeset documenting the breaking change and migration step. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const createOrgAsync = Organization.factoryAsync({ | ||
| id: () => ids.nextFromSequence(), | ||
| createdAt: () => clock.now(), | ||
| }); | ||
| (await orgs.create({ slug, name })).getOrThrow(); | ||
| (await createOrg({ slug, name })).getOrThrow(); |
There was a problem hiding this comment.
Fixed in e8e5554 — the example calls createOrgAsync now.
A rename artefact: I renamed the binding from asyncOrgs to createOrgAsync but the call site was orgs.create(...), and my substitution rewrote that to createOrg(...) from the sync rule. So the snippet ended up awaiting a function that returns a Result, not an AsyncResult — it would not have compiled if README snippets were typechecked.
Also rebased onto main now that #19 is merged. One conflict, in the package README statics table: main had gained the _the class itself_ row from #19, while this branch changed the two factory rows to their function signatures. Kept both.
ede5a9a to
e8e5554
Compare
Nothing but
createever consumed the generators, so the object around it wasceremony around the only thing a factory does.
factoryAsyncchanges the sameway.
On the vocabulary
When I argued against renaming
create→buildearlier, the reason was thatcreateInput(22 uses),CreateInput<T>andCreateInputOfanchor the word.Removing the method does not undermine that:
createInputstill names what acaller may send when creating, and the factory is still the create path. What
goes is a method name that only ever restated its receiver.
The call sites read better for it —
createOrg({ slug, name })says what itdoes at the point of use, where
orgs.create(...)needed the variable and themethod together to make sense.
Naming still fits
A "factory" that is a function is the ordinary factory-function shape, and
arguably a better fit for the DDD pattern than an object with a single method.
Gate
format --check,lint,typecheck(three passes),test(120, 11 files),knip,build— all green.