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

@talosjs/database

v1.7.2

Published

Database layer with a decorator-based ORM for PostgreSQL, MySQL, SQLite, Turso, Bunny, Cloudflare and ClickHouse, plus Redis and MongoDB drivers

Readme

@talosjs/database

Database layer with a decorator-based ORM for PostgreSQL, MySQL, SQLite, Turso, Bunny, Cloudflare and ClickHouse, plus Redis and MongoDB drivers. PostgreSQL, MySQL and SQLite use Bun's native SQL client; Turso uses @libsql/client, Bunny uses its @libsql/client/web entry point, Cloudflare uses a Worker binding or the D1 HTTP API, ClickHouse uses @clickhouse/client, and MongoDB uses the official mongodb client.

Installation

bun add @talosjs/database

Quick start

import {
  Column,
  DataSource,
  Entity,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
  type RelationType,
} from "@talosjs/database";

@Entity("users")
class User {
  @PrimaryGeneratedColumn("uuid")
  public id?: string;

  @Column({ type: "varchar", length: 120, unique: true })
  public email = "";

  @OneToMany(() => Post, (post) => post.author, { cascade: true })
  public posts?: Post[];
}

@Entity("posts")
class Post {
  @PrimaryGeneratedColumn()
  public id?: number;

  @Column({ type: "varchar" })
  public title = "";

  @ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
  public author?: RelationType<User> | null;
}

const dataSource = new DataSource({
  type: "postgres",
  url: "postgres://app:secret@localhost:5432/app",
  entities: [User, Post],
  synchronize: true,
});

await dataSource.initialize();

const users = dataSource.getRepository(User);
const alice = await users.save(users.create({ email: "[email protected]", posts: [{ title: "Hello" }] }));

const found = await users.findOne({ where: { email: "[email protected]" }, relations: { posts: true } });

const popular = await dataSource
  .createQueryBuilder(Post, "post")
  .leftJoinAndSelect("post.author", "author")
  .where("post.title ILIKE :term", { term: "%hello%" })
  .orderBy("post.id", "DESC")
  .take(10)
  .getMany();

await dataSource.transaction(async (manager) => {
  await manager.increment(Post, { id: 1 }, "views", 1);
  await manager.softRemove(alice);
});

A shared source for the process

SqlDatabase is the base class for an application database. Modules declare the entities they persist on the concrete class, and every instance in the process reuses one data source for a given connection identity (a URL or a file path). The database never imports those modules.

import { DataSource, decorator, SqlDatabase } from "@talosjs/database";

@decorator.database()
export class MainDatabase extends SqlDatabase {
  public getSource(): DataSource {
    const url = process.env.DATABASE_URL ?? "";

    return this.sharedSource(url, () => {
      return new DataSource({
        type: "postgres",
        url,
        entities: this.registeredEntities(),
      });
    });
  }
}

MainDatabase.registerEntities(User, Post);

registerEntities() stores each class once, on globalThis, under the database class name, so a bundled copy and the source loaded beside it still see one registry. sharedSource() returns the existing source when the identity matches. A different identity replaces it and closes the previous source when that source is already connected. Register entities before the source is created; an open source keeps the list it was built with.

What is in the box

| Area | Surface | |---|---| | Entities | @Entity, @Column, @PrimaryColumn, @PrimaryGeneratedColumn (increment, uuid, rowid, identity), @CreateDateColumn, @UpdateDateColumn, @DeleteDateColumn, @VersionColumn, @Index, @Unique, entity inheritance | | Relations | @OneToOne, @OneToMany, @ManyToOne, @ManyToMany with @JoinColumn / @JoinTable, eager loading, cascades (insert, update, remove, soft-remove, recover), orphanedRowAction, onDelete / onUpdate | | Connection | DataSource owns the selected native driver client: initialize, destroy, dropDatabase, query; relational drivers also provide synchronize, transaction, repositories and query builders; bring your own client through options.client | | Manager & repository | create, merge, preload, save, remove, softRemove, recover, insert, update, upsert, delete, softDelete, restore, clear, increment, decrement, find*, count*, exists*, sum, average, minimum, maximum, Repository.extend | | Find options | where (objects, arrays, nested relations), select, relations, order, skip / take, withDeleted, loadEagerRelations; operators Equal, Not, In, Like, ILike, Between, MoreThan, LessThan, IsNull, Raw, And, Or, Any, ArrayContains, JsonContains, … | | Query builders | SelectQueryBuilder (joins, Brackets, group / having, pagination, locking, getRawMany, getManyAndCount, streaming-free hydration), InsertQueryBuilder (orUpdate, orIgnore, returning), UpdateQueryBuilder, DeleteQueryBuilder, SoftDeleteQueryBuilder, RelationQueryBuilder (of().add()/remove()/set()/loadMany()) | | Transactions | callback transactions with isolation levels, nested savepoints, QueryRunner for manual control; SQLite transactions are serialised on the single connection | | Schema | SchemaBuilder creates tables, foreign keys, unique constraints and indexes in dependency order (synchronize: true); dropSchema for test databases | | Naming | DefaultNamingStrategy (TypeORM-compatible hashed constraint names), SnakeNamingStrategy, custom INamingStrategy | | Errors | QueryFailedError, EntityNotFoundError, EntityMetadataNotFoundError, TransactionNotStartedError, … all extend DatabaseException | | Key-value | RedisDriver on Bun's native RedisClient; compatible with Redis, Valkey and Dragonfly RESP servers | | Documents | MongoDriver on the official MongoClient; native commands and direct typed client access |

Drivers

| type | Backed by | Notes | |---|---|---| | postgres | Bun SQL (PostgreSQL adapter) | RETURNING, ILIKE, arrays, jsonb, schemas, gen_random_uuid() | | mysql | Bun SQL (MySQL adapter) | ON DUPLICATE KEY UPDATE, LAST_INSERT_ID() reloads, backtick quoting | | sqlite | Bun SQL (SQLite adapter) | file or :memory:, foreign keys on by default, WAL and busy timeout options | | turso | @libsql/client | Remote Turso, local files and embedded replicas with the SQLite ORM dialect | | bunny | @libsql/client/web | Remote Bunny Database over HTTP or WebSocket with the SQLite ORM dialect | | cloudflare | CloudflareDriver with a Worker database binding or the D1 HTTP API | SQLite dialect, prepared statements, schema synchronization and repositories | | clickhouse | @clickhouse/client | HTTP(S), JSONEachRow, typed query parameters, MergeTree synchronization | | redis | Bun RedisClient | Native commands, TLS, reconnects and auto-pipelining; also speaks to Valkey and Dragonfly | | mongodb | mongodb | Native database commands, collection management and typed MongoClient access |

Redis

Redis participates in the same DataSource driver lifecycle as the SQL databases. The initialized client is Bun's native RedisClient, with its fully typed command API, connection management, TLS, reconnect, offline queue and automatic pipelining controls.

import { DataSource } from "@talosjs/database";

const redis = new DataSource({
  type: "redis",
  url: "redis://username:password@localhost:6379",
  connectionTimeout: 5_000,
  enableAutoPipelining: true,
});

await redis.initialize();
await redis.client.set("greeting", "Hello from Bun!");
const greeting = await redis.client.get("greeting");

await redis.destroy();

When the URL is omitted, Bun reads REDIS_URL, then VALKEY_URL, and otherwise connects to its localhost default. Dragonfly uses the same RESP protocol, so it does not need a separate driver. DataSource.query() accepts a Redis command name and argument array for untyped or unsupported commands; use DataSource.client for Bun's typed methods. Relational entities, schema synchronization, repositories and transactions are not supported by the Redis driver.

MongoDB

MongoDB participates in the same DataSource lifecycle through the official Node.js driver. Use the typed client for collection operations, or DataSource.query() for native database commands. The first query argument is the command name; the parameter array contains the command value followed by an optional options document.

const documents = new DataSource({
  type: "mongodb",
  url: "mongodb://localhost:27017/app",
  database: "app",
  poolSize: 10,
});

await documents.initialize();
await documents.client.db("app").collection("events").insertOne({ type: "created" });

const [{ ok }] = await documents.query<{ ok: number }>("ping");
const users = await documents.query("find", ["users", { filter: { active: true } }]);

await documents.destroy();

