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

@donilite/wrap

v1.4.4

Published

OOP backend framework on Hono + Drizzle — Bun-first, vertical-slice, decorator-driven

Readme

@donilite/wrap

OOP backend framework on Hono + Drizzle — Bun-first, vertical-slice, decorator-driven. See the repository README for the overview; this page is the module reference. Every exported class, function and option also carries JSDoc.

bun create @donilite/wrap my-app   # scaffold a ready-to-run project
# or add to an existing app:
bun add @donilite/wrap

Peer dependencies: hono, drizzle-orm, drizzle-zod, zod, pg (plus @electric-sql/pglite and drizzle-kit for the testing utilities).

Requires experimentalDecorators + emitDecoratorMetadata in the consumer tsconfig (the scaffold ships it).

Registry — telling the framework about your app

Types flow from a single declaration-merging point:

// factory/web.factory.ts (scaffolded)
declare module "@donilite/wrap" {
  interface WrapRegistry {
    schema: typeof import("@/db");  // tables + relations → typed populate
    variables: Variables;           // Hono context variables
    roles: UserRoles;               // access-control roles
  }
}

Entities — Entity, relationsOf, BaseRow

export class Example extends Entity("examples", {
  name: text("name").notNull(),
  categoryId: text("category_id").references(() => ExampleCategory.table.id),
}) {}

// Required named exports (drizzle-kit + populate typing):
export const ExampleTable = Example.table;
export const ExampleRelations = relationsOf(Example, ({ one, many, self }) => ({
  items: many(() => ExampleItem),
  category: one(() => ExampleCategory, { fields: [self.categoryId] }),
}));
  • BaseRow columns (id, createdAt, updatedAt, deletedAt) are injected automatically.
  • InstanceType<typeof Example> is the typed row — columns are never re-declared.
  • Relation targets are lazy class thunks: cross-slice references never create import cycles.
  • Declare relations through relationsOf(entity, cb) (the relations option of Entity() also exists, but mutual references inside extends clauses are a TS type cycle).

Database — initializeDatabase, getDatabase, setDatabase, closeDatabase

initializeDatabase({ connectionString, schema, poolSize?, logger? });

Call once at bootstrap, before any controller/repository is instantiated (the scaffold does it in src/bootstrap.ts, first import of src/index.ts). setDatabase(db) injects a prebuilt drizzle instance (used by the testing utilities). Public types depending on the registry are explicitly annotated — keep it that way if you extend the framework.

Repositories — BaseRepository<Tb, TCreate?, TUpdate?>

@Repository("ExampleRepository")
export class ExampleRepository extends BaseRepository<
  typeof Example.table,
  CreateExampleDTO,
  Partial<CreateExampleDTO>
> {
  protected table = Example.table;
  protected override relationalQuery = () => this.db.query.ExampleTable; // populate
  protected override defaultWith = { items: true, category: true } as const;
  protected override searchableFields: (keyof ExampleTableType)[] = ["name"];
  protected override cacheTtl = 30; // seconds — entity-scope cache
}

CRUD surface: create, findById, findOne, findAll, findPaginated, findBy, findOneBy, update, delete, deleteMultiple, count, exists, getStatistics.

  • Read methods accept { with, includeDeleted }; with is typed against your relations() and the result type includes the populated relations.
  • Soft delete: rows with deletedAt set are excluded by default.
  • relationalQuery is a thunk so the database is resolved at query time.
  • cacheTtl opts into the entity-scope cache: reads go through the configured store, any write invalidates the scope (via entity events), transactional reads bypass it.

Services & controllers — BaseService<Repo>, BaseController<Service>

@Service()
export class ExampleService extends BaseService<ExampleRepository> {}

@Controller({ basePath: "/api/examples", tags: ["Examples"] })
export class ExampleController extends BaseController<ExampleService> {
  @Get({ path: "/" })
  @ApiResponse(200, { description: "OK", schema: PaginatedResponseDTO(ExampleBase) })
  @Serialize(PaginatedResponseDTO(ExampleBase))
  async list(c: Context) { ... }
}

Route decorators: @Get/@Post/@Put/@Patch/@Delete/@Route, @UseMiddleware, @Cache, @RateLimit, @Can(roles), @ApiResponse, @Serialize, @ValidateDTO. handleError and the global errorHandler() (register with app.onError) share one error contract; validation failures answer 400 with { property, constraints, value } details.

DTOs — zod-backed classes

export const ExampleBase = SelectDTO(Example, { exclude: ["deletedAt"] });
export class CreateExampleDTO extends InsertDTO(Example) {}   // id/timestamps excluded
export const UpdateExampleDTO = PartialDTO(CreateExampleDTO);
export class ContactDTO extends SchemaDTO(z.object({ email: z.email() })) {} // entity-free

X.from(plain) validates and strips unknown keys; X.schema is the single source for validation, whitelist serialization and OpenAPI (z.toJSONSchema). Refine with static override schema = Parent.schema.extend({...}).

Transactions — withTransaction

await withTransaction(async () => {
  const user = await userRepository.create(dto);
  await profileRepository.create(ProfileDTO.from({ userId: user.id }));
}); // throw → rollback of the whole scope

Implicit AsyncLocalStorage propagation — no tx parameter. Nested calls join the ambient transaction. Entity events are buffered until commit.

Entity events — onEntityEvent, emitEntityEvent

const off = onEntityEvent(Example, (event) => {
  // { type: "created" | "updated" | "deleted", table, data }
});
onEntityEvent("*", audit);

Emitted by every repository write; they power cache invalidation and realtime.

Cache — configureCache, CacheStore, RedisCacheStore

configureCache({ store: new RedisCacheStore({ url: process.env.REDIS_URL }) });

In-memory by default. The store powers repository cacheTtl, the @Cache decorator and cacheMiddleware. RedisCacheStore uses Bun's native Redis client (zero dependency). Custom backends implement CacheStore (get/set/delete/deletePrefix/clear).

Realtime — @donilite/wrap/realtime (Bun-only subpath)

const realtime = createRealtime({ redisUrl, authorize? });
app.get("/realtime", realtime.upgrade);
realtime.bindEntityEvents(); // auto-publish writes on entity:<table>
const server = Bun.serve({ fetch: app.fetch, websocket: realtime.websocket, port });
realtime.attach(server);

Fan-out uses native Bun WebSocket topics; the optional Redis pub/sub relay (single wrap:rt:relay channel) synchronizes instances. Client protocol: { action: "subscribe" | "unsubscribe", channel }{ type: "subscribed" | "message" | "error", ... }.

Auth — createAuth

export const auth = createAuth({ secret, secureCookies: prod });
app.use("/admin/*", auth.authMiddleware);
export const adminOnly = auth.requireRoles(WRITE_ACCESS);

JWT (HS256 by default) + refreshed cookie session; setupCookieSession/clearCookieSession for login/logout flows.

Testing — @donilite/wrap/testing

const testDb = await createTestDatabase({ schema }); // in-process PGlite, no docker
await testDb.truncateAll();                          // between tests (also clears cache)
service.create(dto, testContext({ json: body }));    // minimal Hono context
await requestJson(controller.getApp(), "POST", "/", body);

Pass url to run against a real Postgres for integration tests.

Also exported

ResponseHelper, buildQuery (HTTP query → PaginationQuery), ValidatorHelper, hash/image/date helpers, BaseSeeder/SeederRunner, ServiceFactory, storage providers (configureStorage, LocalStorageProvider), setupSwagger, logger/Logger, rate-limit/cache/request-logger middlewares.