@egi/smart-db
Version:
Unified Smart DB Access
356 lines (265 loc) • 9.71 kB
Markdown
# smart-db
A unified, type-safe ORM abstraction layer for SQLite (better-sqlite3), MySQL, and Oracle. Provides a single API across all three databases with both synchronous and asynchronous access, a composable SQL builder, schema versioning, and optional database-backed logging.
## Installation
```bash
npm install @egi/smart-db
```
Install only the driver(s) you need:
```bash
npm install better-sqlite3 # SQLite
npm install mysql2 # MySQL
npm install oracledb # Oracle
```
## Quick Start
### SQLite
```typescript
import { SmartDbBetterSqlite3 } from "@egi/smart-db/drivers/smart-db-better-sqlite3";
const db = new SmartDbBetterSqlite3(
{ filename: "./my-database.db" },
{ module: "my-app", onReady: (db, err) => { /* ... */ } }
);
```
### MySQL
```typescript
import { SmartDbMysql } from "@egi/smart-db/drivers/smart-db-mysql";
const db = new SmartDbMysql(
{ host: "localhost", user: "root", password: "secret", database: "mydb" },
{ module: "my-app", onReady: (db, err) => { /* ... */ } }
);
```
### Oracle
```typescript
import { SmartDbOracle } from "@egi/smart-db/drivers/smart-db-oracle";
const db = new SmartDbOracle(
{ user: "hr", password: "secret", connectString: "localhost/XEPDB1" },
{ module: "my-app", onReady: (db, err) => { /* ... */ } }
);
```
## Waiting for the Database
All drivers signal readiness asynchronously via an RxJS `BehaviorSubject`. Use `databaseReady()` before running queries, or pass `onReady` in options.
```typescript
await db.databaseReady();
// or subscribe to state changes:
db.onReady.subscribe(state => console.log(state));
```
## Models
Models map TypeScript classes to database tables. Generate them from a live schema using the CLI (see [Schema Extraction](#schema-extraction)), or write them manually:
```typescript
import { AbstractModel, ModelAttributeMap } from "@egi/smart-db";
interface UserData {
user_id: number;
user_name: string;
}
class UserModel extends AbstractModel<UserModel, UserData> {
static readonly attributeMap: ModelAttributeMap = {
id: { attribute: "_id", alias: "user_id", type: "number", typeScriptStyle: true },
name: { attribute: "_name", alias: "user_name", type: "string", typeScriptStyle: true },
};
static getTableName() { return "users"; }
static getPrimaryKey() { return "user_id"; }
static getClassName() { return "UserModel"; }
static getPkSequenceName(){ return ""; }
static from(other: any) { const m = new UserModel(); m.assign(other); return m; }
private _id: number;
private _name: string;
get id() { return this._id; }
set id(v) { this._id = v; }
get name() { return this._name; }
set name(v){ this._name = v; }
clone() { return UserModel.from(this); }
getClassName() { return UserModel.getClassName(); }
getTableName() { return UserModel.getTableName(); }
getPrimaryKey() { return UserModel.getPrimaryKey(); }
getPkSequenceName(){ return UserModel.getPkSequenceName(); }
getAttributeMap() { return UserModel.attributeMap; }
}
```
## CRUD Operations
All methods have async and sync variants. Sync variants return `false` on error; async variants throw. SQLite supports both; MySQL and Oracle are async-only.
### Insert
```typescript
const newId = await db.insert(UserModel, { name: "Alice" });
const newId = db.insertSync(UserModel, { name: "Alice" }); // SQLite only
```
### Query
```typescript
// All rows
const users = await db.getAll(UserModel);
// With WHERE
const admins = await db.getAll(UserModel, { role: "admin" });
// First match
const user = await db.getFirst(UserModel, { id: 42 });
// Full options
const results = await db.get(UserModel, {
where: { status: "active" },
orderBy: ["name asc"],
limit: { limit: 10, offset: 20 },
});
```
### Update
```typescript
const affected = await db.update(UserModel, { name: "Bob" }, { id: 42 });
```
### Delete
```typescript
const deleted = await db.delete(UserModel, { id: 42 });
```
### Raw SQL
```typescript
const rows = await db.query("SELECT * FROM users WHERE id = ?", [42]);
await db.exec("CREATE INDEX idx_name ON users(name)");
```
## WHERE Clauses
WHERE conditions are plain objects. Keys are model attribute names; values can be literals or operator descriptors from `smart-db-globals`.
```typescript
import { GT, LT, IN, LIKE, IS_NULL, BETWEEN, NE } from "@egi/smart-db";
// Simple equality
const where = { status: "active" };
// Operators
const where = {
age: GT(18),
score: BETWEEN(80, 100),
name: LIKE("%smith%"),
role: IN(["admin", "editor"]),
deletedAt: IS_NULL(),
};
// Nested AND / OR
const where = {
and: [
{ status: "active" },
{ or: [{ role: "admin" }, { role: "editor" }] },
],
};
```
## SQL Helper Functions
Imported from `@egi/smart-db` (all re-exported from `smart-db-globals`):
| Function | SQL equivalent |
|---|---|
| `GT(v)` | `> v` |
| `GE(v)` | `>= v` |
| `LT(v)` | `< v` |
| `LE(v)` | `<= v` |
| `NE(v)` | `!= v` |
| `IN([...])` | `IN (...)` |
| `NOT_IN([...])` | `NOT IN (...)` |
| `LIKE(v)` | `LIKE v` |
| `NOT_LIKE(v)` | `NOT LIKE v` |
| `IS_NULL()` | `IS NULL` |
| `IS_NOT_NULL()` | `IS NOT NULL` |
| `BETWEEN(min, max)` | `BETWEEN min AND max` |
| `LITERAL(expr)` | raw SQL fragment |
| `COUNT(field?, alias?)` | `COUNT(*)` / `COUNT(field)` |
| `SUM / MIN / MAX / AVG` | aggregate functions |
| `COALESCE([...], alias?)` | `COALESCE(...)` |
| `FIELD(name, alias?)` | column reference |
| `VALUE(val, alias?)` | scalar value in SELECT |
## Advanced Queries
### Aggregates and Field Selection
```typescript
import { COUNT, SUM, FIELD } from "@egi/smart-db";
const stats = await db.get(UserModel, {
fields: [COUNT("*", "total"), SUM("score", "totalScore")],
groupBy: "role",
});
```
### UNION / INTERSECT / MINUS
```typescript
const results = await db.get(UserModel, {
where: { role: "admin" },
union: [{ model: UserModel, where: { role: "superadmin" } }],
orderBy: "name asc",
});
```
### Distinct and Count
```typescript
const count = await db.get(UserModel, { count: true });
const unique = await db.get(UserModel, { distinct: true, fields: "role" });
```
## Transactions
```typescript
try {
await db.insert(OrderModel, order);
await db.insert(OrderLineModel, line);
await db.commit();
} catch (err) {
await db.rollback();
}
```
## Schema Versioning
SmartDB tracks schema versions per module. SQL upgrade scripts are picked up automatically on init:
```
sql/
my-app-init.sql # Run once on first init
my-app-update.001.sql # Applied in order when version is outdated
my-app-update.002.sql
```
Pass `sqlFilesDirectory` and `module` in options:
```typescript
new SmartDbBetterSqlite3(config, {
module: "my-app",
sqlFilesDirectory: "./sql",
});
```
Use `skipAutoUpgrade: true` to disable automatic execution.
## Schema Extraction
Generate typed model files from a live database:
```bash
# SQLite
npm run extract:sqlite:api
# Oracle
npm run extract:oracle:api
# Arbitrary (via CLI)
extract-db-api --database mydb --username user --password pass
```
## Logging
```typescript
import { SmartSeverityLevel } from "@egi/smart-db";
new SmartDbBetterSqlite3(config, {
logOptions: {
level: SmartSeverityLevel.Info,
dbLogging: true, // also persist logs to smart_db_log table
},
silent: true, // suppress all console output (Fatal only)
});
```
## Browser Usage
The browser entry point exports only `AbstractModel` and `SmartDbDictionary` — no drivers, no Node.js APIs:
```typescript
import { AbstractModel } from "@egi/smart-db"; // resolves to smart-db-browser.js
```
## Date / Time Handling
SmartDB applies a consistent timezone rule across all three drivers, controlled by the `dateTimeMode` option:
| Mode | TIMESTAMP columns | DATE / DATETIME columns |
|---|---|---|
| `"rule"` *(default)* | stored as UTC | stored as local time |
| `"utc"` | stored as UTC | stored as UTC |
| `"local"` | stored as local time | stored as local time |
| `"none"` | no conversion (driver default) | no conversion (driver default) |
```typescript
// Store all dates in UTC
new SmartDbMysql(config, { dateTimeMode: "utc" });
// Disable conversion entirely (e.g. when the DB session already handles it)
new SmartDbOracle(config, { dateTimeMode: "none" });
```
The MySQL driver issues `SET time_zone = '+00:00'` on connect for `"rule"` and `"utc"` modes. For `"local"` and `"none"` it does not, preserving the server's default timezone.
## Date Utilities
```typescript
import { toSmartDbDate, toSmartDbTimestamp, smartDbToDate } from "@egi/smart-db";
toSmartDbDate(new Date()) // "2024-03-15 14:30:00"
toSmartDbTimestamp(new Date()) // "2024-03-15 14:30:00.000"
smartDbToDate("2024-03-15 14:30:00") // Date object
```
## Options Reference
| Option | Type | Description |
|---|---|---|
| `module` | `string \| string[]` | Module name(s) for schema versioning |
| `sqlFilesDirectory` | `string` | Directory for SQL upgrade scripts |
| `onReady` | `(db, err?) => void` | Callback when DB reaches READY or ERROR |
| `delayInit` | `boolean` | Skip `initDb()` in constructor; call manually |
| `connectOnly` | `boolean` | Connect without running upgrade scripts |
| `skipAutoUpgrade` | `boolean` | Skip schema version checks on init |
| `smartDbDictionary` | `typeof SmartDbDictionary` | Register model dictionaries |
| `silent` | `boolean` | Suppress all logging below Fatal |
| `needsExplicitEscape` | `boolean` | Enable explicit escaping (Oracle) |
| `dateTimeMode` | `SmartDbDateTimeMode` | Date/time timezone rule — see [Date / Time Handling](#date--time-handling) |
| `logOptions` | `SmartLogOptions` | Logging configuration |