agentsclimarketplace

Ts ddd repository

Skill llodev/skills/skills/ts-ddd-repository

Collection of Agent Skills for Claude Code, Cursor, Codex, Windsurf, and any agent that speaks the open Skills spec. Ships via npm + plugin + skills add.

Install
npx -y skills add llodev/skills --skill ts-ddd-repository

Assembled 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.

What its author says it does

Copied from the file, not written here

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.

SKILL.md

7.6 KB, 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/sharedResult, 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/firebaseFirestoreService.
  • 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 generic CrudRepository.
  • Aggregate boundary? A single port should own writes to the whole aggregate (e.g. CelebrationRepository owns celebrations doc + its sections subcollection). 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)

NeedUseReturns
Load entity to preserve invariants before updateRepository.findBy*Domain entity (or null)
Existence check before a writeRepository.findBy*Entity or null or Result.fail
Read projection for an API/front responseQuery (separate interface)Read DTO
Custom domain-oriented lookup (findBySlug)Repository methodDomain entity
Paginated list for the UIQueryPaginatedResultDTO<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/ — no firebase-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 in try/catch and return Result.fail("SHORT_SCREAMING_SNAKE_CODE").
  • Lookup methods that legitimately mean "absent" return Result<T | null> (Firestore + InMemory both used findBySlug(...): Promise<Result<Celebration | null>>). Mutation methods that require an existing aggregate return Result.fail("<AGG>_NOT_FOUND").
  • Mapping lives in dedicated <name>.mapper.ts files (toFirestore / fromFirestore returning Result<Entity>) — never inline transformation inside an operation method.
  • save accepts a fully constructed entity (already validated by Entity.tryCreate / cloneWith in 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 Query interface (see ts-query-cqrs).
  • NEVER inline mapping in findBy* / save — extract toFirestore / fromFirestore.
  • NEVER accept partial fields in save(). The use case does cloneWith first; the adapter receives a complete, validated aggregate.
  • NEVER wire an adapter directly via useClass without 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 and Celebration.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 checking snap.exists. It returns undefined and the next field access crashes downstream — guard with if (!snap.exists) return Result.ok<T | null>(null); first.
  • NEVER let firebase-admin/firestore.Timestamp leak into the domain entity. Convert at the boundary (Timestamp.fromDate() on write, .toDate() on read) inside the mapper. The domain holds plain Date, never Timestamp — otherwise the domain layer transitively depends on firebase-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, Timestamp conversions, fake-DB test harness, mapper helpers, aggregate-write caveats).

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.