noorm SDK
Overview
The noorm SDK provides programmatic access to noorm-managed databases. Use it for:
- Test suites - Reset and seed databases between tests
- Scripts - Data transforms, exports, and automation
- CI/CD - Headless database operations
- SDK generation - Introspect schema to generate types
Installation
The SDK is part of the main noorm package:
import { createContext } from 'noorm/sdk'Quick Start
import { createContext } from 'noorm/sdk'
// Create a typed context for the 'dev' config
const ctx = await createContext<{ users: { id: number; name: string } }>({
config: 'dev',
})
// Connect to the database
await ctx.connect()
// Type-safe queries via Kysely (top-level)
const users = await ctx.kysely
.selectFrom('users')
.select(['id', 'name'])
.execute()
// Noorm operations via namespace
await ctx.noorm.changes.ff()
// Disconnect when done
await ctx.disconnect()API Structure
The Context API is split into two levels:
Top-level — SQL-focused operations you use in application code:
kysely,dialect,connected— propertiesconnect(),disconnect()— lifecycletransaction(),proc(),func()— SQL executionnoorm— namespace for management operations
ctx.noorm — noorm management operations, organized by namespace:
run:build(),file(),files(),dir(),discover(),preview()db:truncate(),teardown(),previewTeardown(),reset(),listTables(),describeTable(),overview()changes:apply(),revert(),ff(),status(),pending(),history(),create(),addFile(),removeFile(),renameFile(),reorderFiles(),delete(),discover(),parse(),validate()lock:acquire(),release(),status(),withLock(),forceRelease()dt:exportTable(),importFile()transfer:to(),plan()templates:render()secrets:get()vault:init(),status(),set(),get(),getAll(),list(),delete(),exists(),propagate(),copy()utils:checksum(),testConnection()- Properties:
config,settings,identity,observer
API Reference
Factory Function
createContext(options)
Creates an SDK context for programmatic database access.
interface CreateContextOptions {
config?: string // Config name (or use NOORM_CONFIG env var)
projectRoot?: string // Project root path (see note below)
requireTest?: boolean // Refuse if config.isTest !== true
allowProtected?: boolean // Allow destructive ops on protected configs
stage?: string // Stage name for stage defaults
}Finding the project root: Unlike the CLI, the SDK does not automatically walk up directories to find the project. Pass
projectRootexplicitly, or use Project Discovery to find it first:typescriptimport { findProjectRoot } from 'noorm/core' const { projectRoot } = findProjectRoot() const ctx = await createContext({ projectRoot })
const ctx = await createContext<MyDatabase>({
config: 'test',
requireTest: true,
})Safety Options:
requireTest: true- ThrowsRequireTestErrorif the config doesn't haveisTest: true. Use this in test suites to prevent accidentally running against production.allowProtected: true- Allows destructive operations (truncate,teardown,reset) on configs withprotected: true. Use with caution.
Environment Variable Support
The SDK supports environment variable overrides and env-only mode for CI/CD.
ENV Overrides
Override any config property via NOORM_* environment variables:
# Override connection host for CI runner
export NOORM_CONNECTION_HOST=db.ci.internal
export NOORM_CONFIG=staging// SDK uses 'staging' config with host overridden
const ctx = await createContext()Priority (highest to lowest):
NOORM_*env vars- Stored config
- Stage defaults
- Defaults
Env-Only Mode (No Stored Config)
In CI pipelines, you can run without any stored configs:
# GitHub Actions
env:
NOORM_CONNECTION_DIALECT: postgres
NOORM_CONNECTION_HOST: ${{ secrets.DB_HOST }}
NOORM_CONNECTION_DATABASE: ${{ secrets.DB_NAME }}
NOORM_CONNECTION_USER: ${{ secrets.DB_USER }}
NOORM_CONNECTION_PASSWORD: ${{ secrets.DB_PASSWORD }}
steps:
- run: node deploy.js// deploy.js - no config name needed
const ctx = await createContext()
await ctx.connect()
await ctx.noorm.changes.ff()
await ctx.disconnect()Minimum required env vars for env-only mode:
NOORM_CONNECTION_DIALECT(postgres, mysql, sqlite, mssql)NOORM_CONNECTION_DATABASE
See the Configuration documentation for the full list of supported environment variables.
Top-Level Properties
| Property | Type | Description |
|---|---|---|
kysely | Kysely<DB> | Direct Kysely access (requires connect()) |
dialect | Dialect | Database dialect (postgres, mysql, etc.) |
connected | boolean | Whether currently connected |
noorm | NoormOps | Noorm management operations (lazy singleton) |
Lifecycle Methods
connect()
Establishes the database connection.
await ctx.connect()disconnect()
Closes the database connection.
await ctx.disconnect()Transactions
transaction<T>(fn)
Execute operations within a database transaction. The callback receives a full Kysely Transaction<DB> with query builder, sql template literal, and all Kysely features.
import { sql } from 'kysely'
const result = await ctx.transaction(async (trx) => {
await trx
.updateTable('accounts')
.set({ balance: sql`balance - ${100}` })
.where('id', '=', 1)
.execute()
await trx
.updateTable('accounts')
.set({ balance: sql`balance + ${100}` })
.where('id', '=', 2)
.execute()
return { transferred: 100 }
})Stored Procedures & Functions
Stored procedures and database functions get their own type-safe methods. Define your signatures as interfaces and pass them as extra generics:
interface MyProcs {
'get_users': { department_id: number; active: boolean }
'refresh_cache': void
}
interface MyFuncs {
'calc_total': { order_id: number }
'get_version': void
}
const ctx = await createContext<MyDB, MyProcs, MyFuncs>({ config: 'dev' })
await ctx.connect()
// Stored procedure — returns result set rows
const users = await ctx.proc<User>('get_users', { department_id: 1, active: true })
// Database function — returns scalar as { column: value }
const result = await ctx.func<{ total: number }>('calc_total', { order_id: 42 }, 'total')
// No-param variants
await ctx.proc('refresh_cache')
const ver = await ctx.func<{ v: string }>('get_version', 'v')Parameter types control the call signature:
- Object → named params where the dialect supports it (MSSQL
@key =, PGkey =>), positional fallback on MySQL - Tuple → always positional
- void → no params required
Both methods throw on SQLite, which has no stored procedure or function call support.
ctx.noorm — Noorm Operations
Properties
| Property | Type | Description |
|---|---|---|
config | Config | The resolved config object |
settings | Settings | Project settings (paths, rules, stages) |
identity | Identity | Current operator identity |
observer | ObserverEngine | Event observer for subscriptions |
Schema Operations
run.build(options?)
Execute all SQL files in the schema directory.
const result = await ctx.noorm.run.build({ force: true })
console.log(`Ran ${result.filesRun} files`)db.truncate()
Wipe all data, keeping the schema intact.
const result = await ctx.noorm.db.truncate()
console.log(`Truncated ${result.truncated.length} tables`)db.teardown()
Drop all database objects except noorm tracking tables.
const result = await ctx.noorm.db.teardown()db.previewTeardown()
Preview what teardown would drop without executing.
const preview = await ctx.noorm.db.previewTeardown()db.reset()
Full rebuild: teardown + build.
await ctx.noorm.db.reset()File Runner
run.file(filepath, options?)
Execute a single SQL file.
await ctx.noorm.run.file('seeds/test-data.sql')
await ctx.noorm.run.file('/absolute/path/to/seed.sql')run.files(filepaths, options?)
Execute multiple SQL files sequentially.
await ctx.noorm.run.files([
'functions/utils.sql',
'triggers/audit.sql',
])run.dir(dirpath, options?)
Execute all SQL files in a directory.
await ctx.noorm.run.dir('seeds/')run.discover(dirpath?)
Discover SQL files in a directory. Defaults to the configured SQL directory.
const files = await ctx.noorm.run.discover('sql/')run.preview(filepaths, output?)
Preview SQL files — render templates without executing.
const results = await ctx.noorm.run.preview(['sql/001.sql', 'sql/002.sql'])Changes
changes.apply(name, options?)
Apply a specific change.
const result = await ctx.noorm.changes.apply('2024-01-15-add-users')changes.revert(name, options?)
Revert a specific change.
const result = await ctx.noorm.changes.revert('2024-01-15-add-users')changes.ff()
Apply all pending changes.
const result = await ctx.noorm.changes.ff()
console.log(`Applied ${result.executed} changes`)changes.status()
Get status of all changes.
const changes = await ctx.noorm.changes.status()
for (const cs of changes) {
console.log(`${cs.name}: ${cs.status}`)
}changes.pending()
Get only pending changes.
const pending = await ctx.noorm.changes.pending()changes.history(limit?)
Get execution history.
const history = await ctx.noorm.changes.history(10)
for (const record of history) {
console.log(`${record.name}: ${record.status} at ${record.executedAt}`)
}Scaffold Methods
These methods work offline (no database connection required) and manage change directories on disk.
changes.create(options)
Create a new change directory with change/ and revert/ folders.
const change = await ctx.noorm.changes.create({ description: 'add-user-roles' })changes.addFile(change, folder, options)
Add a file to a change.
const updated = await ctx.noorm.changes.addFile(change, 'change', {
name: 'create-table',
type: 'sql',
})changes.removeFile(change, folder, filename)
Remove a file from a change.
await ctx.noorm.changes.removeFile(change, 'change', '001_create-table.sql')changes.renameFile(change, folder, oldFilename, newDescription)
Rename a file in a change.
await ctx.noorm.changes.renameFile(change, 'change', '001_old.sql', 'new-name')changes.reorderFiles(change, folder, newOrder)
Reorder files in a change folder.
await ctx.noorm.changes.reorderFiles(change, 'change', ['002_b.sql', '001_a.sql'])changes.delete(change)
Delete a change directory from disk.
await ctx.noorm.changes.delete(change)changes.discover()
Discover all changes on disk.
const changes = await ctx.noorm.changes.discover()changes.parse(name)
Parse a single change from disk by name.
const change = await ctx.noorm.changes.parse('2024-01-15-add-users')changes.validate(change)
Validate a change's structure. Throws ChangeValidationError if invalid.
ctx.noorm.changes.validate(change)Explore
db.listTables()
List all tables in the database.
const tables = await ctx.noorm.db.listTables()
for (const table of tables) {
console.log(`${table.name}: ${table.columnCount} columns`)
}db.describeTable(name, schema?)
Get detailed information about a table.
const detail = await ctx.noorm.db.describeTable('users')
if (detail) {
for (const col of detail.columns) {
console.log(`${col.name}: ${col.dataType}`)
}
}db.overview()
Get database overview with counts of all object types.
const overview = await ctx.noorm.db.overview()
console.log(`Tables: ${overview.tables}, Views: ${overview.views}`)Locks
lock.acquire(options?)
Acquire a database lock.
const lock = await ctx.noorm.lock.acquire({ timeout: 60000 })lock.release()
Release the current lock.
await ctx.noorm.lock.release()lock.status()
Get current lock status.
const status = await ctx.noorm.lock.status()
if (status.isLocked) {
console.log(`Locked by ${status.lock.lockedBy}`)
}lock.withLock(fn, options?)
Execute an operation with automatic lock acquisition and release.
await ctx.noorm.lock.withLock(async () => {
await ctx.noorm.run.build()
await ctx.noorm.changes.ff()
})lock.forceRelease()
Force release any database lock regardless of ownership. Returns true if a lock was released.
await ctx.noorm.lock.forceRelease()Templates
templates.render(filepath)
Render a template file without executing.
const result = await ctx.noorm.templates.render('sql/001_users.sql.tmpl')
console.log(result.sql)Secrets
secrets.get(key)
Get a config-scoped secret.
const apiKey = ctx.noorm.secrets.get('API_KEY')Vault
Encrypted team secrets stored in the database. All operations require a connection. Operations that decrypt secrets require the user's private key.
vault.init()
Initialize the vault for this database.
const [vaultKey, err] = await ctx.noorm.vault.init()vault.status()
Get vault status.
const status = await ctx.noorm.vault.status()vault.set(key, value, privateKey)
Set a vault secret.
const [, err] = await ctx.noorm.vault.set('API_KEY', 'sk-live-...', privateKey)vault.get(key, privateKey)
Get a vault secret by key. Returns null if not found or no vault access.
const value = await ctx.noorm.vault.get('API_KEY', privateKey)vault.getAll(privateKey)
Get all vault secrets.
const all = await ctx.noorm.vault.getAll(privateKey)vault.list()
List all vault secret keys without decrypting values.
const keys = await ctx.noorm.vault.list()vault.delete(key)
Delete a vault secret.
const [deleted, err] = await ctx.noorm.vault.delete('OLD_KEY')vault.exists(key)
Check if a vault secret exists.
const exists = await ctx.noorm.vault.exists('API_KEY')vault.propagate(privateKey)
Propagate vault key to all users without access.
const result = await ctx.noorm.vault.propagate(privateKey)vault.copy(destConfig, keys, privateKey, options?)
Copy vault secrets to another config's database.
const [result, err] = await ctx.noorm.vault.copy(destConfig, ['API_KEY'], privateKey)Utilities
utils.checksum(filepath)
Compute SHA-256 checksum for a file.
const checksum = await ctx.noorm.utils.checksum('sql/001_users.sql')utils.testConnection()
Tests if the connection can be established without actually connecting.
const result = await ctx.noorm.utils.testConnection()
if (!result.ok) {
console.error('Connection failed:', result.error)
}Transfer
transfer.to(destConfig, options?)
Transfer data from this context's database to a destination.
const source = await createContext({ config: 'staging' })
const dest = await createContext({ config: 'dev' })
await source.connect()
await dest.connect()
const [result, err] = await source.noorm.transfer.to(dest.noorm.config, {
tables: ['users', 'posts'],
onConflict: 'skip',
})
await source.disconnect()
await dest.disconnect()transfer.plan(destConfig, options?)
Generate a transfer plan without executing.
const [plan, err] = await source.noorm.transfer.plan(dest.noorm.config)
if (plan) {
console.log(`${plan.estimatedRows} rows across ${plan.tables.length} tables`)
}DT File Operations
dt.exportTable(tableName, filepath, options?)
Export a table to a .dt file. Extension determines format: .dt, .dtz (gzipped), .dtzx (encrypted).
const [result, err] = await ctx.noorm.dt.exportTable('users', './exports/users.dtz')dt.importFile(filepath, options?)
Import a .dt file into the connected database.
const [result, err] = await ctx.noorm.dt.importFile('./exports/users.dtz', {
onConflict: 'skip',
})Event Subscriptions
Subscribe to core events via the observer:
ctx.noorm.observer.on('file:after', (event) => {
console.log(`Executed ${event.filepath} in ${event.durationMs}ms`)
})
ctx.noorm.observer.on('change:complete', (event) => {
console.log(`Change ${event.name}: ${event.status}`)
})Use Cases
Test Suites (Jest/Vitest)
import { createContext, Context } from 'noorm/sdk'
describe('User API', () => {
let ctx: Context
beforeAll(async () => {
ctx = await createContext({ config: 'test', requireTest: true })
await ctx.connect()
await ctx.noorm.db.reset() // Clean slate
})
afterAll(async () => {
await ctx.disconnect()
})
beforeEach(async () => {
await ctx.noorm.db.truncate() // Wipe between tests
})
it('creates a user', async () => {
await ctx.kysely
.insertInto('users')
.values({ name: 'Alice' })
.execute()
const rows = await ctx.kysely
.selectFrom('users')
.selectAll()
.execute()
expect(rows).toHaveLength(1)
})
})Scripts and Tooling
import { createContext } from 'noorm/sdk'
// Data export script
const ctx = await createContext({ config: 'prod' })
await ctx.connect()
const users = await ctx.kysely
.selectFrom('users')
.selectAll()
.where('active', '=', true)
.execute()
await writeJson('users-export.json', users)
await ctx.disconnect()Type Generation
import { createContext } from 'noorm/sdk'
const ctx = await createContext({ config: 'dev' })
await ctx.connect()
const tables = await ctx.noorm.db.listTables()
for (const table of tables) {
const detail = await ctx.noorm.db.describeTable(table.name)
generateTypeDefinition(detail)
}
await ctx.disconnect()CI/CD Pipeline
import { createContext } from 'noorm/sdk'
const ctx = await createContext({ config: process.env.DB_CONFIG })
await ctx.connect()
// Test connection
const test = await ctx.noorm.utils.testConnection()
if (!test.ok) {
console.error('Database not available:', test.error)
process.exit(1)
}
// Apply changes with lock
await ctx.noorm.lock.withLock(async () => {
await ctx.noorm.changes.ff()
})
await ctx.disconnect()Error Handling
import {
createContext,
RequireTestError,
ProtectedConfigError,
LockAcquireError,
} from 'noorm/sdk'
try {
const ctx = await createContext({ config: 'prod', requireTest: true })
} catch (err) {
if (err instanceof RequireTestError) {
console.error('Cannot use production config in tests')
}
}
try {
await ctx.noorm.db.truncate()
} catch (err) {
if (err instanceof ProtectedConfigError) {
console.error('Cannot truncate protected database')
}
}
try {
await ctx.noorm.lock.acquire()
} catch (err) {
if (err instanceof LockAcquireError) {
console.error(`Lock held by ${err.holder}`)
}
}Headless/CI Mode
CLI Headless Commands
noorm supports headless mode for CI/CD pipelines. Use -H or --headless flag:
# Build schema
noorm -H --config dev run/build
# Fast-forward changes
noorm -H --config dev change/ff
# Apply single change
noorm -H --config dev change/run --name 2024-01-15-add-users
# Truncate database
noorm -H --config test db/truncate
# Get JSON output for scripting
noorm -H --json --config dev change/ff | jq '.status'Available Headless Commands
| Route | Description |
|---|---|
run/build | Build schema from SQL files |
run/file | Run single SQL file |
run/dir | Run all files in directory |
db/truncate | Truncate all tables |
db/teardown | Drop all objects |
db/explore | Database overview |
db/explore/tables | List tables |
db/explore/tables/detail | Describe a table |
change | List change status |
change/ff | Apply pending changes |
change/run | Apply single change |
change/revert | Revert single change |
change/history | Execution history |
lock/status | Lock status |
lock/acquire | Acquire lock |
lock/release | Release lock |
Headless Detection
Headless mode is auto-detected when:
--headlessor-Hflag is passedNOORM_HEADLESS=trueenvironment variable- CI environment variables (
CI,GITHUB_ACTIONS,GITLAB_CI, etc.) - No TTY available
Use --tui to force TUI mode in CI environments.
GitHub Actions Example
name: Database Changes
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Apply changes
run: |
npx noorm -H --config ${{ vars.DB_CONFIG }} change/ff
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}TypeScript Support
The SDK is fully typed. Use generics for type-safe Kysely access:
interface Database {
users: {
id: number
name: string
email: string
}
posts: {
id: number
user_id: number
title: string
}
}
const ctx = await createContext<Database>({ config: 'dev' })
await ctx.connect()
// ctx.kysely is now Kysely<Database> - full type safety
const users = await ctx.kysely
.selectFrom('users')
.select(['id', 'name']) // Autocomplete works
.where('email', '=', email) // Type-checked
.execute()