Skip to content

feat: Repository typed events, consumer migration + API positioning (PR 2+3 of 3)#777

Merged
loopingz merged 12 commits into
mainfrom
feat/repository-refactor-pr2
Jun 27, 2026
Merged

feat: Repository typed events, consumer migration + API positioning (PR 2+3 of 3)#777
loopingz merged 12 commits into
mainfrom
feat/repository-refactor-pr2

Conversation

@loopingz

@loopingz loopingz commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Second of three PRs for the Repository concept refactor (follows #776). This PR makes Repository<T> the single source of CRUD events: each concrete store wraps its repository in EventRepository, so typed ModelEvents (Created/Updated/Patched/Deleted/PartialUpdated) fire on every operation and are reachable via useRepository(Model).on(...). Per maintainer direction, downstream consumers listen on the repository, not on the legacy Store.* events — so this PR migrates graphql to repository events rather than re-emitting Store.* (a deviation from the original plan's Task 12).

Spec/Plan: docs/superpowers/specs/2026-05-09-repository-design.md (local-only, gitignored).

What's in this PR

  • @webda/models — export EventRepository (was implemented but unexported); add Node-EventEmitter-compat shims to AbstractRepository (getMaxListeners/setMaxListeners/removeListener) so a repository can be consumed by @webda/runtime EventIterator.
  • MemoryStore, FileStore, PostgresStoregetRepository() wraps the underlying repository in EventRepository. find()/simulateFind() are unaffected (they only call repository.get(), which EventRepository proxies).
  • PostgresStore — table DDL pushed down to PostgresRepository.setupTable()/getTable(); getRepositories() unwraps the EventRepository per managed model; checkTable() delegates per repository (deduped by table).
  • @webda/core — re-export EventRepository and useRepository.
  • @webda/graphql — query, single-instance, and generic event subscriptions now source from useRepository(model) and listen on typed events (SavedCreated); authorizeClientEvent is still consulted on the store.

Tests

  • @webda/models — 88 passed (new: AbstractRepository EventEmitter-compat shim).
  • @webda/core — 561 passed / 1 todo (new: repository emits Created; useRepository(Model).on fires on Model.create()).
  • @webda/fs — 124 passed (new: FileStore repository emits Created; storage-access tests unwrap the EventRepository).
  • @webda/postgres — non-DB tests pass (new: getRepository wraps in EventRepository; getRepositories() unwraps to PostgresRepository). The 21 DB-backed failures are pre-existing ECONNREFUSED (no local PostgreSQL).
  • Full pnpm run build green (except the pre-existing docs/Docusaurus failure). Lint clean on all touched packages.

Verification caveats (build-only)

  • graphql tests are disabled ("Tests disabled — needs migration from @testdeck/mocha to @webda/test"), so the graphql migration is typecheck/build-verified only. The contract it relies on (useRepository(Model).on("Created") fires on Model.create()) is covered by a new integration test in @webda/core.
  • postgres DB-backed behavior (event emission, setupTable DDL) needs a live PostgreSQL; only construction-level tests run here.

Out of scope (deferred)

  • @webda/elasticsearch consumer migration — the package is excluded from the pnpm workspace (!packages/elasticsearch, "not yet ready"): no node_modules, stale webda build. It needs the same useRepository(model) migration when revived alongside its test-suite migration.
  • aws/gcp/mongodb — untouched / not in active workspace (as in feat(core): flat models[] config + internal field migration (PR 1 of 3) #776).
  • @internal tagging, sample-app + docs migration → PR 3.

🤖 Generated with Claude Code


Update — Phase 3 (API positioning) added to this branch

This PR now also carries Phase 3, so it covers PRs 2 and 3 of the refactor.

  • @internal taggingStore, StoreParameters, StoreInterface, StoreFindResult, StoreEvents, Repositories, registerRepository flagged @internal; useRepository stays public. JSDoc-only, build-verified. (The plan also listed useStore() — it does not exist in the codebase.)
  • Agent docsCLAUDE.md "Store Pattern" → "Repository Pattern" (Model.create/ref/query, useRepository(Model).on for events); DI example no longer @Injects a store; store config examples use flat models[]. .github/copilot-instructions.md config example uses models[]. AGENTS.md had no persistence section (unchanged).
  • Docs site — new Concepts/Stores/Repositories.md page (canonical persistence API); Stores.md demoted to "storage backends — advanced" with an @internal banner pointing to Repositories. Markdown-only (Docusaurus build is pre-existing broken on a missing typedoc folder).
  • Sample-app migration — no-op: sample-apps/blog-system (and the legacy sample-app) are already Repository-first (User.create(), Post.ref().get()), with no @Inject(store)/useStore. Verified blog-system builds clean.

Phase 3 verification

  • Full pnpm run build green (except pre-existing docs/Docusaurus). models 88, core 561+1 todo, fs 124 pass; postgres non-DB pass (21 pre-existing ECONNREFUSED). Lint clean on all touched packages.

loopingz and others added 8 commits June 14, 2026 14:46
…events

MemoryStore.getRepository() now wraps its MemoryRepository in EventRepository
so typed CRUD events (Created/Updated/Patched/Deleted/PartialUpdated) fire on
every operation. Consumers reach them via useRepository(model).on(...).

- Export EventRepository from @webda/models (was implemented but unexported)
- Update CoreTest.registry to unwrap the EventRepository when reaching the
  backing MemoryRepository storage
- New StoreRepositoryEventsTest asserts Created fires on create

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FileStore.getRepository() wraps its MemoryRepository in EventRepository so
typed CRUD events fire. find()/simulateFind() only call repository.get(),
which EventRepository proxies, so querying is unaffected.

- Re-export EventRepository from @webda/core
- Unwrap EventRepository in the filestore-unit storage-access tests
- New repositoryEmitsTypedCreatedEvent test asserts Created fires

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Integration test for the consumer contract Phase 2 establishes: listeners
registered via useRepository(Model).on("Created") receive typed events when
Model.create() runs through the registered EventRepository. This is the
mechanism graphql/elasticsearch migrate onto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds getMaxListeners/setMaxListeners + removeListener (alias for off) so a
repository can be consumed wherever a Node EventEmitter is expected — notably
@webda/runtime EventIterator, which graphql uses for subscriptions. The
max-listeners value is a hint only; repositories keep an unbounded listener Set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The query, single-instance, and generic event subscriptions now source from
useRepository(model) and listen on typed ModelEvents (Created/Updated/Patched/
Deleted/PartialUpdated) — Saved maps to Created. This revives subscriptions
that depended on Store.* events nothing emits today. authorizeClientEvent is
still consulted on the store. Export useRepository from @webda/core.

Build-verified only: graphql's test suite is disabled pending the
@testdeck/mocha -> @webda/test migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o repository

- PostgresStore.getRepository() wraps PostgresRepository in EventRepository so
  typed CRUD events fire (consumers use useRepository(model).on(...))
- PostgresRepository owns its DDL via setupTable()/getTable()
- PostgresStore.getRepositories() unwraps the EventRepository per managed model
- checkTable() delegates to each repository's setupTable(), deduped by table name
- Non-DB tests assert the wrapping + unwrap (DB-backed tests remain gated on a
  live PostgreSQL; the 21 ECONNREFUSED failures are pre-existing/environmental)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.41935% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.05%. Comparing base (56d4b01) to head (1dfcd33).

Files with missing lines Patch % Lines
packages/postgres/src/postgresstore.ts 63.15% 3 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #777      +/-   ##
==========================================
+ Coverage   74.02%   74.05%   +0.02%     
==========================================
  Files         229      229              
  Lines       15610    15628      +18     
  Branches     3790     3792       +2     
==========================================
+ Hits        11556    11573      +17     
- Misses       2967     2968       +1     
  Partials     1087     1087              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

loopingz and others added 4 commits June 15, 2026 07:33
Store, StoreParameters, StoreInterface, StoreFindResult, StoreEvents,
Repositories, registerRepository flagged @internal. App code uses
useRepository(Model) or Model.create()/.query()/.ref(); useRepository stays
public. No runtime change.

(Plan listed useStore() too, but it does not exist in the codebase.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CLAUDE.md: 'Store Pattern' -> 'Repository Pattern' (Model.create/ref/query,
  useRepository(Model).on for events); DI example no longer @injects a store;
  store config examples use flat models[]
- copilot-instructions: store config uses models[]
- AGENTS.md unchanged (no persistence-pattern section)

Stores are internal infrastructure; @Inject(storeName) for stores is deprecated
for app code.
Repository is the canonical persistence API (Model.create/ref/query,
useRepository(Model).on for events). The Stores page gets an @internal banner
pointing to Repositories and sorts after it in the sidebar.

Markdown-only; the Docusaurus build is pre-existing broken (missing typedoc
folder) and unrelated to this change.
@loopingz loopingz changed the title feat: Repository typed events + consumer migration (PR 2 of 3) feat: Repository typed events, consumer migration + API positioning (PR 2+3 of 3) Jun 15, 2026
@loopingz
loopingz merged commit 70b0a75 into main Jun 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant