tbenandClaude Opus 5.5 b8dd453519 Don't reuse closed or mismatched MariaDB clients
grabMariaDBClient reused global.MARIADB_CLIENT whenever the requested
config matched global.CONFIG. It never checked that the client was made
from that config or was still open. init() sets global.CONFIG before
creating the client, so a re-init after a close, or a config change, got
back a closed or wrong client.

- Remember the cache key each client was created with, and compare that
- Wrapping close() drops the client from the cache and unsets
  global.MARIADB_CLIENT, so the next grab or ensureInit makes a new one
- resolveClient goes through grabMariaDBClient instead of repeating the
  global.CONFIG comparison
- Remove the queue test's workaround for this

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 15:46:22 +01:00
2026-06-21 06:18:34 +01:00
2026-06-21 06:18:34 +01:00
2026-07-14 09:13:07 +01:00
2026-10-18 15:43:22 +01:00
2026-10-14 01:47:55 +01:00

Bun MariaDB

@moduletrace/bun-mariadb

A schema-driven MariaDB manager for Bun, featuring automatic schema synchronization, type-safe CRUD, native VECTOR support, HTML sanitization, and TypeScript type definition generation.


Table of Contents


Features

  • Schema-first design — define your database in TypeScript; the library syncs MariaDB to match
  • Automatic migrations — adds/modifies/drops columns, manages indexes & foreign keys, drops removed tables
  • Type-safe CRUD — generic select, insert, update, delete, and raw sql
  • Rich query DSL — filtering, ordering, pagination, joins, grouping, full-text search
  • Native VECTOR columns — MariaDB VECTOR(n) types and VECTOR INDEX support
  • HTML sanitization — fields with html: true are sanitized on insert/update
  • TypeScript codegen — generate .ts type definitions from your schema
  • Zero-config defaults — id, created_at, and updated_at are added to every table automatically

Prerequisites

  • Bun installed
  • A running MariaDB server (11.7+ recommended for native VECTOR support)

Installation

Via the private npm registry

@moduletrace/bun-mariadb is published to a private Gitea npm registry. Configure the @moduletrace scope first:

# .npmrc
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
bun add @moduletrace/bun-mariadb

Via Git

bun add github:moduletrace/bun-mariadb

Environment Variables

Connection settings are read from the environment (not the config file):

Variable Required Description
BUN_MARIADB_SERVER_HOST Yes MariaDB host
BUN_MARIADB_SERVER_USERNAME Yes Database user
BUN_MARIADB_SERVER_PASSWORD Yes Database password
BUN_MARIADB_SERVER_PORT No Port (server default if omitted)
BUN_MARIADB_SERVER_SSL_KEY_PATH No Optional SSL key path (legacy/env)

Example .env:

BUN_MARIADB_SERVER_HOST=127.0.0.1
BUN_MARIADB_SERVER_PORT=3306
BUN_MARIADB_SERVER_USERNAME=root
BUN_MARIADB_SERVER_PASSWORD=secret

Quick Start

1. Create the config file

Create bun-mariadb.config.ts at your project root:

import type { BunMariaDBConfig } from "@moduletrace/bun-mariadb";

const config: BunMariaDBConfig = {
    db_name: "my_app",
    db_dir: "./db",
    typedef_file_path: "./db/types/db.ts",
    // ssl_ca: "./db/ca-cert.pem",
};

export default config;

2. Define your schema

Create ./db/schema.ts (filename is fixed as schema.ts inside db_dir):

import type { BUN_MARIADB_DatabaseSchemaType } from "@moduletrace/bun-mariadb";

const schema: BUN_MARIADB_DatabaseSchemaType = {
    tables: [
        {
            tableName: "users",
            fields: [
                {
                    fieldName: "first_name",
                    dataType: "VARCHAR",
                    dataLength: 255,
                },
                {
                    fieldName: "last_name",
                    dataType: "VARCHAR",
                    dataLength: 255,
                },
                {
                    fieldName: "email",
                    dataType: "VARCHAR",
                    dataLength: 255,
                    unique: true,
                },
                { fieldName: "bio", dataType: "LONGTEXT", html: true },
            ],
        },
    ],
};

export default schema;

3. Sync the schema to MariaDB

