Skip to content

[Refactor] Implement Repository Pattern for Clean Data Access Layer Abstraction #22

Description

@KarenZita01

Description

The current EquipChain codebase has data access logic mixed directly into route handlers and services, leading to tight coupling between business logic and data storage mechanisms. This refactoring introduces the Repository Pattern to abstract data access behind clean interfaces, making the codebase more maintainable, testable, and adaptable to different data storage backends (in-memory, database, external API).

The Repository Pattern involves creating repository classes for each domain entity that encapsulate all data access operations (CRUD, query, aggregation). Route handlers and services will depend on these repository interfaces rather than directly manipulating data structures. This abstraction enables: swapping storage backends without changing business logic (e.g., migrating from in-memory to PostgreSQL), simplifying unit tests by mocking repositories, centralizing data access validation and error handling, and providing a single place to add caching, logging, or audit trails for data operations.

Initial repositories to create: UserRepository (admin users), DeviceRepository (registered meters/devices), MeterReadingRepository (reading data points), WebhookRepository (webhook registrations and delivery logs), ApiKeyRepository (API key store), and ConfigRepository (protocol configuration). Each repository should follow a consistent interface: findById(id), findAll(query), create(data), update(id, data), delete(id). Additional domain-specific methods should be added as needed (e.g., findByPublicKey(publicKey) for users, findByMeterId(meterId, dateRange) for readings).

Technical Context & Impact

  • Dependencies: No new external dependencies. This is a code architecture refactoring. TypeScript (Issue [Monitoring] Integrate Structured Logging (Pino) and Request Tracing with OpenTelemetry #15) will make the repository interfaces more explicit.
  • Architecture: New src/repositories/ directory with one file per entity. Each repository exports a class implementing a documented interface. Current in-memory stores will be migrated from service files into these repositories. Service files (auth, analytics, webhooks) will be updated to use repositories instead of direct data manipulation.
  • Impact: This is a foundational refactoring. It affects almost every service and route handler. The immediate benefit is testability (repositories can be mocked). Long-term, it enables smooth migration to a database without rewriting business logic.

Step-by-Step Implementation Guide

  1. Define Repository Interface: Create src/repositories/base.js (or a JSDoc type definition) documenting the standard repository API: findById, findAll, create, update, delete. Include TypeScript-style JSDoc comments for IDE autocompletion.
  2. Create In-Memory Repository Base: Write src/repositories/BaseRepository.js implementing the common interface using an in-memory Map. Include built-in filtering, pagination (reusing Issue [Feature] Create Pagination, Filtering, and Search Utilities for All List Endpoints #17), and event emission for data changes. Other repositories will extend this base class.
  3. Implement Domain Repositories: Create UserRepository.js, DeviceRepository.js, MeterReadingRepository.js, WebhookRepository.js, ApiKeyRepository.js, ConfigRepository.js. Each extends BaseRepository, adds domain-specific methods, and pre-seeds any default data (e.g., default config values, initial admin user).
  4. Migrate Data from Services: Move in-memory stores from service files (auth.js, webhook.js, apiKey.js, etc.) into the appropriate repositories. Update service files to import and use repositories instead of maintaining their own data stores.
  5. Update Route Handlers: Refactor route handlers to use repositories. For example, admin/users.js should call userRepository.findAll() instead of directly accessing an array.
  6. Update Tests: Refactor tests to use repositories with mock data. Create tests/unit/repositories/ with tests for each repository verifying CRUD operations, filtering, and edge cases.
  7. Write Integration Test: Create a test that verifies the full flow from route → service → repository with the in-memory backend.

Verification & Testing Steps

  1. Run npm test — verify existing tests still pass after the refactoring. Some tests may need minor updates due to changed import paths.
  2. Verify each repository's CRUD operations work correctly by running tests/unit/repositories/*.test.js.
  3. Start the server and verify all API endpoints still function correctly — create a user, list devices, register webhooks, etc.
  4. Create a mock repository for testing (e.g., class MockUserRepository extends BaseRepository) and verify it integrates cleanly with the service layer.
  5. Verify that the in-memory store is no longer accessed directly from any service or route handler — grep for direct Map or Array usage in service files to confirm migration is complete.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions