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

@objectstack/driver-sql

v17.2.0

Published

SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex

Readme

@objectstack/driver-sql

SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex.js.

Features

  • Multi-Database Support: PostgreSQL, MySQL, SQLite, and other Knex-supported databases
  • Query Builder: the underlying Knex.js instance is reachable via getKnex()
  • Managed Schema: tables, columns and indexes are reconciled from your object metadata
  • Connection Pooling: Efficient connection management
  • Transactions: Full ACID transaction support
  • Raw SQL: Execute raw SQL when needed
  • Type-Safe: Full TypeScript support with inferred types
  • Production-Ready: Battle-tested Knex.js under the hood

Installation

pnpm add @objectstack/driver-sql

Database-Specific Drivers

pg, mysql2 and tedious are optional peer dependencies — install the one your database needs:

# PostgreSQL
pnpm add pg

# MySQL
pnpm add mysql2

# SQL Server
pnpm add tedious

SQLite needs nothing extra: better-sqlite3 ships as an optional dependency of this package.

Basic Usage

A driver is not a defineStack() key — it is a plugin. Wrap it in DriverPlugin from @objectstack/runtime and list it under plugins.

PostgreSQL

import { defineStack } from '@objectstack/spec';
import { DriverPlugin } from '@objectstack/runtime';
import { SqlDriver } from '@objectstack/driver-sql';

export default defineStack({
  manifest: {
    id: 'com.example.myapp',
    version: '1.0.0',
    type: 'app',
    name: 'My App',
  },
  plugins: [
    new DriverPlugin(
      new SqlDriver({
        client: 'pg',
        connection: {
          host: 'localhost',
          port: 5432,
          user: 'postgres',
          password: process.env.DB_PASSWORD,
          database: 'myapp',
        },
        pool: {
          min: 2,
          max: 10,
        },
      }),
    ),
  ],
});

MySQL

Same plugins entry — only the driver config changes:

import { SqlDriver } from '@objectstack/driver-sql';

const driver = new SqlDriver({
  client: 'mysql2',
  connection: {
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: process.env.DB_PASSWORD,
    database: 'myapp',
  },
});

SQLite

import { SqlDriver } from '@objectstack/driver-sql';

const driver = new SqlDriver({
  client: 'better-sqlite3',
  connection: {
    filename: './data/app.db',
  },
  useNullAsDefault: true,
});

Without writing any code

os dev / os serve build this driver for you from the database URL, so a project that needs no custom driver options can set an environment variable instead of registering a plugin:

OS_DATABASE_URL=postgres://user:pass@localhost:5432/myapp
OS_DATABASE_URL=mysql://user:pass@localhost:3306/myapp
OS_DATABASE_URL=file:./data/app.db

Configuration Options

The exported config type is SqlDriverConfig. It is Knex's own Knex.Config (client, connection, pool, useNullAsDefault, debug, …) plus the four ObjectStack-specific keys below, which are stripped before the config reaches Knex:

import type { SqlDriverConfig } from '@objectstack/driver-sql';

const config: SqlDriverConfig = {
  // ── Knex.Config ──────────────────────────────────────────────────────────
  client: 'pg',
  connection: {
    host: 'localhost',
    port: 5432,
    user: 'postgres',
    password: process.env.DB_PASSWORD,
    database: 'myapp',
  },
  pool: { min: 2, max: 10 },
  debug: false,

  // ── ObjectStack-specific ─────────────────────────────────────────────────

  /** 'managed' (default) reconciles tables from metadata; 'external' is read-only. */
  schemaMode: 'managed',

  /** Dev-only non-destructive auto-reconcile. 'off' (default) only warns. */
  autoMigrate: 'off',

  /** File-backed SQLite journal mode. Defaults to 'wal'. */
  sqliteJournalMode: 'wal',

  /** What to do when a file-backed SQLite target does not exist. Default 'create'. */
  sqliteAbsentFile: 'create',
};

Database Operations

The SQL driver implements the standard ObjectStack driver interface:

import type { IDataDriver } from '@objectstack/spec/contracts';

// `SqlDriver implements IDataDriver` — all standard operations are supported:
// find, findOne, create, update, delete, count

Every driver method takes the object name as its first argument; the query AST that follows carries no object key.

Advanced Queries

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Filters are the ObjectQL filter dialect: `{ field: { $op: value } }`.
const results = await driver.find('crm_opportunity', {
  where: {
    amount: { $gte: 10000 },
    stage: { $in: ['proposal', 'negotiation'] },
  },
  orderBy: [{ field: 'amount', order: 'desc' }],
  limit: 100,
  offset: 0,
});

Schema Management

ObjectStack manages the physical schema from your object metadata — there are no hand-written migration files. In the default schemaMode: 'managed', the driver creates each object's table on first boot and reports drift afterwards.

The physical table name is the object's (namespace-prefixed) name: crm_account is stored in a table called crm_account, sys_user in sys_user. Nothing is prefixed with objectstack_.

Reviewing and applying schema changes

# Dry-run diff of metadata vs the physical database (never mutates)
os migrate plan

# Apply the reconcile
os migrate apply

# Resume an interrupted apply
os migrate resume

Indexes

Declare indexes on the object, not in DDL — the driver materializes them:

import { defineStack } from '@objectstack/spec';

export default defineStack({
  manifest: { id: 'com.example.crm', version: '1.0.0', type: 'app', name: 'CRM' },
  objects: [
    {
      name: 'crm_opportunity',
      fields: {
        account_id: { type: 'lookup', reference: 'crm_account' },
        stage: { type: 'text' },
        amount: { type: 'number' },
      },
      indexes: [
        { fields: ['account_id'] },
        { fields: ['stage'] },
        { name: 'crm_opportunity_created_stage', fields: ['created_at', 'stage'] },
        { fields: ['external_ref'], unique: 'organization' },
      ],
    },
  ],
});

unique: 'organization' makes the constraint one-holder-per-organization; unique: 'global' makes it installation-wide.

Dev-only auto-reconcile

import { SqlDriver } from '@objectstack/driver-sql';

const driver = new SqlDriver({
  client: 'pg',
  connection: process.env.DATABASE_URL,
  // Applies non-destructive alters (relax NOT NULL, widen varchar) at boot.
  // Force-disabled when NODE_ENV === 'production'.
  autoMigrate: 'safe',
});

Transactions

beginTransaction() returns the handle; pass it to every write as options.transaction, then commit() or rollback():

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

const trx = await driver.beginTransaction();
try {
  const account = await driver.create(
    'crm_account',
    { name: 'Acme Corp' },
    { transaction: trx },
  );

  await driver.create(
    'crm_contact',
    { name: 'John Doe', account_id: account.id },
    { transaction: trx },
  );

  await driver.commit(trx);
} catch (error) {
  await driver.rollback(trx);
  throw error;
}

Raw SQL Queries

When ObjectQL isn't sufficient, execute raw SQL with execute():

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Raw query
const rollup = await driver.execute(`
  SELECT
    c.name,
    COUNT(o.id) as opportunity_count,
    SUM(o.amount) as total_revenue
  FROM crm_account c
  LEFT JOIN crm_opportunity o ON o.account_id = c.id
  WHERE o.stage = 'closed_won'
  GROUP BY c.id, c.name
  ORDER BY total_revenue DESC
  LIMIT 10
`);

// Raw query with parameters (prevent SQL injection)
const users = await driver.execute(
  'SELECT * FROM sys_user WHERE email = ?',
  ['[email protected]'],
);

⚠️ Raw SQL bypasses driver-level tenant isolation. The WHERE organization_id = ? predicate that find / update / delete auto-apply is not added to driver.execute() or engine.execute() output. Always include the tenant predicate yourself when running raw queries against tenant-scoped tables.

Tenant Isolation (Row-Level)

When an object declares a tenant field (either explicitly via tenancy.tenantField, or implicitly by having an organization_id field), the driver auto-scopes every CRUD call by the caller's options.tenantId:

| Operation | Scope behavior | |---|---| | find, findOne, count, aggregate | WHERE <tenantField> = :tenantId injected | | update, delete, updateMany, deleteMany, bulkDelete | Same WHERE injected — cross-tenant writes silently no-op | | create, upsert, bulkCreate | <tenantField> auto-injected on each row if absent |

The engine (@objectstack/objectql) threads ExecutionContext.tenantId into options for you; manual driver.find(...) calls can pass { tenantId: '...' } directly.

Declaring the tenant field

import { defineStack } from '@objectstack/spec';

export default defineStack({
  manifest: { id: 'com.example.ws', version: '1.0.0', type: 'app', name: 'Workspace' },
  objects: [
    {
      name: 'ws_item',
      // Custom tenant column (default is 'organization_id')
      tenancy: { enabled: true, tenantField: 'workspace_id' },
      fields: {
        workspace_id: { type: 'text' },
        title: { type: 'text' },
      },
    },
  ],
});

tenancy.strategy and tenancy.crossTenantAccess were removed after spec 15.0 and are now rejected outright — the two supported modes are database-per-tenant (a deployment choice, no object config) and row-level isolation (tenancy.enabled + tenancy.tenantField).

Bypasses (intentional, documented)

| Path | Tenant-scoped? | Why | |---|---|---| | Callers that omit options.tenantId | No | Seed scripts, boot-time installers, admin tooling | | ExecutionContext.isSystem === true | No (auto-bypassTenantAudit) | Kernel-internal mirrors, scheduled hooks | | Explicit organization_id on insert row | Wins | Admin tooling can target a specific tenant | | driver.execute() / engine.execute(sql) | No | Raw SQL is on you | | driver.bulkUpdate | Yes (it loops update) | Same scope as update |

Audit warning