bunx bun-mariadb schema

This creates/updates all tables, indexes, and foreign keys to match your schema.

4. Use the CRUD API

import BunMariaDB from "@moduletrace/bun-mariadb";

// Insert
await BunMariaDB.insert({
    table: "users",
    data: [{ first_name: "Alice", email: "[email protected]" }],
});

// Select
const result = await BunMariaDB.select({ table: "users" });
console.log(result.payload);

// Update
await BunMariaDB.update({
    table: "users",
    targetId: 1,
    data: { first_name: "Alicia" },
});

// Delete
await BunMariaDB.delete({ table: "users", targetId: 1 });

Configuration

The config file must be named bun-mariadb.config.ts and placed at the project root.

Field Type Required Description
db_name string Yes MariaDB database name
db_dir string Yes Directory for schema, types, and local artifacts (relative to project root)
db_backup_dir string No Backup directory name, relative to db_dir (default: .backups)
max_backups number No Max backup files to keep (default: 10)
max_exports number No Max export archives to keep (default: 10)
typedef_file_path string No Output path for generated TypeScript types (relative to project root)
db_config object No Extra options passed to Bun's SQL MariaDB adapter
charset string No Database charset (default: utf8mb4)
connection_timeout number No Connection timeout in ms (default: 10000)
ssl_ca string No Path to SSL CA certificate (relative to project root)
ssl_reject_unauthorized boolean No Verify the server TLS certificate (default: true). Set false only for trusted self-signed setups
html_sanitize object No Extra HTML sanitizer allowlists (see HTML Sanitization)

Schema file path is always: {db_dir}/schema.ts.


Schema Definition

Database Schema

interface BUN_MARIADB_DatabaseSchemaType {
    tables: BUN_MARIADB_TableSchemaType[];
    collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci";
}

Table Schema

interface BUN_MARIADB_TableSchemaType {
    tableName: string;
    tableDescription?: string;
    fields: BUN_MARIADB_FieldSchemaType[];
    indexes?: BUN_MARIADB_IndexSchemaType[];
    uniqueConstraints?: BUN_MARIADB_UniqueConstraintSchemaType[];
    parentTableName?: string; // inherit / merge fields from another table
    tableNameOld?: string; // rename: old name triggers ALTER TABLE RENAME
    collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci";
    queue_table?: boolean | BUN_MARIADB_QueueTableOptions; // job queue, see Queue Tables
}

Field Schema

type BUN_MARIADB_FieldSchemaType = {
    fieldName?: string;
    dataType:
        | "CHAR"
        | "VARCHAR"
        | "TEXT"
        | "TINYTEXT"
        | "MEDIUMTEXT"
        | "LONGTEXT"
        | "TINYINT"
        | "SMALLINT"
        | "MEDIUMINT"
        | "INT"
        | "BIGINT"
        | "FLOAT"
        | "DOUBLE"
        | "DECIMAL"
        | "BINARY"
        | "VARBINARY"
        | "BLOB"
        | "TINYBLOB"
        | "MEDIUMBLOB"
        | "LONGBLOB"
        | "DATE"
        | "TIME"
        | "DATETIME"
        | "TIMESTAMP"
        | "YEAR"
        | "BOOLEAN"
        | "UUID"
        | "JSON"
        | "INET6"
        | "ENUM"
        | "SET"
        | "VECTOR";
    primaryKey?: boolean;
    autoIncrement?: boolean;
    notNullValue?: boolean;
    unique?: boolean;
    defaultValue?: string | number;
    defaultValueLiteral?: string; // raw SQL, e.g. "CURRENT_TIMESTAMP"
    onUpdate?: string;
    onUpdateLiteral?: string;
    foreignKey?: BUN_MARIADB_ForeignKeyType;
    dataLength?: string | number; // e.g. VARCHAR length
    decimals?: string | number; // DECIMAL scale
    options?: (string | number)[]; // ENUM / SET values
    isVector?: boolean; // native VECTOR column
    vectorSize?: number; // dimensions (default: 1536)
    // Content mode flags (editor metadata); only `html: true` affects runtime:
    html?: boolean;
    markdown?: boolean;
    plain?: boolean;
    // ...
};

Foreign Key

type BUN_MARIADB_ForeignKeyRule =
    | "CASCADE"
    | "SET NULL"
    | "RESTRICT"
    | "NO ACTION";

interface BUN_MARIADB_ForeignKeyType {
    foreignKeyName?: string;
    destinationTableName?: string;
    destinationTableColumnName?: string;
    cascadeDelete?: boolean; // shorthand for onDelete: "CASCADE"
    cascadeUpdate?: boolean; // shorthand for onUpdate: "CASCADE"
    onDelete?: BUN_MARIADB_ForeignKeyRule; // default RESTRICT
    onUpdate?: BUN_MARIADB_ForeignKeyRule; // default RESTRICT
}
{
    fieldName: "author_id",
    dataType: "BIGINT",
    foreignKey: {
        foreignKeyName: "post_author_fk",
        destinationTableName: "users",
        destinationTableColumnName: "id",
        onDelete: "SET NULL", // column must be nullable
    },
}
  • onDelete / onUpdate take precedence over the cascadeDelete / cascadeUpdate shorthands. The deprecated field-level onDeleteLiteral / onDelete are still honored as the FK's ON DELETE action when foreignKey.onDelete is not set.
  • SET NULL on a notNullValue or primary key column is rejected when the schema syncs.
  • Changing a rule on an existing FK drops and recreates the constraint. NO ACTION and RESTRICT are treated as the same rule, so switching between them causes no churn.
  • Foreign keys are created after every table exists. Self references (e.g. comments.parent_comment_id), forward references and cycles between tables are all supported.

Index

interface BUN_MARIADB_IndexSchemaType {
    indexName?: string;
    indexTableFields?: string[];
    indexType?: "BTREE" | "HASH" | "FULLTEXT" | "SPATIAL" | "VECTOR";
    vectorDistanceMetric?: "euclidean" | "cosine"; // VECTOR indexes only
    comment?: string;
}

Unique Constraint

interface BUN_MARIADB_UniqueConstraintSchemaType {
    constraintName?: string;
    constraintTableFields?: { value: string }[];
}

CLI Commands

CLI binary: bun-mariadb.

schema — Sync database to schema

bunx bun-mariadb schema [options]
Option Description
-t, --typedef Also generate TypeScript type definitions after syncing
--dry-run Print the planned schema changes without applying them
-y, --yes Apply destructive changes (dropped tables / columns) without confirmation

Examples:

# Sync schema only
bunx bun-mariadb schema

# Sync schema and regenerate types
bunx bun-mariadb schema --typedef

# Review planned changes first
bunx bun-mariadb schema --dry-run

Changes that discard data (dropping a table or column, or recreating a VECTOR column whose size changed) are listed and need confirmation before they run. Non-interactive runs (CI, scripts) refuse them unless --yes is passed.

What it does:

  1. Ensures the internal __db_schema_manager__ tracking table exists
  2. Orders tables by foreign-key dependencies
  3. Creates missing tables / surgically updates existing ones
  4. Syncs indexes (including VECTOR INDEX)
  5. Drops tables removed from the schema (skips tables still referenced by FKs)
  6. Optionally writes TypeScript types and a live schema snapshot

typedef — Generate TypeScript types only

bunx bun-mariadb typedef

Writes types to typedef_file_path from config.

backup — SQL dump backup

bunx bun-mariadb backup

Runs mariadb-dump (or mysqldump) against the configured database and writes a timestamped .sql file into db_backup_dir. Old dumps are pruned to max_backups.

Requires mariadb-dump or mysqldump on PATH.

restore — Restore from an SQL dump

bunx bun-mariadb restore

Interactive picker of available .sql backups (newest first). Imports the selected dump via mariadb / mysql into the configured database.

Requires mariadb or mysql on PATH.

export — Portable SQL + schema archive

bunx bun-mariadb export [options]
Option Description
-o, --output Output archive path (.tar.gz or .zip). Defaults to {db_dir}/.exports
-f, --format When --output is omitted: tar.gz (default) or zip

Creates a portable archive containing:

  1. dump.sql — full SQL dump (same options as backup)
  2. schema.ts — the project’s TypeScript schema file from db_dir

Archives are written to {db_dir}/.exports (sibling of the backups directory). Old exports are pruned using max_exports.

Examples:

# Write {db_name}-{timestamp}.tar.gz into {db_dir}/.exports
bunx bun-mariadb export

# Explicit path / format
bunx bun-mariadb export -o ./my-app-export.tar.gz
bunx bun-mariadb export -f zip
bunx bun-mariadb export -o ./my-app-export.zip

Requires mariadb-dump or mysqldump on PATH. Zip output also requires zip.

import — SQL or full export archive

bunx bun-mariadb import [file] [options]
Option Description
[file] Path to a .sql dump or export archive (.tar.gz / .tar / .zip)
--sql-only Archive only: restore SQL, do not overwrite schema.ts
--schema-only Archive only: write schema.ts, do not restore SQL
-y, --yes Archive only: skip the confirmation before writing a .ts/.js schema

Behavior:

  • .sql file — restores SQL into the configured database (same as restore, but non-interactive)
  • Export archive — restores dump.sql and writes schema.ts into db_dir. A .ts/.js schema is executed when loaded, so you are asked to confirm first (non-interactive runs require --yes). Only import archives from sources you trust.
  • No file argument — interactive picker of archives in {db_dir}/.exports

Examples:

# Plain SQL dump
bunx bun-mariadb import ./db/.backups/my_app-1710000000000.sql

# Full export (SQL + schema.ts)
bunx bun-mariadb import ./db/exports/my_app-1710000000000.tar.gz

# Archive: SQL only / schema only
bunx bun-mariadb import ./export.tar.gz --sql-only
bunx bun-mariadb import ./export.tar.gz --schema-only

# Interactive picker (from {db_dir}/.exports)
bunx bun-mariadb import

Requires mariadb or mysql on PATH. Zip archives also require unzip.

admin — Interactive admin tools

bunx bun-mariadb admin

Interactive menu to list tables / inspect data and run SQL.


CRUD API

import BunMariaDB from "@moduletrace/bun-mariadb";

All methods return a DBResponseObject<T>:

{
    success: boolean;
    payload?: T[];           // rows (select / returning)
    single_res?: T;          // first row
    count?: number;
    insert_return?: {
        count?: number;
        last_insert_id?: number;
        affected_rows?: number;
    };
    error?: any;
    msg?: string;
    debug?: any;
}

Select

BunMariaDB.select<T>({ table, query?, count?, targetId? })
Parameter Type Description
table string Table name
query ServerQueryParam<T> Query/filter options (see Query API)
count boolean Return row count instead of rows
targetId string | number Shorthand to filter by id

Examples:

const res = await BunMariaDB.select({ table: "users" });

const res = await BunMariaDB.select({ table: "users", targetId: 42 });

const res = await BunMariaDB.select({
    table: "users",
    query: {
        query: {
            first_name: { value: "Ali", equality: "LIKE" },
        },
    },
});

const res = await BunMariaDB.select({ table: "users", count: true });
console.log(res.count);

const res = await BunMariaDB.select({
    table: "users",
    query: { limit: 10, page: 2 },
});

Insert

BunMariaDB.insert<T>({ table, data, update_on_duplicate? })
Parameter Type Description
table string Table name
data T[] Array of row objects to insert
update_on_duplicate boolean Use ON DUPLICATE KEY UPDATE when unique exists

created_at and updated_at are set automatically to Date.now().

Fields with html: true are sanitized before insert.

const res = await BunMariaDB.insert({
    table: "users",
    data: [
        { first_name: "Alice", last_name: "Smith", email: "[email protected]" },
        { first_name: "Bob", last_name: "Jones", email: "[email protected]" },
    ],
});

console.log(res.insert_return?.last_insert_id);

Update

BunMariaDB.update<T>({ table, data, query?, targetId? })
Parameter Type Description
table string Table name
data Partial<T> Fields to update
query ServerQueryParam<T> WHERE clause conditions
targetId string | number Shorthand to filter by id

A WHERE clause is required. updated_at is set automatically. HTML fields are sanitized.

await BunMariaDB.update({
    table: "users",
    targetId: 1,
    data: { first_name: "Alicia" },
});

await BunMariaDB.update({
    table: "users",
    data: { last_name: "Doe" },
    query: {
        query: {
            email: { value: "[email protected]", equality: "EQUAL" },
        },
    },
});

Delete

BunMariaDB.delete<T>({ table, query?, targetId? })

A WHERE clause is required.

await BunMariaDB.delete({ table: "users", targetId: 1 });

await BunMariaDB.delete({
    table: "users",
    query: {
        query: {
            first_name: { value: "Ben", equality: "LIKE" },
        },
    },
});

Raw SQL

BunMariaDB.sql<T>({ sql, values?, config? })

success reflects whether the query ran; a SELECT with no rows returns success: true and an empty payload.

const res = await BunMariaDB.sql({
    sql: "SELECT * FROM users WHERE id = ?",
    values: [1],
});
console.log(res.payload);

Queue Tables

Set queue_table: true on a table to use it as a job queue. Any number of workers can pull from it at the same time: claims use SELECT ... FOR UPDATE SKIP LOCKED (MariaDB 10.6+), so no job is handed to two workers and workers don't wait on each other.

{
    tableName: "email_jobs",
    queue_table: true,
    fields: [
        { fieldName: "to", dataType: "VARCHAR", dataLength: 255 },
        { fieldName: "subject", dataType: "VARCHAR", dataLength: 255 },
    ],
}

Pass an options object instead of true to set the queue's defaults. Arguments to the queue methods override them.

queue_table: {
    max_attempts: 5,       // attempts before a job is marked `failed` (default 5)
    lease_ms: 60_000,      // how long a claim lasts (default 30s)
    keep_completed: true,  // `ack` marks jobs `done` instead of deleting them (default false)
}

bunx bun-mariadb schema adds these columns, plus the idx_queue_claim and idx_queue_lease indexes. They are managed by the library: declaring a field or index with one of these names on a queue table is an error.

Field Type Description
queue_status ENUM(...) Job state, see below
queue_available_at BIGINT (ms) The job can't be claimed before this time
queue_attempts INT How many times the job has been claimed
queue_lease_expires_at BIGINT (ms) When the current claim expires
queue_lock_token VARCHAR(64) Token of the current claim
queue_last_error TEXT Error from the last failed attempt

Statuses:

Status Meaning
pending Waiting to be claimed. The only status claim picks up
processing Claimed by a worker
done Acknowledged and kept (keep_completed). Otherwise jobs are deleted
failed Used up its attempts (dead letter)
cancelled Taken out of the queue with queue.cancel

The statuses are fixed because the queue's SQL depends on them. For your own job outcomes (sent, bounced, ...), add a field of your own and set it before calling ack.

Worker loop

await BunMariaDB.queue.push({
    table: "email_jobs",
    data: [{ to: "[email protected]", subject: "Hi" }],
    delay_ms: 5_000, // optional
});

const { payload: jobs } = await BunMariaDB.queue.claim({
    table: "email_jobs",
    limit: 10,
    lease_ms: 60_000,
});

for (const job of jobs || []) {
    try {
        await sendEmail(job);
        await BunMariaDB.queue.ack({
            table: "email_jobs",
            id: job.id,
            lock_token: job.queue_lock_token,
        });
    } catch (e: any) {
        await BunMariaDB.queue.fail({
            table: "email_jobs",
            id: job.id,
            lock_token: job.queue_lock_token,
            error: e.message,
            retry_delay_ms: 30_000,
        });
    }
}
Method Description
queue.push({ table, data, delay_ms? }) Insert jobs as pending
queue.claim({ table, limit?, lease_ms?, max_attempts? }) Claim up to limit (default 1) jobs, oldest first, for lease_ms (default 30s)
queue.ack({ table, id, lock_token }) Job done: deletes it, or marks it done with keep_completed
queue.fail({ table, id, lock_token, error?, retry_delay_ms?, max_attempts? }) Back to pending after retry_delay_ms, or failed once max_attempts (default 5) is reached
queue.extend({ table, id, lock_token, lease_ms? }) Extend the lease for a long-running job
queue.cancel({ table, id }) Take a pending job out of the queue. Claimed jobs can't be cancelled

Delivery is at least once. If a worker crashes or runs past its lease, the next claim puts the job back in the queue (or marks it failed on its last attempt), and the next claim gets a new queue_lock_token. After that, ack / fail / extend with the old token return success: false. Make job handlers idempotent. Lease times use the database clock, so worker clock drift doesn't matter.