The URL defaults to mongodb://127.0.0.1:27017. poolSize maps to maxPoolSize, connectTimeoutMS is forwarded, and remaining official client settings go in extra. dropDatabase() enumerates and drops collections. SQL repositories, schema synchronization and the SQL transaction API are not supported by the MongoDB driver; use the native client for document CRUD, indexes and sessions.

Turso

Turso uses the production-ready @libsql/client integration recommended for ORMs. It shares the SQLite dialect, so entities, repositories, query builders and schema synchronization work with remote Turso databases, local files and in-memory databases.

const turso = new DataSource({
  type: "turso",
  url: "libsql://app-organization.turso.io",
  authToken: process.env.TURSO_AUTH_TOKEN,
  entities: [User],
  synchronize: false,
});

await turso.initialize();
const users = await turso.getRepository(User).find();
await turso.destroy();

syncUrl, syncInterval, concurrency, timeout and intMode map directly to the libSQL client; remaining settings go in extra. Talos callback and QueryRunner transactions are rejected because they require one reserved Bun SQL connection. Use turso.client.batch() or turso.client.transaction() when atomic Turso operations are needed.

Bunny

Bunny Database speaks libSQL, so the driver reuses the Turso query path and the SQLite dialect but creates its client from @libsql/client/web, which only uses fetch and WebSocket and therefore also runs in Bunny Edge Scripting and Magic Containers. Copy the URL and generate an access token under Dashboard > Edge Platform > Database > [your database] > Access.

const bunny = new DataSource({
  type: "bunny",
  url: "libsql://01jxyz-app.lite.bunnydb.net",
  authToken: process.env.DATABASE_BUNNY_TOKEN,
  entities: [User],
});

await bunny.initialize();
const users = await bunny.getRepository(User).find();
await bunny.destroy();

@talosjs/app-env reads the URL and token from DATABASE_BUNNY_URL and DATABASE_BUNNY_TOKEN. Only remote libsql:, https:, http:, wss: and ws: URLs are accepted; local files and :memory: need the turso driver. concurrency and intMode map to the client, and remaining settings such as a custom fetch go in extra. Talos transactions are rejected as with Turso; use bunny.client.batch(statements, "write") for atomic batches or bunny.client.transaction("write") for interactive transactions, which lock the database for writes until committed, with a 5-second timeout.

Cloudflare

Pass the Cloudflare binding from the Worker's environment into DataSource. The driver uses Cloudflare prepared statements and SQLite SQL semantics, including numbered ?1, ?2, … placeholders generated by the ORM.

import { DataSource, type ICloudflareDatabase } from "@talosjs/database";

export default {
  async fetch(_request: Request, env: { DB: ICloudflareDatabase }): Promise<Response> {
    const database = new DataSource({
      type: "cloudflare",
      client: env.DB,
      entities: [User],
      synchronize: false,
    });

    await database.initialize();
    const users = await database.getRepository(User).find();
    await database.destroy();

    return Response.json(users);
  },
};

The Worker runtime owns the binding, so destroy() does not close it. Cloudflare does not support connection-scoped BEGIN / COMMIT calls through separate binding invocations; callback transactions and manual QueryRunner transactions therefore fail explicitly, while normal repository writes run without an automatic transaction. Use Cloudflare's batch() API directly when a group of statements must execute sequentially and atomically.

Outside a Worker, leave client out and pass D1 HTTP API settings instead. Each query is then sent to the D1 query endpoint with an API token that has the D1 Write permission (D1 Read is enough for read-only access to an existing database).

const database = new DataSource({
  type: "cloudflare",
  accountId: process.env.DATABASE_CLOUDFLARE_ACCOUNT_ID,
  apiToken: process.env.DATABASE_CLOUDFLARE_API_TOKEN,
  databaseName: "app",
  primaryLocationHint: "weur",
  entities: [User],
});

await database.initialize();
const users = await database.getRepository(User).find();
await database.destroy();

