npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@di-framework/repo

v5.3.7

Published

A coherent abstraction of repositories and storage adapters for di-framework.

Readme

@di-framework/repo

A coherent abstraction of repositories and storage adapters for TypeScript, with optional integration for di-framework-core.

Features

  • Storage Agnostic: Decouples your business logic from the underlying storage technology (SQL, NoSQL, In-Memory, etc.).
  • Standardized Patterns: Provides BaseRepository, EntityRepository, and SoftDeleteRepository to handle common data access patterns.
  • Built-in Pagination: Standardized Page and PaginatedResult types with built-in support in adapters and repositories.
  • In-Memory Implementation: Includes a fully functional InMemoryRepository for prototyping and testing.
  • DI Integration: Seamlessly integrates with di-framework-core via the @Repository decorator.
  • Models: Optional Spring/JPA-style @Model, @Id, and @GeneratedValue for multi-context identity metadata.

Installation

bun add @di-framework/repo

Required for DI integration: If you want to use the @Repository decorator for dependency injection, install the DI framework peer dependency.

bun add @di-framework/core

Important: Always import from the scoped package name @di-framework/core/*.

Mixing different import IDs (e.g., di-framework/* or relative paths to sources) can load a second copy of the library and create a second global container instance.

Correct:

import { useContainer } from '@di-framework/core/container';
import { Container, Component } from '@di-framework/core/decorators';

Avoid:

import { useContainer } from 'di-framework/container'; // Wrong: unscoped id
import { Container } from '../../di-framework/decorators'; // Wrong: relative id

Basic Usage

1. Define your Model

Use @Model on a class. Mark identity fields with @Id and optional @GeneratedValue (stacked like Spring/JPA). The class is the TypeScript type — no separate interface is required. Storage can still use plain objects assignable to that shape.

import {
  GeneratedValue,
  GenerationType,
  Id,
  IdKind,
  Model,
} from '@di-framework/repo';

@Model()
class User {
  @Id()
  @GeneratedValue({ strategy: GenerationType.Identity })
  id!: number;

  @Id({ kind: IdKind.Public })
  @GeneratedValue({ strategy: GenerationType.UUID }) // UUIDv7 in this framework
  publicId!: string;

  name!: string;
  email!: string;
}

Identity contexts (IdKind)

A model may have several identity fields — each identifies the record in a different context:

| Kind | Typical field | Role | | --- | --- | --- | | Primary (default) | id | Database / repository primary key. Several Primary fields = composite PK. | | Public | publicId | Safe id for URLs / APIs (often UUIDv7). | | External | stripeCustomerId | Id assigned by another system. | | Legacy | legacyId | Retained during migration. | | Tenant | tenantId | Owning org / customer (may also be part of a composite PK). | | Version | versionId | Particular revision. |

@Id is for identities of this model. Foreign keys to other models are not IdKinds (relations stay separate).

Generation (GenerationType)

Aligned with Jakarta Persistence GenerationType: Auto, Identity, Sequence, Table, UUID.

  • @GeneratedValue must be stacked with @Id on the same property.
  • Default strategy when @GeneratedValue() is used without options is Auto.
  • GenerationType.UUID means UUIDv7 (RFC 9562 time-ordered), not random v4 — intentional default for index-friendly keys.

Metadata is optional for repositories today — use getModelMetadata / getIdentities / getPrimaryId / isModel when adapters need it. Repositories still take an explicit id type parameter (InMemoryRepository<User, number>).

Plain interfaces still work if you prefer them:

interface User {
  id: number;
  name: string;
  email: string;
}

2. Implement a Repository

You can extend InMemoryRepository for quick prototyping:

import { InMemoryRepository } from '@di-framework/repo';

class UserRepository extends InMemoryRepository<User, number> {
  async findByEmail(email: string): Promise<User | null> {
    const all = await this.findAll();
    return all.find((u) => u.email === email) || null;
  }
}

3. Use with di-framework

Use the @Repository decorator to automatically register your repository with the di-framework-core container.

import {
  GeneratedValue,
  GenerationType,
  Id,
  Model,
  Repository,
  InMemoryRepository,
} from '@di-framework/repo';

@Model()
class User {
  @Id()
  @GeneratedValue({ strategy: GenerationType.Identity })
  id!: number;
  name!: string;
  email!: string;
}

@Repository()
class UserRepository extends InMemoryRepository<User, number> {
  // ...
}

// In another service
@Container()
class UserService {
  constructor(@Component(UserRepository) private users: UserRepository) {}

  async listUsers() {
    return this.users.findAll();
  }
}

Storage Adapters

The StorageAdapter interface allows you to implement custom backends.

import { StorageAdapter, BaseRepository } from '@di-framework/repo';

class MyCustomAdapter<E, ID> implements StorageAdapter<E, ID> {
  // Implement findById, save, delete, findPaginated, etc.
}

class MyRepository extends BaseRepository<User, number> {
  constructor(adapter: MyCustomAdapter<User, number>) {
    super(adapter);
  }
}

API Overview

Repository Classes

  • BaseRepository<E, ID>: The foundational repository class.
  • EntityRepository<E, ID>: Standard entity-aware repository.
  • SoftDeleteRepository<E, ID>: Repository with soft-delete capabilities.
  • InMemoryRepository<E, ID>: Ready-to-use in-memory implementation.

Decorators

  • @Model(): Marks a class as a domain data model.
  • @Id(options?): Marks an identity field (kind?: IdKind, default Primary).
  • @GeneratedValue(options?): Stacked with @Id; strategy?: GenerationType (default Auto). UUID ⇒ UUIDv7.
  • @Repository(options): Registers the class as a singleton in di-framework-core.

Identity helpers

  • IdKind / GenerationType: const objects + string unions.
  • getModelMetadata / getIdentities / getPrimaryId / isModel.

Types

  • StorageAdapter<E, ID>: Interface for storage implementations.
  • Page<T> / PaginatedResult<T>: Standardized pagination metadata.
  • EntityId: Type alias for string | number.

Durable SQL adapters

BunSqliteAdapter and D1Adapter implement the complete StorageAdapter contract over an existing SQLite-compatible table. The adapter intentionally does not run migrations: create the table in your application and provide an explicit table (plus idColumn, entityToRow, and rowToEntity when needed).

const users = new BunSqliteAdapter(db, { table: 'users', idColumn: 'id' });
await users.findPaginated({ page: 1, size: 20, sort: 'name:asc', filter: { active: true } });

Cloudflare D1 uses the same options and binding API. D1 transactions are scoped to the callback and D1's platform batch/statement limits apply; schema changes and migrations remain out of band. Bun adapters close their database from dispose(); D1 disposal is a no-op.

Conditional Writes & Atomic Operations

Adapters may optionally implement ConditionalStorageAdapter<E, ID> to support atomic conditional write capabilities:

  • saveIfAbsent(entity: E): Promise<boolean>: Inserts entity only if no record with its ID currently exists. Returns true if inserted, or false if a record with the same ID already exists. Built-in SQL adapters use INSERT INTO ... ON CONFLICT (...) DO NOTHING and check for positive affected rows.
  • compareAndSwap(id: ID, mutate: (current: E | null) => E | null): Promise<boolean>: Executes a synchronous, side-effect-free mutate function atomically against the current state at id. If mutate returns null (condition failed or abort requested), the operation aborts without writing and returns false. If mutate returns a non-null entity, it updates the record and returns true.

Use the exported type guard supportsConditionalWrite(adapter) to detect capability support at runtime:

import { supportsConditionalWrite } from '@di-framework/repo';

if (supportsConditionalWrite(adapter)) {
  const inserted = await adapter.saveIfAbsent(entity);
  const swapped = await adapter.compareAndSwap(id, (current) => {
    if (!current || current.version !== expectedVersion) return null;
    return { ...current, version: current.version + 1 };
  });
}

Built-in adapters (InMemoryRepository, SqlStorageAdapter, BunSqliteAdapter, D1Adapter) implement ConditionalStorageAdapter. Custom StorageAdapter implementations do not require source changes unless conditional write capability is desired.

Blob Storage Primitives & S3 Adapters

For binary assets, large files, media uploads, and documents, @di-framework/repo provides first-class Blob storage primitives:

  • BaseBlobRepository: Abstract base class for type-safe binary asset repositories.
  • BlobStorageAdapter: Contract for blob storage engines.
  • InMemoryBlobStorageAdapter: In-memory implementation with full pagination and delimiter support for testing.
  • S3BlobStorageAdapter: Production adapter supporting AWS S3, Cloudflare R2, MinIO, Wasabi, and S3-compatible APIs, including automatic multipart uploads for large files and presigned URLs.

Usage Example

import {
  BaseBlobRepository,
  InMemoryBlobStorageAdapter,
  S3BlobStorageAdapter,
} from '@di-framework/repo';

class MediaRepository extends BaseBlobRepository {
  constructor() {
    super(
      new S3BlobStorageAdapter({
        bucket: 'my-app-media',
        region: 'us-east-1',
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      }),
    );
  }
}

const mediaRepo = new MediaRepository();

// Put file (supports string, Buffer, Uint8Array, ReadableStream, Blob)
await mediaRepo.put('uploads/doc.pdf', fileStream, {
  contentType: 'application/pdf',
  customMetadata: { userId: '123' },
});

// Read file
const blob = await mediaRepo.get('uploads/doc.pdf');
if (blob) {
  console.log(blob.metadata.size);
  const text = await blob.text();
}

// Generate presigned URL
const signedUrl = await mediaRepo.getSignedUrl('uploads/doc.pdf', {
  operation: 'get',
  expiresInSeconds: 3600,
});

// List with pagination and prefix
const results = await mediaRepo.list({ prefix: 'uploads/', delimiter: '/' });

Database Migrations

@di-framework/repo provides database schema migrations supporting both decorator-based definitions and SQL/manifest-based discovery:

  • @Migration({ version, description, binding }): Class decorator defining migrations with dependency-injected execution context.
  • Manifest and SQL Discovery: Discovers .sql migration files with standard version headers (-- migration:version, -- migration:description, etc.) or manifest files (migrations.json).
  • Shared Runner (MigrationRunner):
    • DB-backed locking: Serializes concurrent migration attempts using a lock table (_migrations_lock).
    • History tracking: Records applied versions, checksums, timestamps, and execution times in _migrations.
    • Version ordering: Applies pending migrations sequentially in ascending version order.
    • Integrity validation: Validates applied migration checksums against current definitions and rejects out-of-order pending migrations.
    • Dev auto-apply: Automatically applies pending migrations in development/test environments.

Example: Decorator Migration

import { Migration, type MigrationExecutionContext } from '@di-framework/repo';

@Migration({
  version: 1,
  description: 'create users table',
  binding: 'default',
})
export class CreateUsersTable {
  async up(ctx: MigrationExecutionContext): Promise<void> {
    await ctx.sql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
  }

  async down(ctx: MigrationExecutionContext): Promise<void> {
    await ctx.sql('DROP TABLE users');
  }
}

CLI Commands

# Check status of applied and pending migrations
di-framework migrations status --db ./dev.db --dir ./migrations

# Execute pending migrations
di-framework migrations execute --db ./dev.db --dir ./migrations

# Dry run / preview plan
di-framework migrations execute --db ./dev.db --dir ./migrations --dry-run

# Output stable JSON envelope
di-framework migrations status --db ./dev.db --json