How regular CRUD treats queue tables:

  • insert / update refuse to set queue_status, queue_attempts, queue_lease_expires_at, queue_lock_token or queue_last_error. Only the queue API changes those.
  • update / delete skip jobs that are processing, so a worker's claimed job can't change or disappear while it runs.
  • select is unchanged. Filter on queue_status to get the rows you want:
// Dead letters
await BunMariaDB.select({
    table: "email_jobs",
    query: { query: { queue_status: { value: "failed" } } },
});

// Everything still in the queue
await BunMariaDB.select({
    table: "email_jobs",
    count: true,
    query: { query: { queue_status: { value: ["pending", "processing"], equality: "IN" } } },
});
  • To retry a dead-lettered job, delete it and push it again, or reset it with raw sql.
  • With keep_completed, done rows stay until you delete them, e.g. delete with a queue_status / updated_at filter.

Query API Reference

query on select / update / delete accepts ServerQueryParam<T>:

type ServerQueryParam<T> = {
    query?: { [key in keyof T]?: ServerQueryObject };
    selectFields?: (keyof T | { fieldName: keyof T; alias?: string })[];
    omitFields?: (keyof T)[];
    limit?: number;
    page?: number;
    offset?: number;
    order?:
        | { field: keyof T; strategy: "ASC" | "DESC" }
        | { field: keyof T; strategy: "ASC" | "DESC" }[];
    searchOperator?: "AND" | "OR";
    join?: ServerQueryParamsJoin[];
    group?: keyof T | { field: keyof T; table?: string } | Array<...>;
    countSubQueries?: ServerQueryParamsCount[];
    fullTextSearch?: {
        fields: (keyof T)[];
        searchTerm: string;
        scoreAlias: string;
    };
};

Equality Operators

Equality SQL Equivalent
EQUAL (default) =
NOT EQUAL !=
LIKE LIKE '%value%'
LIKE_RAW LIKE 'value'
LIKE_LOWER LOWER(field) LIKE '%value%'
NOT LIKE NOT LIKE '%value%'
GREATER THAN >
GREATER THAN OR EQUAL >=
LESS THAN <
LESS THAN OR EQUAL <=
IN IN (...)
NOT IN NOT IN (...)
BETWEEN BETWEEN a AND b
IS NULL IS NULL
IS NOT NULL IS NOT NULL
REGEXP REGEXP
FULLTEXT full-text match
MATCH vector / specialized match
const res = await BunMariaDB.select({
    table: "users",
    query: {
        query: {
            email: { equality: "IS NOT NULL" },
        },
        order: { field: "created_at", strategy: "DESC" },
        limit: 20,
    },
});

JOIN

const res = await BunMariaDB.select({
    table: "posts",
    query: {
        join: [
            {
                joinType: "LEFT JOIN",
                tableName: "users",
                match: { source: "user_id", target: "id" },
                selectFields: ["first_name", "email"],
            },
        ],
    },
});

Vector Support

MariaDB native VECTOR(n) columns and VECTOR INDEX are supported (MariaDB 11.7+).

Schema

{
    tableName: "documents",
    fields: [
        {
            fieldName: "embedding",
            dataType: "VECTOR",
            isVector: true,
            vectorSize: 1536,
        },
        {
            fieldName: "title",
            dataType: "VARCHAR",
            dataLength: 255,
        },
    ],
    indexes: [
        {
            indexName: "idx_documents_embedding",
            indexType: "VECTOR",
            indexTableFields: ["embedding"],
            vectorDistanceMetric: "cosine", // or "euclidean" (default)
        },
    ],
}

Notes:

  • isVector: true or dataType: "VECTOR" maps to VECTOR(n) (n defaults to 1536)
  • Vector columns are created as NOT NULL (required for vector indexes)
  • Dimension / storage changes trigger an automatic table recreate and data copy
  • No special CLI flag is required

Sync

bunx bun-mariadb schema

HTML Sanitization

Fields with explicit html: true are sanitized on insert and update only.

{ fieldName: "body", dataType: "LONGTEXT", html: true }

Other fields (including text that happens to contain HTML tags) are not sanitized.