The driver logs one warning per {object}:{op} when a write hits a tenant-scoped object without options.tenantId. Genuine system writes (ExecutionContext.isSystem === true) auto-silence; everything else surfaces as [tenant-audit] ... so missing-context bugs are visible.

Override per call: options.bypassTenantAudit = true. Override globally: OS_TENANT_AUDIT=0.

Database-Specific Features

PostgreSQL Features

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Use PostgreSQL-specific features
const tech = await driver.execute(`
  SELECT * FROM crm_opportunity
  WHERE data @> '{"industry": "Technology"}'::jsonb
`);

// Full-text search
const articles = await driver.execute(`
  SELECT * FROM blog_article
  WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('objectstack')
`);

MySQL Features

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Use MySQL-specific features
const products = await driver.execute(`
  SELECT * FROM shop_product
  WHERE MATCH(name, description) AGAINST ('widget' IN NATURAL LANGUAGE MODE)
`);

Connection Management

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Get underlying Knex instance
const knex = driver.getKnex();

// Check connection — resolves to false rather than throwing
const healthy: boolean = await driver.checkHealth();

// Close all connections
await driver.disconnect();

Performance Optimization

Query Optimization

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Ask the database for the plan behind a query
const plan = await driver.explain('crm_opportunity', {
  where: { stage: 'proposal' },
});

// Partial / covering indexes that the declaration surface does not express are
// issued at the database layer, through the Knex instance.
await driver.getKnex().raw(`
  CREATE INDEX idx_active_opportunities
  ON crm_opportunity(account_id, amount)
  WHERE stage NOT IN ('closed_won', 'closed_lost')
`);

Best Practices

  1. Connection Pooling: Configure appropriate pool size based on load
  2. Schema: Declare fields and indexes in metadata; review changes with os migrate plan
  3. Transactions: Use transactions for multi-step operations
  4. Prepared Statements: Use parameterized queries to prevent SQL injection
  5. Indexes: Declare indexes on frequently queried fields
  6. Monitoring: Monitor slow query logs and connection pool metrics
  7. Backups: Implement regular database backups

Environment-Specific Configuration

import { defineStack } from '@objectstack/spec';
import { DriverPlugin } from '@objectstack/runtime';
import { SqlDriver, type SqlDriverConfig } from '@objectstack/driver-sql';

// config/database.ts
const configs: Record<string, SqlDriverConfig> = {
  development: {
    client: 'better-sqlite3',
    connection: { filename: './data/dev.db' },
    useNullAsDefault: true,
    debug: true,
  },
  test: {
    client: 'better-sqlite3',
    connection: { filename: ':memory:' },
    useNullAsDefault: true,
  },
  production: {
    client: 'pg',
    // `ssl` belongs to the CONNECTION, not the top level of the config.
    connection: {
      connectionString: process.env.DATABASE_URL,
      ssl: { rejectUnauthorized: false },
    },
    pool: { min: 2, max: 10 },
  },
};

export const getDatabaseConfig = (): SqlDriverConfig =>
  configs[process.env.NODE_ENV ?? 'development'] ?? configs.development;

export default defineStack({
  manifest: { id: 'com.example.myapp', version: '1.0.0', type: 'app', name: 'My App' },
  plugins: [new DriverPlugin(new SqlDriver(getDatabaseConfig()))],
});

Troubleshooting

Connection Issues

import type { SqlDriver } from '@objectstack/driver-sql';

declare const driver: SqlDriver;

// Test database connection
if (await driver.checkHealth()) {
  console.log('Database connected successfully');
} else {
  console.error('Database connection failed');
}

Schema Drift

# Review what metadata wants versus what the database has
os migrate plan

# Apply it
os migrate apply

Query Debugging

import { DriverPlugin } from '@objectstack/runtime';
import { SqlDriver } from '@objectstack/driver-sql';

// Enable query logging
const plugin = new DriverPlugin(
  new SqlDriver({
    client: 'pg',
    connection: process.env.DATABASE_URL,
    debug: true, // Log all queries
  }),
);

Deployment

Heroku PostgreSQL

# Heroku automatically provides DATABASE_URL
heroku addons:create heroku-postgresql:hobby-dev

# ObjectStack reads it directly
OS_DATABASE_URL="$DATABASE_URL"

Railway PostgreSQL

# Use Railway's DATABASE_URL
railway up

Vercel PostgreSQL

import { defineStack } from '@objectstack/spec';
import { DriverPlugin } from '@objectstack/runtime';
import { SqlDriver } from '@objectstack/driver-sql';

export default defineStack({
  manifest: { id: 'com.example.myapp', version: '1.0.0', type: 'app', name: 'My App' },
  plugins: [
    new DriverPlugin(
      new SqlDriver({
        client: 'pg',
        // Vercel Postgres pools through this URL.
        connection: process.env.POSTGRES_URL,
      }),
    ),
  ],
});

License

Apache-2.0. See LICENSING.md.

See Also