UNPKG

@egi/smart-db

Version:

Unified Smart DB Access

356 lines (265 loc) 9.71 kB
# 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 |