Extend the allowlist

// bun-mariadb.config.ts
const config: BunMariaDBConfig = {
    db_name: "my_app",
    db_dir: "./db",
    html_sanitize: {
        allowed_tags: ["iframe", "video"],
        allowed_attributes: {
            iframe: ["src", "width", "height", "allow", "allowfullscreen"],
            video: ["src", "controls", "width", "height"],
            "*": ["data-id"],
        },
    },
};

Config values are appended to the built-in defaults (safe rich-text tags/attributes).

style and id attributes (and the button tag) are not allowed by default, since they enable CSS overlays and DOM clobbering. Add them through allowed_attributes / allowed_tags if you need them.


TypeScript Type Generation

bunx bun-mariadb typedef
# or
bunx bun-mariadb schema --typedef

Generates:

  • A const array of table names
  • A type per table (BUN_MARIADB_<DB>_<TABLE>)
  • A union of all table types

Example:

export const BunMariaDBTables = ["users"] as const;

export type BUN_MARIADB_MY_APP_USERS = {
    /** The unique identifier of the record. */
    id?: number;
    /** The time when the record was created. (Unix Timestamp) */
    created_at?: number;
    /** The time when the record was updated. (Unix Timestamp) */
    updated_at?: number;
    first_name?: string;
    last_name?: string;
    email?: string;
};

export type BUN_MARIADB_MY_APP_ALL_TYPEDEFS = BUN_MARIADB_MY_APP_USERS;
import BunMariaDB from "@moduletrace/bun-mariadb";
import type { BUN_MARIADB_MY_APP_USERS } from "./db/types/db";

const res = await BunMariaDB.select<BUN_MARIADB_MY_APP_USERS>({
    table: "users",
});

Default Fields

Every table automatically receives:

Field Type Description
id BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL Unique row identifier
created_at BIGINT Unix timestamp set on insert
updated_at BIGINT Unix timestamp updated on every update

You do not need to declare these in your schema.


Project Structure

bun-mariadb/
├── src/
│   ├── index.ts                        # Main export (BunMariaDB object)
│   ├── commands/
│   │   ├── index.ts                    # CLI entry point
│   │   ├── schema.ts                   # `schema` command
│   │   ├── typedef.ts                  # `typedef` command
│   │   ├── backup.ts                   # `backup` command
│   │   ├── restore.ts                  # `restore` command
│   │   └── admin/                      # Interactive admin tools
│   ├── functions/
│   │   ├── init.ts                     # Config + schema + client loader
│   │   ├── grab-mariadb-client.ts      # Bun SQL MariaDB client
│   │   └── live-schema.ts              # Live schema snapshot I/O
│   ├── lib/
│   │   ├── db-handler.ts               # Shared query runner
│   │   ├── mariadb/
│   │   │   ├── db-select.ts
│   │   │   ├── db-insert.ts
│   │   │   ├── db-update.ts
│   │   │   ├── db-delete.ts
│   │   │   ├── db-sql.ts
│   │   │   └── schema-to-typedef.ts
│   │   └── schema/                     # Schema sync engine (modular)
│   │       ├── create-db-schema.ts
│   │       ├── create-table.ts
│   │       ├── update-table.ts
│   │       ├── sync-indexes.ts
│   │       └── ...
│   ├── types/
│   │   └── index.ts
│   └── utils/
│       ├── sql-generator.ts
│       ├── sql-insert-generator.ts
│       ├── sanitize-html-fields.ts
│       └── ...
└── tests/                              # bun test suite

Testing

bun run test

Starts a throwaway MariaDB container (Docker required, image mariadb:11.8 by default), runs every test file against it, then removes the container. Extra arguments are passed to bun test, e.g. bun run test tests/db.test.ts.

Variable Description
BUN_MARIADB_TEST_IMAGE Docker image to use (MariaDB 11.7+ for VECTOR)
BUN_MARIADB_TEST_DB Skip Docker and use this existing database (with BUN_MARIADB_SERVER_*). The tests create and drop tables, so use a throwaway database.

bun run test:unit (or plain bun test) runs without a database; the integration tests are skipped.


License

MIT

S
Description
No description provided
Readme
956 KiB
Languages
TypeScript 99.6%
Shell 0.4%