databaseId targets a database directly. Without it, initialize() looks databaseName up by exact name and creates the database when it is missing; jurisdiction (eu, fedramp, us), primaryLocationHint (wnam, enam, weur, eeur, apac, oc) and readReplicationMode (auto, disabled) apply only to that creation. @talosjs/app-env reads each setting from its DATABASE_CLOUDFLARE_* variable. Parameters are encoded the way the binding sends them, booleans as 1 / 0 and binary values as byte arrays, and transactions are rejected in the same way. Every query is an HTTPS round trip to the rate-limited Cloudflare API, so prefer the binding for code running in a Worker.

ClickHouse

Pass the HTTP(S) endpoint and credentials to DataSource. Initialization uses an authenticated SELECT 1 ping, and destroy() closes the official client when the data source created it.

const analytics = new DataSource({
  type: "clickhouse",
  url: "https://cluster.example:8443",
  username: "default",
  password: "secret",
  database: "analytics",
  entities: [Event],
});

await analytics.initialize();

const recent = await analytics.query<{ id: string; occurred_at: string }>(
  "SELECT id, occurred_at FROM events WHERE occurred_at >= $1 ORDER BY occurred_at DESC LIMIT $2",
  ["2026-01-01 00:00:00", 100],
);

The driver translates positional $1 placeholders into ClickHouse typed query parameters. SELECT, SHOW, DESCRIBE and EXPLAIN statements use JSONEachRow; other statements use command(). ORM updates and deletes run as synchronous ALTER TABLE ... UPDATE/DELETE mutations.

With synchronize: true, tables use MergeTree and order by the entity's primary columns. ClickHouse does not enforce the ORM's foreign keys, unique constraints or regular relational indexes, so synchronization omits them. Transactions and upserts are rejected explicitly. Use UUID generated primary columns when the database should create IDs; ClickHouse has no auto-increment equivalent.

Connection controls map as follows: poolSize to max_open_connections, requestTimeoutMS to request_timeout, and clickhouseSettings to clickhouse_settings. compression is passed through directly. Any other official client option can be supplied in extra.

The Bun SQL client

The PostgreSQL, MySQL and SQLite drivers adapt Bun's built-in SQL client: the data source calls new SQL({ adapter }), runs statements through sql.unsafe(text, parameters), reserves a pooled connection with sql.reserve() for each transaction (SQLite serialises them on its single connection) and closes the pool with sql.close() on destroy(). The pool is yours to use directly:

const sql = dataSource.client; // Bun.SQL, available once initialize() ran

const [{ total }] = await sql`SELECT COUNT(*)::int AS total FROM ${sql("users")} WHERE age > ${18}`;

await sql.begin(async (tx) => {
  await tx`UPDATE accounts SET balance = balance - ${100} WHERE id = ${1}`;
});

Pass an existing native client through options.client when it is shared with other code; the data source then neither opens nor closes it. For Bun SQL drivers, pool size, timeouts, TLS, prepare and bigint map onto the Bun options (poolSize, connectTimeoutMS, ssl, prepare, bigint); anything else goes through extra. QueryFailedError keeps the native driver error in driverError and exposes code and sqlState when the adapter reports them.

Raw statements run through Repository.query(), EntityManager.query(), QueryRunner.query() and DataSource.query() bind their parameters the way the query builder does: the driver writes each value in its own format, so an array reaches PostgreSQL as the {a,b} literal WHERE "id" = ANY($1::varchar[]) expects, a dictionary is serialised as JSON and undefined binds as NULL.

const rows = await repository.query(`SELECT * FROM "users" WHERE "id" = ANY($1::varchar[])`, [["u1", "u2"]]);

Coming from TypeORM

The decorators, DataSource, EntityManager, Repository, find options and query-builder APIs follow TypeORM's names, so most call sites port unchanged. Differences worth knowing:

  • Column types must be declared (@Column({ type: "varchar" })); nothing is inferred from reflect-metadata.
  • To-one relation properties that reference a class declared later in the file (or in a circular import) should be typed with RelationType<T> so emitDecoratorMetadata does not evaluate the class too early.
  • synchronize only creates what is missing — it never alters or drops existing tables. Use migrations for that.
  • save() returns the objects it was given, with generated columns filled in; plain objects stay plain unless created through create().
  • Type aliases end with Type and interfaces start with I (DataSourceOptionsType, FindManyOptionsType, INamingStrategy).

Documentation

Read the full documentation at docs.talosjs.com/components/database.

License

MIT