Ts ddd repository
Create, review, or guide repository contracts and implementations in a TypeScript + DDD codebase. Use when the request involves `*.repository.ts` files under `apps/api/src/<bc>/domain/repositories/` or `apps/api/src/<bc>/infra/{firestore,memory}/`, persistence operations (save / findBy* / list*), the `<NAME>_REPOSITORY` DI token, adapting the Firebase Admin SDK to a domain port, `toFirestore`/`fromFirestore` mappers, the side-by-side Firestore + InMemory adapter pair, contract tests under `apps/api/test/<bc>/infra/{firestore,memory}/`, `Result` error handling, or CQRS Repository vs Query separation.From its SKILL.md
npx -y skills add llodev/skills --skill ts-ddd-repositoryAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.6 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it
TypeScript DDD Repository
MANDATORY — READ ENTIRE FILE: Before any implementation step, read
references/repository-pattern.md completely,
then references/firestore-adapter.md completely.
The Firestore Admin SDK is the only production persistence stack in this repo
(no Prisma, no MongoDB, no Supabase).
Do NOT load other DDD skills (ts-ddd-use-case, ts-ddd-entity,
ts-ddd-dto, ts-query-cqrs) unless explicitly requested.
Before You Start — ask: which aggregate owns this write? Persistence boundaries must match aggregate boundaries; if a single
save()touches data from two aggregates, you've conflated them.
Where the files live
Each bounded context owns a hexagonal slice:
apps/api/src/<bc>/
domain/
repositories/
<name>.repository.ts ← port (interface) + DI token symbol
index.ts ← barrel
infra/
firestore/
firestore-<name>.repository.ts ← Firestore adapter (Injectable)
<name>.mapper.ts ← to/from Firestore helpers
index.ts
memory/
in-memory-<name>.repository.ts ← InMemory adapter (no decorators)
index.ts
<bc>.module.ts ← wires { provide: <NAME>_REPOSITORY, useClass: Firestore<Name>Repository }
apps/api/test/<bc>/infra/
firestore/firestore-<name>.repository.test.ts ← fake-DB at the SDK boundary
memory/in-memory-<name>.repository.test.ts ← straight unit test
Path aliases used everywhere:
@acme/shared→Result,Entity, base building blocks.@acme/<bc>-contracts→ wire types, status/kind enums (e.g.CelebrationStatusEnum).@<bc>/domain/...,@<bc>/infra/...→ cross-layer imports inside the BC.@shared/firebase→FirestoreService.- Relative paths only within the same folder.
Before You Start
Answer first:
- Which operations? Define the smallest port that satisfies the use cases. Prefer intent-named methods (
save,findBySlug,listByStatus,saveSection,deleteSection) over a genericCrudRepository. - Aggregate boundary? A single port should own writes to the whole aggregate (e.g.
CelebrationRepositoryownscelebrationsdoc + itssectionssubcollection). Do not split aggregates across multiple ports. - Repository or Query? Loading an entity to enforce invariants → Repository. Returning a read DTO for the API/front → Query (separate interface, separate skill:
ts-query-cqrs). - DI token name? Add an exported
Symbol("<NAME>_REPOSITORY")next to the interface — Nest injects by token, not class. - InMemory adapter? Always ship one. Use-case tests substitute it via the same DI token; no need for
jest.fn()stubs.
Repository vs Query (CQRS)
| Need | Use | Returns |
|---|---|---|
| Load entity to preserve invariants before update | Repository.findBy* | Domain entity (or null) |
| Existence check before a write | Repository.findBy* | Entity or null or Result.fail |
| Read projection for an API/front response | Query (separate interface) | Read DTO |
Custom domain-oriented lookup (findBySlug) | Repository method | Domain entity |
| Paginated list for the UI | Query | PaginatedResultDTO<XxxListItem> |
Rule: if the caller needs the entity to run domain logic → Repository. If the caller only needs data to display → Query. Both can share an adapter class but the TypeScript interfaces must be separate.
Core Rules
- The port lives in
<bc>/domain/repositories/— nofirebase-admin, no@nestjs/*, no Zod, no DTOs. - Export a DI token symbol next to the port:
export const CELEBRATION_REPOSITORY = Symbol("CELEBRATION_REPOSITORY"); - Every method returns
Promise<Result<T>>. Adapters never throw in the normal flow — wrap I/O intry/catchand returnResult.fail("SHORT_SCREAMING_SNAKE_CODE"). - Lookup methods that legitimately mean "absent" return
Result<T | null>(Firestore + InMemory both usedfindBySlug(...): Promise<Result<Celebration | null>>). Mutation methods that require an existing aggregate returnResult.fail("<AGG>_NOT_FOUND"). - Mapping lives in dedicated
<name>.mapper.tsfiles (toFirestore/fromFirestorereturningResult<Entity>) — never inline transformation inside an operation method. saveaccepts a fully constructed entity (already validated byEntity.tryCreate/cloneWithin the use case). The adapter does not patch partial fields.- Aggregate writes that touch a subcollection (e.g. wholesale-replace sections) document their atomicity guarantees as a code comment — if the write is non-transactional, say so.
NEVER
- NEVER import
firebase-admin,@nestjs/*, Zod, or BC contracts that carry HTTP/UI shapes into a port file. - NEVER add a read-projection / DTO-returning method to a Repository — that belongs in a
Queryinterface (seets-query-cqrs). - NEVER inline mapping in
findBy*/save— extracttoFirestore/fromFirestore. - NEVER accept partial fields in
save(). The use case doescloneWithfirst; the adapter receives a complete, validated aggregate. - NEVER wire an adapter directly via
useClasswithout the token symbol — Nest cannot inject an interface, only a token. - NEVER hit real Firestore from a unit test. Pass a fake at the SDK boundary (see firestore-adapter reference).
- NEVER share entity references across repository round-trips. Always
toSnapshot()on save andCelebration.tryCreate(structuredClone(snap))on read — otherwise two callers can mutate the same in-memory aggregate and corrupt invariants between use cases. - NEVER call
snap.data()without first checkingsnap.exists. It returnsundefinedand the next field access crashes downstream — guard withif (!snap.exists) return Result.ok<T | null>(null);first. - NEVER let
firebase-admin/firestore.Timestampleak into the domain entity. Convert at the boundary (Timestamp.fromDate()on write,.toDate()on read) inside the mapper. The domain holds plainDate, neverTimestamp— otherwise the domain layer transitively depends onfirebase-admin.
References
references/repository-pattern.md— port shape, DI token, InMemory adapter, dual-adapter test strategy, enums-in-fixtures rule, checklist.references/firestore-adapter.md— Firebase Admin SDK basics (collection().doc().get(), subcollections,Timestampconversions, fake-DB test harness, mapper helpers, aggregate-write caveats).
What ships with it: 10 files
48.0 KB alongside SKILL.md, 2 of them executable
docs/
- i18n/README.es-ES.md5.5 KB
- i18n/README.pt-BR.md5.4 KB
examples/
- in-memory-product.repository.tsruns2.5 KB
- product.repository.tsruns1.2 KB
references/
- firestore-adapter.md15.0 KB
- repository-pattern.md10.5 KB
- CHANGELOG.md1012 B
- LICENSE1.1 KB
- package.json801 B
- README.md5.1 KB
Gives 0 of the 12 instructions most test skills give in ~1.6k tokens
Counted across 1,201 of the 2,096 authors here whose files we hold, read 2026-09-06
- Write a failing test before writing codein 43 of 1201, across 36 files
- Run the full test suitein 36 of 1201, across 35 files
- Test only one variable per experimentin 34 of 1201, across 17 files
- Read product marketing context before asking questionsin 34 of 1201, across 14 files
- Mock external dependenciesin 34 of 1201, across 30 files
- Define primary, secondary, and guardrail metricsin 33 of 1201, across 16 files
- Pre-determine sample size before startingin 31 of 1201, across 14 files
- Test behavior rather than implementationin 31 of 1201, across 29 files
- Formulate a hypothesis before designing a testin 30 of 1201, across 13 files
- Document every test hypothesis, variant, and resultin 29 of 1201, across 11 files
- Use descriptive test function namesin 25 of 1201, across 21 files
- Commit to the methodology without stopping earlyin 24 of 1201, across 8 files
Said here and by no other author read
- Read repository and firestore adapter references before starting
- Define the smallest port satisfying the use cases
- Export a DI token symbol next to the port
- Return Promise Result for every repository method
- Wrap all I/O in try catch blocks
- Extract mapping logic into dedicated mapper files
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.