├── .gitignore ├── README.md ├── db.ts ├── icon.png ├── package-lock.json ├── package.json ├── src └── index.ts ├── test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | blogpost.md 2 | dist 3 | Bruv-migrations 4 | tests.ts 5 | Database.db 6 | .env 7 | node_modules 8 | TODO.md 9 | Database.sqlite 10 | Bruv-migrations -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLiteBruv Query Builder 2 | 3 | A Tiny Type-Safe, Secure SQLite Query Builder with D1/Turso support with built-in migrations and security features. 4 | 5 | [![npm version](https://badge.fury.io/js/sqlitebruv.svg)](https://www.npmjs.com/package/sqlitebruv) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![npm](https://img.shields.io/npm/dm/sqlitebruv.svg)](https://www.npmjs.com/package/sqlitebruv) [![TypeScript](https://img.shields.io/badge/Typescript-%3E%3D4.0-blue.svg)](https://www.typescriptlang.org/) 7 | 8 | ## Features 9 | 10 | - 🛡️ Security-first design with SQL injection prevention 11 | - 📡 JSON interface for http no sql queries 12 | - 🔄 Automatic schema migrations 13 | - 🏃‍♂️ In-memory caching 14 | - 🌐 Cloudflare D1 & Turso support 15 | - 📝 Type-safe queries 16 | - 🔍 Query validation & sanitization 17 | - 📊 Schema management 18 | - 🌠 Bunjs Support 100% 19 | 20 |
21 | 22 |
23 | ## Installation 24 | 25 | ```bash 26 | npm install sqlite-bruv 27 | ``` 28 | 29 | ## 🚀 Updates 30 | 31 | - **Light weight**: Zero dependency and small size. 32 | - **Bun-Ready**: built for Bunjs 33 | - **Platform Support**: 34 | - Cloudflare D1 35 | - Turso 36 | - Local SQLite 37 | - raw query output 38 | - **Security**: SQL injection prevention, query validation, parameter sanitization 39 | - **Type Safety**: Full TypeScript support with inferred types 40 | - **Migrations**: Automatic schema diff detection and migration generation 41 | - **Caching**: Built-in memory caching with invalidation 42 | - **Relations**: Support for one-to-one and one-to-many relationships 43 | 44 | ## 📦 Installation 45 | 46 | ```bash 47 | # bun 48 | bun add sqlite-bruv 49 | 50 | # npm 51 | npm install sqlite-bruv 52 | ``` 53 | 54 | ## Usage/Examples 55 | 56 | ```typescript 57 | import { SqliteBruv, Schema } from "sqlite-bruv"; 58 | 59 | // Define your schema 60 | const UserSchema = new Schema<{ 61 | name: string; 62 | email: string; 63 | role: "admin" | "user"; 64 | createdAt: Date; 65 | }>({ 66 | name: "users", 67 | columns: { 68 | name: { type: "TEXT", required: true }, 69 | email: { type: "TEXT", unique: true }, 70 | role: { type: "TEXT", default: () => "user" }, 71 | createdAt: { type: "DATETIME", default: () => new Date() }, 72 | }, 73 | }); 74 | 75 | const PostSchema = new Schema({ 76 | name: "posts", 77 | columns: { 78 | title: { type: "TEXT", required: true }, 79 | content: { type: "TEXT" }, 80 | userId: { 81 | type: "TEXT", 82 | target: "users", 83 | relationType: "ONE", 84 | }, 85 | }, 86 | }); 87 | 88 | const CommentSchema = new Schema({ 89 | name: "comments", 90 | columns: { 91 | content: { type: "TEXT", required: true }, 92 | postId: { 93 | type: "TEXT", 94 | target: "posts", 95 | relationType: "MANY", 96 | }, 97 | }, 98 | }); 99 | 100 | // Initialize database 101 | 102 | const db = new SqliteBruv({ 103 | schema: [UserSchema], 104 | }); 105 | ``` 106 | 107 | Platform-Specific Setup 108 | Cloudflare D1 109 | 110 | ```typescript 111 | const db = new SqliteBruv({ 112 | D1: { 113 | accountId: process.env.CF_ACCOUNT_ID, 114 | databaseId: process.env.D1_DATABASE_ID, 115 | apiKey: process.env.CF_API_KEY, 116 | }, 117 | schema: [UserSchema, PostSchema, CommentSchema], 118 | }); 119 | ``` 120 | 121 | Turso; 122 | 123 | ```typescript 124 | const db = new SqliteBruv({ 125 | turso: { 126 | url: process.env.TURSO_URL, 127 | authToken: process.env.TURSO_AUTH_TOKEN, 128 | }, 129 | schema: [UserSchema, PostSchema, CommentSchema], 130 | }); 131 | ``` 132 | 133 | ## Example usage: 134 | 135 | ```typescript 136 | const queryBuilder = new SqliteBruv({ 137 | schema: [UserSchema, PostSchema, CommentSchema], 138 | }); 139 | 140 | // Insert 141 | await queryBuilder 142 | .from("users") 143 | .insert({ name: "John Doe", email: "john@example.com" }) 144 | .then((changes) => { 145 | // console.log({ changes }); 146 | }); 147 | 148 | // Update 149 | await queryBuilder 150 | .from("users") 151 | .where("id = ?", 1) 152 | .update({ name: "Jane Doe" }) 153 | .then((changes) => { 154 | // console.log({ changes }); 155 | }); 156 | 157 | // Search 158 | await queryBuilder 159 | .from("users") 160 | .where("id = ?", 1) 161 | .andWhere("name LIKE ?", `%oh%`) 162 | .get() 163 | .then((changes) => { 164 | // console.log({ changes }); 165 | }); 166 | 167 | // Delete 168 | await queryBuilder 169 | .from("users") 170 | .where("id = ?", 1) 171 | .delete() 172 | .then((changes) => { 173 | console.log({ changes }); 174 | }); 175 | 176 | // Get all users 177 | queryBuilder 178 | .from("users") 179 | .get() 180 | .then((changes) => { 181 | // console.log({ changes }); 182 | }); 183 | 184 | // Get one user 185 | await queryBuilder 186 | .from("users") 187 | .where("id = ?", 1) 188 | .getOne() 189 | .then((changes) => { 190 | // console.log({ changes }); 191 | }); 192 | 193 | // Select specific columns 194 | await queryBuilder 195 | .from("users") 196 | .select("id", "name") 197 | .get() 198 | .then((changes) => { 199 | // console.log({ changes }); 200 | }); 201 | 202 | // Where conditions 203 | await queryBuilder 204 | .from("users") 205 | .where("age > ?", 18) 206 | .get() 207 | .then((changes) => { 208 | // console.log({ changes }); 209 | }); 210 | 211 | // AndWhere conditions 212 | await queryBuilder 213 | .from("users") 214 | .where("age > ?", 18) 215 | .andWhere("country = ?", "USA") 216 | .get() 217 | .then((changes) => { 218 | // console.log({ changes }); 219 | }); 220 | 221 | // OrWhere conditions 222 | await queryBuilder 223 | .from("users") 224 | .where("age > ?", 18) 225 | .orWhere("country = ?", "Canada") 226 | .get() 227 | .then((changes) => { 228 | // console.log({ changes }); 229 | }); 230 | 231 | // Limit and Offset 232 | await queryBuilder 233 | .from("users") 234 | .limit(10) 235 | .offset(5) 236 | .get() 237 | .then((changes) => { 238 | // console.log({ changes }); 239 | }); 240 | 241 | // OrderBy 242 | await queryBuilder 243 | .from("users") 244 | .orderBy("name", "ASC") 245 | .get() 246 | .then((changes) => { 247 | // console.log({ changes }); 248 | }); 249 | 250 | await queryBuilder 251 | .from("users") 252 | .orderBy("name", "ASC") 253 | .get() 254 | .then((changes) => { 255 | // console.log({ changes }); 256 | }); 257 | ``` 258 | 259 | ## 💡 Advanced Usage 260 | 261 | Complex Queries 262 | 263 | ```ts 264 | // Relations and joins 265 | const posts = await db 266 | .from("posts") 267 | .select("posts.*", "users.name as author") 268 | .where("posts.published = ?", true) 269 | .andWhere("posts.views > ?", 1000) 270 | .orderBy("posts.createdAt", "DESC") 271 | .limit(10) 272 | .get(); 273 | 274 | // Raw queries with safety 275 | await db.raw("SELECT * FROM users WHERE id = ?", [userId]); 276 | 277 | // Cache usage 278 | const users = await db 279 | .from("users") 280 | .select("*") 281 | .where("active = ?", true) 282 | .cacheAs("active-users") 283 | .get(); 284 | 285 | // Cache invalidation 286 | db.invalidateCache("active-users"); 287 | ``` 288 | 289 | ## Using from over the network via JSON interface 290 | 291 | ```typescript 292 | // JSON interface structure 293 | interface Query { 294 | from: string; 295 | select?: string[]; 296 | where?: { 297 | condition: string; 298 | params: any[]; 299 | }[]; 300 | andWhere?: { 301 | condition: string; 302 | params: any[]; 303 | }[]; 304 | orWhere?: { 305 | condition: string; 306 | params: any[]; 307 | }[]; 308 | orderBy?: { 309 | column: string; 310 | direction: "ASC" | "DESC"; 311 | }; 312 | limit?: number; 313 | offset?: number; 314 | cacheAs?: string; 315 | invalidateCache?: string; 316 | action?: "get" | "getOne" | "insert" | "update" | "delete" | "count"; 317 | /** 318 | ### For insert and update only 319 | */ 320 | data?: any; 321 | } 322 | // Example usage in an Express.js route 323 | import express from "express"; 324 | const app = express(); 325 | app.use(express.json()); 326 | 327 | app.post("/execute-query", async (req, res) => { 328 | try { 329 | const queryInput = req.body; 330 | // do your role authentication here, 331 | // use query.from to know the table being accessed 332 | const result = await qb.executeJsonQuery(queryInput); 333 | res.json(result); 334 | } catch (error) { 335 | res.status(500).json({ error: error.message }); 336 | } 337 | }); 338 | ``` 339 | 340 | ## 🔄 Migrations 341 | 342 | Migrations are automatically generated when schema changes are detected: 343 | 344 | ```sql 345 | -- Generated in ./Bruv-migrations/timestamp_add_user_role.sql: 346 | -- Up 347 | ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'; 348 | 349 | -- Down 350 | ALTER TABLE users DROP COLUMN role; 351 | ``` 352 | 353 | ### Setting up your schema 354 | 355 | This if your DB is new and your are not using any orm, just call toString 356 | and query your db with the queryBuilder.raw() method. 357 | 358 | Note: raw is not secured, it can be used to apply migrations too. 359 | be careful what you do with queryBuilder.raw(). 360 | 361 | ```ts 362 | console.log(user.toString()); 363 | const raw = await qb.raw(user.toString()); 364 | console.log({ raw }); 365 | ``` 366 | 367 | ## 🛡️ Security Features 368 | 369 | The query builder implements several security measures to prevent SQL injection and malicious queries: 370 | 371 | - Parameter validation (max 100 params) 372 | - SQL injection prevention 373 | - Query timeout limits 374 | - Rate limiting 375 | - String length validation 376 | - Dangerous pattern detection 377 | - Allowed parameter types: string, number, boolean, null 378 | 379 | #### Condition Validation 380 | 381 | - Whitelisted operators: `=, >, <, >=, <=, LIKE, IN, BETWEEN, IS NULL, IS NOT NULL` 382 | - Blocked dangerous patterns: `; DROP, DELETE, UPDATE, INSERT, ALTER, EXEC, UNION` 383 | - Parameterized queries enforced 384 | 385 | ### Security Examples 386 | 387 | ```typescript 388 | // ✅ Safe queries 389 | db.from("users") 390 | .where("email LIKE ?", "%@example.com") // ✅ Safe 391 | .andWhere("role = ?", "admin") // ✅ Safe 392 | .get(); 393 | db.from("users") 394 | .where("age > ?", 18) 395 | .andWhere("status = ?", "active") 396 | .orWhere("role IN (?)", ["admin", "mod"]); 397 | 398 | // ❌ These will throw security errors: 399 | db.where("1=1; DROP TABLE users;"); // Dangerous pattern 400 | db.where("col = (SELECT ...)"); // Complex subqueries blocked 401 | db.where("name = ?", "a".repeat(1001)); // String too long 402 | ``` 403 | 404 | ## 🎮 Features 405 | 406 | **Cloudflare D1** 407 | 408 | - D1 API integration 409 | 410 | **Turso** 411 | 412 | - HTTP API support 413 | 414 | **📊 Performance** 415 | 416 | - Prepared statements 417 | - Connection pooling 418 | - Built-in Query caching 419 | 420 | **🚔 Security** 421 | 422 | - Block dangerous patterns 423 | - Block Complex subqueries 424 | - Block very long string parameters 425 | 426 | **🤝 Contributing** 427 | 428 | 1. Fork the repository 429 | 2. Create feature branch (git checkout -b feature/amazing) 430 | 3. Commit changes (git commit -am 'Add amazing feature') 431 | 4. Push branch (git push origin feature/amazing) 432 | 5. Open a Pull Request 433 | 434 | ## 📝 License 435 | 436 | [MIT License](https://choosealicense.com/licenses/mit/) - see LICENSE file 437 | 438 | ### 🆘 Support 439 | 440 | Contributions are always welcome! creates issues and pull requests. 441 | Documentation 442 | GitHub Issues 443 | Discord Community 444 | -------------------------------------------------------------------------------- /db.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SqliteBruv } from "./src/index.ts"; 2 | // Example usage: 3 | 4 | export const user = new Schema<{ 5 | name: string; 6 | username: string; 7 | location: string; 8 | age: number; 9 | createdAt: Date; 10 | }>({ 11 | name: "users", 12 | columns: { 13 | name: { type: "TEXT", required: true }, 14 | username: { type: "TEXT", required: true, unique: true }, 15 | age: { type: "INTEGER", required: true }, 16 | location: { 17 | type: "TEXT", 18 | required: true, 19 | default() { 20 | return "earth"; 21 | }, 22 | }, 23 | createdAt: { 24 | type: "DATETIME", 25 | default() { 26 | return "CURRENT_TIMESTAMP"; 27 | }, 28 | }, 29 | }, 30 | }); 31 | export const works = new Schema<{ 32 | name: string; 33 | user: string; 34 | createdAt: Date; 35 | rating: number; 36 | }>({ 37 | name: "works", 38 | columns: { 39 | name: { 40 | // unique: true, 41 | type: "TEXT", 42 | required: true, 43 | }, 44 | user: { 45 | type: "TEXT", 46 | required: true, 47 | target: "users", 48 | }, 49 | rating: { 50 | type: "INTEGER", 51 | default() { 52 | return "1"; 53 | }, 54 | check: ["1", "1.5", "2", "2.5", "3", "3.5", "4", "4.5", "5"], 55 | }, 56 | createdAt: { 57 | type: "DATETIME", 58 | default() { 59 | return "CURRENT_TIMESTAMP"; 60 | }, 61 | }, 62 | }, 63 | }); 64 | 65 | export const db = new SqliteBruv({ 66 | schema: [user, works], 67 | // QueryMode: true, 68 | TursoConfig: { 69 | url: process.env.TURSO_URL!, 70 | authToken: process.env.TURSO_AUTH_TOKEN!, 71 | }, 72 | // D1Config: { 73 | // accountId: process.env.CFAccountId!, 74 | // databaseId: process.env.D1databaseId!, 75 | // apiKey: process.env.CFauthorizationToken!, 76 | // }, 77 | // localFile: "sample.sqlite", 78 | logging: true, 79 | }); 80 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeDynasty-dev/SQLiteBruv/a30e637aed15f0fd0c5d15fb70c3a40aa8f4c5d3/icon.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlitebruv", 3 | "version": "1.1.15", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sqlitebruv", 9 | "version": "1.1.15", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/bun": "^1.1.14", 13 | "@types/node": "^22.10.2" 14 | } 15 | }, 16 | "node_modules/@types/bun": { 17 | "version": "1.1.14", 18 | "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.1.14.tgz", 19 | "integrity": "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA==", 20 | "dev": true, 21 | "license": "MIT", 22 | "dependencies": { 23 | "bun-types": "1.1.37" 24 | } 25 | }, 26 | "node_modules/@types/node": { 27 | "version": "22.10.2", 28 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", 29 | "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", 30 | "dev": true, 31 | "license": "MIT", 32 | "dependencies": { 33 | "undici-types": "~6.20.0" 34 | } 35 | }, 36 | "node_modules/@types/ws": { 37 | "version": "8.5.13", 38 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", 39 | "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", 40 | "dev": true, 41 | "license": "MIT", 42 | "dependencies": { 43 | "@types/node": "*" 44 | } 45 | }, 46 | "node_modules/bun-types": { 47 | "version": "1.1.37", 48 | "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.37.tgz", 49 | "integrity": "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA==", 50 | "dev": true, 51 | "license": "MIT", 52 | "dependencies": { 53 | "@types/node": "~20.12.8", 54 | "@types/ws": "~8.5.10" 55 | } 56 | }, 57 | "node_modules/bun-types/node_modules/@types/node": { 58 | "version": "20.12.14", 59 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz", 60 | "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==", 61 | "dev": true, 62 | "license": "MIT", 63 | "dependencies": { 64 | "undici-types": "~5.26.4" 65 | } 66 | }, 67 | "node_modules/bun-types/node_modules/undici-types": { 68 | "version": "5.26.5", 69 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 70 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 71 | "dev": true, 72 | "license": "MIT" 73 | }, 74 | "node_modules/undici-types": { 75 | "version": "6.20.0", 76 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 77 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", 78 | "dev": true, 79 | "license": "MIT" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlitebruv", 3 | "description": "A Simple and Efficient Query Builder for D1/Turso and Bun's SQLite", 4 | "version": "1.1.15", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "files": [ 8 | "dist/index.d.ts", 9 | "dist/index.js" 10 | ], 11 | "scripts": { 12 | "compile": " rm -rf dist && tsc" 13 | }, 14 | "keywords": [ 15 | "SQL", 16 | "SQLite", 17 | "sqlitebruv", 18 | "D1", 19 | "bun", 20 | "query" 21 | ], 22 | "author": "", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/bun": "^1.1.14", 26 | "@types/node": "^22.10.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, unlinkSync } from "node:fs"; 2 | import { mkdir, writeFile } from "node:fs/promises"; 3 | import path, { join } from "node:path"; 4 | import { randomBytes } from "node:crypto"; 5 | 6 | // TYPES: 7 | 8 | interface BruvSchema { 9 | name: string; 10 | columns: { 11 | [x in keyof Omit]: SchemaColumnOptions; 12 | }; 13 | } 14 | 15 | interface SchemaColumnOptions { 16 | type: "INTEGER" | "REAL" | "TEXT" | "DATETIME"; 17 | required?: boolean; 18 | unique?: boolean; 19 | default?: () => string; 20 | target?: string; 21 | check?: string[]; 22 | } 23 | 24 | type Params = string | number | null | boolean; 25 | type rawSchema = { name: string; schema: { sql: string } }; 26 | interface Query { 27 | from: string; 28 | select?: string[]; 29 | where?: { 30 | condition: string; 31 | params: any[]; 32 | }[]; 33 | andWhere?: { 34 | condition: string; 35 | params: any[]; 36 | }[]; 37 | orWhere?: { 38 | condition: string; 39 | params: any[]; 40 | }[]; 41 | orderBy?: { 42 | column: string; 43 | direction: "ASC" | "DESC"; 44 | }; 45 | limit?: number; 46 | offset?: number; 47 | cacheAs?: string; 48 | invalidateCache?: string; 49 | action?: "get" | "getOne" | "insert" | "update" | "delete" | "count"; 50 | /** 51 | ### For insert and update only 52 | */ 53 | data?: any; 54 | } 55 | 56 | interface TursoConfig { 57 | url: string; 58 | authToken: string; 59 | } 60 | interface D1Config { 61 | accountId: string; 62 | databaseId: string; 63 | apiKey: string; 64 | } 65 | 66 | // SqliteBruv class 67 | 68 | export class SqliteBruv< 69 | T extends Record = Record 70 | > { 71 | static migrationFolder = "./Bruv-migrations"; 72 | /** 73 | * @internal 74 | */ 75 | db?: any; 76 | /** 77 | * @internal 78 | */ 79 | _localFile?: any; 80 | private _columns: string[] = ["*"]; 81 | private _conditions: string[] = []; 82 | private _tableName?: string = undefined; 83 | private _params: Params[] = []; 84 | private _limit?: number; 85 | private _offset?: number; 86 | private _orderBy?: { column: string; direction: "ASC" | "DESC" }; 87 | private _logging: boolean = false; 88 | private _hotCache: Record = {}; 89 | private _turso?: TursoConfig; 90 | private _D1?: TursoConfig; 91 | private _QueryMode?: boolean = false; 92 | private readonly MAX_PARAMS = 100; 93 | private readonly ALLOWED_OPERATORS = [ 94 | "=", 95 | ">", 96 | "<", 97 | ">=", 98 | "<=", 99 | "LIKE", 100 | "IN", 101 | "BETWEEN", 102 | "IS NULL", 103 | "IS NOT NULL", 104 | ]; 105 | private readonly DANGEROUS_PATTERNS = [ 106 | /;\s*$/, 107 | /UNION/i, 108 | /DROP/i, 109 | /DELETE/i, 110 | /UPDATE/i, 111 | /INSERT/i, 112 | /ALTER/i, 113 | /EXEC/i, 114 | ]; 115 | loading?: Promise; 116 | constructor({ 117 | logging, 118 | schema, 119 | D1Config, 120 | TursoConfig, 121 | localFile, 122 | QueryMode, 123 | createMigrations, 124 | }: { 125 | D1Config?: D1Config; 126 | TursoConfig?: TursoConfig; 127 | QueryMode?: boolean; 128 | localFile?: string; 129 | schema: Schema[]; 130 | logging?: boolean; 131 | createMigrations?: boolean; 132 | }) { 133 | //? warning 134 | if ( 135 | [D1Config, TursoConfig, localFile, QueryMode].filter((v) => v).length === 136 | 0 137 | ) { 138 | throw new Error( 139 | "\nPlease pass any of \n1. LocalFile or \n2. D1Config or \n3. TursoConfig\nin SqliteBruv constructor" 140 | ); 141 | } 142 | if ( 143 | [D1Config, TursoConfig, localFile, QueryMode].filter((v) => v).length > 1 144 | ) { 145 | throw new Error( 146 | "\nPlease only pass one of \n1. LocalFile or \n2. D1Config or \n3. TursoConfig\nin SqliteBruv constructor" 147 | ); 148 | } 149 | // ? setup each schema 150 | schema.forEach((s) => { 151 | s.db = this; 152 | }); 153 | this.loading = new Promise(async (r) => { 154 | const bun = avoidError(() => (Bun ? true : false)); 155 | let Database; 156 | if (bun) { 157 | Database = (await import("bun:sqlite")).Database; 158 | } else { 159 | Database = (await import("node:sqlite")).DatabaseSync; 160 | } 161 | // ?setup db 162 | if (localFile) { 163 | this._localFile = true; 164 | this.db = new Database(localFile, { 165 | create: true, 166 | strict: true, 167 | }); 168 | } 169 | //? D1 setup 170 | if (D1Config) { 171 | const { accountId, databaseId, apiKey } = D1Config; 172 | this._D1 = { 173 | url: `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`, 174 | authToken: apiKey, 175 | }; 176 | } 177 | //? Turso setup 178 | if (TursoConfig) { 179 | this._turso = TursoConfig; 180 | } 181 | // ? setup 182 | if (QueryMode === true) { 183 | this._QueryMode = true; 184 | } 185 | //? logger setup 186 | if (logging === true) { 187 | this._logging = true; 188 | } 189 | 190 | if (!schema?.length) { 191 | throw new Error("Not database schema passed!"); 192 | } else { 193 | // ? init each schema 194 | schema.forEach((s) => { 195 | s.db = this; 196 | }); 197 | } 198 | //? evaluate conditions for calculating migration 199 | const shouldMigrate = 200 | !process.argv 201 | .slice(1) 202 | .some((v) => v.includes("Bruv-migrations/migrate.ts")) && 203 | createMigrations !== false && 204 | QueryMode !== true; 205 | //? Auto create migration files if needed 206 | if (shouldMigrate) { 207 | const clonedSchema = schema.map((s) => s._clone()); 208 | const tempDbPath = path.join(import.meta.dirname, "./temp.sqlite"); 209 | const tempDb = new SqliteBruv({ 210 | schema: clonedSchema, 211 | localFile: tempDbPath, 212 | createMigrations: false, 213 | }); 214 | 215 | // ? init each schema 216 | clonedSchema 217 | .map((s) => s._clone()) 218 | .forEach((s) => { 219 | s.db = tempDb; 220 | if (!QueryMode) s._induce(); 221 | }); 222 | 223 | Promise.all([getSchema(this), getSchema(tempDb)]) 224 | .then(async ([currentSchema, targetSchema]) => { 225 | const migration = await generateMigration( 226 | currentSchema || [], 227 | targetSchema || [] 228 | ); 229 | await createMigrationFileIfNeeded(migration); 230 | }) 231 | .catch((e) => { 232 | console.log(e); 233 | }) 234 | .finally(() => { 235 | unlinkSync(tempDbPath); 236 | }); 237 | } 238 | this.loading = undefined; 239 | r(undefined); 240 | }); 241 | } 242 | from = Record>( 243 | tableName: string 244 | ) { 245 | this._tableName = tableName; 246 | return this as unknown as SqliteBruv; 247 | } 248 | // Read queries 249 | select(...columns: string[]) { 250 | this._columns = columns || ["*"]; 251 | return this; 252 | } 253 | private validateCondition(condition: string): boolean { 254 | // Check for dangerous patterns 255 | if (this.DANGEROUS_PATTERNS.some((pattern) => pattern.test(condition))) { 256 | throw new Error("Invalid condition pattern detected"); 257 | } 258 | 259 | // Validate operators 260 | const hasValidOperator = this.ALLOWED_OPERATORS.some((op) => 261 | condition.toUpperCase().includes(op) 262 | ); 263 | if (!hasValidOperator) { 264 | throw new Error("Invalid or missing operator in condition"); 265 | } 266 | 267 | return true; 268 | } 269 | 270 | private validateParams(params: Params[]): boolean { 271 | if (params.length > this.MAX_PARAMS) { 272 | throw new Error("Too many parameters"); 273 | } 274 | 275 | for (const param of params) { 276 | if ( 277 | param !== null && 278 | !["string", "number", "boolean"].includes(typeof param) 279 | ) { 280 | throw new Error("Invalid parameter type"); 281 | } 282 | 283 | if (typeof param === "string" && param.length > 1000) { 284 | throw new Error("Parameter string too long"); 285 | } 286 | } 287 | 288 | return true; 289 | } 290 | where(condition: string, ...params: Params[]) { 291 | // Validate inputs 292 | if (!condition || typeof condition !== "string") { 293 | throw new Error("Condition must be a non-empty string"); 294 | } 295 | 296 | this.validateCondition(condition); 297 | this.validateParams(params); 298 | 299 | // Use parameterized query 300 | this._conditions.push(`WHERE ${condition}`); 301 | this._params.push(...params); 302 | 303 | return this; 304 | } 305 | andWhere(condition: string, ...params: Params[]) { 306 | this.validateCondition(condition); 307 | this.validateParams(params); 308 | 309 | this._conditions.push(`AND ${condition}`); 310 | this._params.push(...params); 311 | return this; 312 | } 313 | orWhere(condition: string, ...params: Params[]) { 314 | this.validateCondition(condition); 315 | this.validateParams(params); 316 | 317 | this._conditions.push(`OR ${condition}`); 318 | this._params.push(...params); 319 | return this; 320 | } 321 | limit(count: number) { 322 | this._limit = count; 323 | return this; 324 | } 325 | offset(count: number) { 326 | this._offset = count || -1; 327 | return this; 328 | } 329 | orderBy(column: string, direction: "ASC" | "DESC") { 330 | this._orderBy = { column, direction }; 331 | return this; 332 | } 333 | invalidateCache(cacheName: string) { 334 | this._hotCache[cacheName] = undefined; 335 | return undefined; 336 | } 337 | get({ cacheAs }: { cacheAs?: string } = {}): Promise { 338 | if (cacheAs && this._hotCache[cacheAs]) return this._hotCache[cacheAs]; 339 | const { query, params } = this.build(); 340 | return this.run(query, params, { single: false }); 341 | } 342 | getOne({ cacheAs }: { cacheAs?: string } = {}): Promise { 343 | if (cacheAs && this._hotCache[cacheAs]) return this._hotCache[cacheAs]; 344 | const { query, params } = this.build(); 345 | return this.run(query, params, { single: true }); 346 | } 347 | insert(data: T): Promise { 348 | // @ts-ignore 349 | data.id = Id(); // sqlitebruv provide you with string id by default 350 | const attributes = Object.keys(data); 351 | const columns = attributes.join(", "); 352 | const placeholders = attributes.map(() => "?").join(", "); 353 | const query = `INSERT INTO ${this._tableName} (${columns}) VALUES (${placeholders})`; 354 | const params = Object.values(data) as Params[]; 355 | this.clear(); 356 | return this.run(query, params, { single: true }); 357 | } 358 | update(data: Partial): Promise { 359 | const columns = Object.keys(data) 360 | .map((column) => `${column} = ?`) 361 | .join(", "); 362 | const query = `UPDATE ${ 363 | this._tableName 364 | } SET ${columns} ${this._conditions.join(" AND ")}`; 365 | const params = [...(Object.values(data) as Params[]), ...this._params]; 366 | this.clear(); 367 | return this.run(query, params); 368 | } 369 | delete(): Promise { 370 | const query = `DELETE FROM ${this._tableName} ${this._conditions.join( 371 | " AND " 372 | )}`; 373 | const params = [...this._params]; 374 | this.clear(); 375 | return this.run(query, params); 376 | } 377 | count({ cacheAs }: { cacheAs?: string } = {}): Promise<{ 378 | [x: string]: any; 379 | count: number; 380 | }> { 381 | if (cacheAs && this._hotCache[cacheAs]) return this._hotCache[cacheAs]; 382 | const query = `SELECT COUNT(*) as count FROM ${ 383 | this._tableName 384 | } ${this._conditions.join(" AND ")}`; 385 | const params = [...this._params]; 386 | this.clear(); 387 | return this.run(query, params, { single: true }); 388 | } 389 | 390 | // Parser function 391 | async executeJsonQuery(query: Query): Promise { 392 | if (!query.from) { 393 | throw new Error("Table is required."); 394 | } 395 | let queryBuilder = this.from(query.from); 396 | if (!query.action) { 397 | if (query.invalidateCache) 398 | return queryBuilder.invalidateCache(query.invalidateCache); 399 | throw new Error("Action is required."); 400 | } 401 | if (query.select) queryBuilder = queryBuilder.select(...query.select); 402 | if (query.limit) queryBuilder = queryBuilder.limit(query.limit); 403 | if (query.offset) queryBuilder = queryBuilder.offset(query.offset); 404 | if (query.where) { 405 | for (const condition of query.where) { 406 | queryBuilder = queryBuilder.where( 407 | condition.condition, 408 | ...condition.params 409 | ); 410 | } 411 | } 412 | 413 | if (query.andWhere) { 414 | for (const condition of query.andWhere) { 415 | queryBuilder = queryBuilder.andWhere( 416 | condition.condition, 417 | ...condition.params 418 | ); 419 | } 420 | } 421 | 422 | if (query.orWhere) { 423 | for (const condition of query.orWhere) { 424 | queryBuilder = queryBuilder.orWhere( 425 | condition.condition, 426 | ...condition.params 427 | ); 428 | } 429 | } 430 | 431 | if (query.orderBy) { 432 | queryBuilder = queryBuilder.orderBy( 433 | query.orderBy.column, 434 | query.orderBy.direction 435 | ); 436 | } 437 | 438 | let result: any; 439 | 440 | try { 441 | switch (query.action) { 442 | case "get": 443 | result = await queryBuilder.get({ cacheAs: query.cacheAs }); 444 | break; 445 | case "count": 446 | result = await queryBuilder.count({ cacheAs: query.cacheAs }); 447 | break; 448 | case "getOne": 449 | result = await queryBuilder.getOne({ cacheAs: query.cacheAs }); 450 | break; 451 | case "insert": 452 | if (!query.data) { 453 | throw new Error("Data is required for insert action."); 454 | } 455 | result = await queryBuilder.insert(query.data); 456 | break; 457 | case "update": 458 | if (!query.data || !query.from || !query.where) { 459 | throw new Error( 460 | "Data, from, and where are required for update action." 461 | ); 462 | } 463 | result = await queryBuilder.update(query.data); 464 | break; 465 | case "delete": 466 | if (!query.from || !query.where) { 467 | throw new Error("From and where are required for delete action."); 468 | } 469 | result = await queryBuilder.delete(); 470 | break; 471 | default: 472 | throw new Error("Invalid action specified."); 473 | } 474 | } catch (error) { 475 | // Handle errors and return appropriate response 476 | console.error("Query execution failed:", error); 477 | } 478 | 479 | return result; 480 | } 481 | 482 | private build() { 483 | const query = [ 484 | `SELECT ${this._columns.join(", ")} FROM ${this._tableName}`, 485 | ...this._conditions, 486 | this._orderBy 487 | ? `ORDER BY ${this._orderBy.column} ${this._orderBy.direction}` 488 | : "", 489 | this._limit ? `LIMIT ${this._limit}` : "", 490 | this._offset ? `OFFSET ${this._offset}` : "", 491 | ] 492 | .filter(Boolean) 493 | .join(" "); 494 | const params = [...this._params]; 495 | this.clear(); 496 | return { query, params }; 497 | } 498 | clear() { 499 | if (!this._tableName || typeof this._tableName !== "string") { 500 | throw new Error("no table selected!"); 501 | } 502 | this._conditions = []; 503 | this._params = []; 504 | this._limit = undefined; 505 | this._offset = undefined; 506 | this._orderBy = undefined; 507 | this._tableName = undefined; 508 | } 509 | /** 510 | * @internal 511 | */ 512 | async run( 513 | query: string, 514 | params: (string | number | null | boolean)[], 515 | { single, cacheName }: { single?: boolean; cacheName?: string } = {} 516 | ) { 517 | if (this.loading) await this.loading; 518 | if (this._QueryMode) return { query, params } as any; 519 | if (this._logging) { 520 | console.log({ query, params }); 521 | } 522 | // turso 523 | if (this._turso) { 524 | let results = await this.executeTursoQuery(query, params); 525 | 526 | if (single) { 527 | results = results[0]; 528 | } 529 | if (cacheName) { 530 | return this.cacheResponse(results, cacheName); 531 | } 532 | return results; 533 | } 534 | // d1 535 | if (this._D1) { 536 | const res = await fetch(this._D1.url, { 537 | method: "POST", 538 | headers: { 539 | Authorization: `Bearer ${this._D1.authToken}`, 540 | "Content-Type": "application/json", 541 | }, 542 | body: JSON.stringify({ sql: query, params }), 543 | }); 544 | const data = await res.json(); 545 | let result; 546 | if (data.success && data.result[0].success) { 547 | if (single) { 548 | result = data.result[0].results[0]; 549 | } else { 550 | result = data.result[0].results; 551 | } 552 | if (cacheName) { 553 | return this.cacheResponse(result, cacheName); 554 | } 555 | return result; 556 | } 557 | throw new Error(JSON.stringify(data.errors)); 558 | } 559 | // local db 560 | if (single === true) { 561 | if (cacheName) { 562 | return this.cacheResponse( 563 | this.db.query(query).get(...params), 564 | cacheName 565 | ); 566 | } 567 | return this.db.query(query).get(...params); 568 | } 569 | if (single === false) { 570 | if (cacheName) { 571 | return this.cacheResponse( 572 | this.db.prepare(query).all(...params), 573 | cacheName 574 | ); 575 | } 576 | return this.db.prepare(query).all(...params); 577 | } 578 | return this.db.prepare(query).run(...params); 579 | } 580 | private async executeTursoQuery( 581 | query: string, 582 | params: any[] = [] 583 | ): Promise { 584 | if (!this._turso) { 585 | throw new Error("Turso configuration not found"); 586 | } 587 | 588 | const response = await fetch(this._turso.url, { 589 | method: "POST", 590 | headers: { 591 | Authorization: `Bearer ${this._turso.authToken}`, 592 | "Content-Type": "application/json", 593 | }, 594 | body: JSON.stringify({ 595 | statements: [ 596 | { 597 | q: query, 598 | params: params, 599 | }, 600 | ], 601 | }), 602 | }); 603 | 604 | if (!response.ok) { 605 | console.error(await response.text()); 606 | throw new Error(`Turso API error: ${response.statusText}`); 607 | } 608 | 609 | const results = (await response.json())[0]; 610 | const { columns, rows } = results?.results || {}; 611 | if (results.error) { 612 | throw new Error(`Turso API error: ${results.error}`); 613 | } 614 | 615 | // Map each row to an object 616 | const transformedRows = rows.map((row: any[]) => { 617 | const rowObject: any = {}; 618 | columns.forEach((column: string, index: number) => { 619 | rowObject[column] = row[index]; 620 | }); 621 | return rowObject; 622 | }); 623 | 624 | return transformedRows; 625 | } 626 | raw(raw: string, params: (string | number | boolean)[] = []) { 627 | return this.run(raw, params); 628 | } 629 | rawAll(raw: string) { 630 | return this.db.prepare(raw).all(); 631 | } 632 | async cacheResponse(response: any, cacheName?: string) { 633 | await response; 634 | this._hotCache[cacheName!] = response; 635 | return response; 636 | } 637 | } 638 | 639 | export class Schema = {}> { 640 | private string: string = ""; 641 | name: string; 642 | db?: SqliteBruv; 643 | columns: { [x in keyof Omit]: SchemaColumnOptions }; 644 | constructor(def: BruvSchema) { 645 | this.name = def.name; 646 | this.columns = def.columns; 647 | } 648 | get query() { 649 | if (this.db?.loading) { 650 | throw new Error("Database not loaded yet!!"); 651 | } 652 | return this.db!.from(this.name) as SqliteBruv; 653 | } 654 | queryRaw(raw: string) { 655 | return this.db?.from(this.name).raw(raw, [])!; 656 | } 657 | /** 658 | * @internal 659 | */ 660 | _induce() { 661 | const tables = Object.keys(this.columns); 662 | this.string = `CREATE TABLE IF NOT EXISTS ${ 663 | this.name 664 | } (\n id text PRIMARY KEY NOT NULL,\n ${tables 665 | .map( 666 | (col, i) => 667 | col + 668 | " " + 669 | this.columns[col].type + 670 | (this.columns[col].unique ? " UNIQUE" : "") + 671 | (this.columns[col].required ? " NOT NULL" : "") + 672 | (this.columns[col].target 673 | ? " REFERENCES " + this.columns[col].target + "(id)" 674 | : "") + 675 | (this.columns[col].check?.length 676 | ? " CHECK (" + 677 | col + 678 | " IN (" + 679 | this.columns[col].check.map((c) => "'" + c + "'").join(",") + 680 | ")) " 681 | : "") + 682 | (this.columns[col].default 683 | ? " DEFAULT " + this.columns[col].default() 684 | : "") + 685 | (i + 1 !== tables.length ? ",\n " : "\n") 686 | ) 687 | .join(" ")})`; 688 | try { 689 | this.db?.raw(this.string); 690 | } catch (error) { 691 | console.log({ err: String(error), schema: this.string }); 692 | } 693 | } 694 | /** 695 | * @internal 696 | */ 697 | _clone() { 698 | return new Schema({ columns: this.columns, name: this.name }); 699 | } 700 | async getSql() { 701 | await this.db?.loading; 702 | return this.string; 703 | } 704 | } 705 | 706 | async function getSchema(db: SqliteBruv<{}>): Promise { 707 | if (db.loading) await db.loading; 708 | try { 709 | let tables = {}, 710 | schema = []; 711 | if (!db._localFile) { 712 | tables = 713 | (await db.run( 714 | "SELECT name FROM sqlite_master WHERE type='table'", 715 | [] 716 | )) || {}; 717 | schema = await Promise.all( 718 | Object.values(tables).map(async (table: any) => ({ 719 | name: table.name, 720 | schema: await db.run( 721 | `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table.name}'`, 722 | [], 723 | { single: false } 724 | ), 725 | })) 726 | ); 727 | } else { 728 | tables = 729 | ( 730 | await db.db.prepare( 731 | "SELECT name FROM sqlite_master WHERE type='table'" 732 | ) 733 | ).all() || {}; 734 | schema = await Promise.all( 735 | Object.values(tables).map(async (table: any) => ({ 736 | name: table.name, 737 | schema: await db.db 738 | .prepare( 739 | `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table.name}'` 740 | ) 741 | .get(), 742 | })) 743 | ); 744 | } 745 | return schema; 746 | } catch (error) { 747 | console.error(error); 748 | //todo: Close the db connection 749 | } 750 | } 751 | 752 | async function generateMigration( 753 | currentSchema: rawSchema[], 754 | targetSchema: rawSchema[] 755 | ): Promise<{ up: string; down: string }> { 756 | if (!targetSchema?.length || targetSchema[0].name == null) 757 | return { up: "", down: "" }; 758 | 759 | const currentTables: Record = Object.fromEntries( 760 | currentSchema.map(({ name, schema }) => [ 761 | name, 762 | Array.isArray(schema) ? schema[0].sql : schema.sql, 763 | ]) 764 | ); 765 | 766 | const targetTables: Record = Object.fromEntries( 767 | targetSchema.map(({ name, schema }) => [ 768 | name, 769 | Array.isArray(schema) ? schema[0].sql : schema.sql, 770 | ]) 771 | ); 772 | 773 | let upStatements: string[] = ["-- Up migration"]; 774 | let downStatements: string[] = ["-- Down migration"]; 775 | 776 | // Helper function to parse column definitions 777 | function parseSchema( 778 | sql: string 779 | ): Record { 780 | const columnRegex = 781 | /(?\w+)\s+(?\w+)(?:\s+(?.*?))?(?:,|\))/gi; 782 | 783 | const columnSectionMatch = sql.match(/\(([\s\S]+)\)/); 784 | if (!columnSectionMatch) return {}; 785 | 786 | const columnSection = columnSectionMatch[1]; 787 | const matches = columnSection.matchAll(columnRegex); 788 | 789 | const columns: Record = {}; 790 | for (const match of matches) { 791 | const columnName = match.groups?.["column_name"] || ""; 792 | const dataType = match.groups?.["data_type"] || ""; 793 | const constraints = (match.groups?.["constraints"] || "").trim(); 794 | columns[columnName] = { type: dataType, constraints }; 795 | } 796 | return columns; 797 | } 798 | 799 | // Generate migration steps 800 | let shouldMigrate = false; 801 | 802 | for (const [tableName, currentSql] of Object.entries(currentTables)) { 803 | const targetSql = targetTables[tableName]; 804 | if (!targetSql) { 805 | // Table dropped 806 | shouldMigrate = true; 807 | upStatements.push(`DROP TABLE ${tableName};`); 808 | downStatements.push( 809 | "-- " + 810 | currentSql 811 | .replace(tableName, `${tableName}_old`) 812 | .replaceAll("\n", "\n--") + 813 | ";" 814 | ); 815 | continue; 816 | } 817 | 818 | const currentColumns = parseSchema(currentSql); 819 | const targetColumns = parseSchema(targetSql); 820 | 821 | if (JSON.stringify(currentColumns) !== JSON.stringify(targetColumns)) { 822 | // Recreate table to reflect column changes 823 | shouldMigrate = true; 824 | 825 | // 1. Create a new table with the target schema 826 | upStatements.push(targetSql.replace(tableName, `${tableName}_new`) + ";"); 827 | 828 | // 2. Copy data to the new table 829 | const commonColumns = Object.keys(currentColumns) 830 | .filter((col) => targetColumns[col]) 831 | .join(", "); 832 | upStatements.push( 833 | `INSERT INTO ${tableName}_new (${commonColumns}) SELECT ${commonColumns} FROM ${tableName};` 834 | ); 835 | 836 | // 3. Drop the old table 837 | upStatements.push(`DROP TABLE ${tableName};`); 838 | 839 | // 4. Rename the new table to the old table's name 840 | upStatements.push(`ALTER TABLE ${tableName}_new RENAME TO ${tableName};`); 841 | 842 | // Down migration (reverse steps) 843 | // 1. Recreate the old table with the original schema 844 | downStatements.push( 845 | "-- " + 846 | currentSql 847 | .replace(tableName, `${tableName}_old`) 848 | .replaceAll("\n", "\n--") + 849 | ";" 850 | ); 851 | 852 | // 2. Copy data back to the old table 853 | downStatements.push( 854 | `-- INSERT INTO ${tableName}_old (${commonColumns}) SELECT ${commonColumns} FROM ${tableName};` 855 | ); 856 | 857 | // 3. Drop the current table 858 | downStatements.push(`-- DROP TABLE ${tableName};`); 859 | 860 | // 4. Rename the old table back to the original name 861 | downStatements.push( 862 | `-- ALTER TABLE ${tableName}_old RENAME TO ${tableName};` 863 | ); 864 | } 865 | } 866 | 867 | // Handle new tables 868 | for (const [tableName, targetSql] of Object.entries(targetTables)) { 869 | if (!currentTables[tableName]) { 870 | shouldMigrate = true; 871 | upStatements.push(targetSql + ";"); 872 | downStatements.push(`-- DROP TABLE ${tableName};`); 873 | } 874 | } 875 | 876 | return shouldMigrate 877 | ? { up: upStatements.join("\n"), down: downStatements.join("\n") } 878 | : { up: "", down: "" }; 879 | } 880 | 881 | async function createMigrationFileIfNeeded( 882 | migration: { up: string; down: string } | null 883 | ) { 884 | if (!migration?.up || !migration?.down) return; 885 | const timestamp = new Date().toString().split(" ").slice(0, 5).join("_"); 886 | const filename = `${timestamp}.sql`; 887 | const filepath = join(SqliteBruv.migrationFolder, filename); 888 | const filepath2 = join(SqliteBruv.migrationFolder, "migrate.ts"); 889 | const fileContent = `${migration.up}\n\n${migration.down}`; 890 | try { 891 | await mkdir(SqliteBruv.migrationFolder, { recursive: true }).catch( 892 | (e) => {} 893 | ); 894 | if (isDuplicateMigration(fileContent)) return; 895 | await writeFile(filepath, fileContent, {}); 896 | await writeFile( 897 | filepath2, 898 | `// Don't rename this file or the directory 899 | // import your db class correctly below and run the file to apply. 900 | import { db } from "path/to/db-class-instance"; 901 | import { readFileSync } from "node:fs"; 902 | 903 | const filePath = "${filepath}"; 904 | const migrationQuery = readFileSync(filePath, "utf8"); 905 | const info = await db.raw(migrationQuery); 906 | console.log(info); 907 | // bun Bruv-migrations/migrate.ts 908 | ` 909 | ); 910 | console.log(`Created migration file: ${filename}`); 911 | } catch (error) { 912 | console.error("Error during file system operations: ", error); 913 | } 914 | } 915 | 916 | function isDuplicateMigration(newContent: string) { 917 | const migrationFiles = readdirSync(SqliteBruv.migrationFolder); 918 | for (const file of migrationFiles) { 919 | const filePath = join(SqliteBruv.migrationFolder, file); 920 | const existingContent = readFileSync(filePath, "utf8"); 921 | if (existingContent.trim() === newContent.trim()) { 922 | return true; 923 | } 924 | } 925 | return false; 926 | } 927 | 928 | const PROCESS_UNIQUE = randomBytes(5); 929 | const buffer = Buffer.alloc(12); 930 | const Id = (): string => { 931 | let index = ~~(Math.random() * 0xffffff); 932 | const time = ~~(Date.now() / 1000); 933 | const inc = (index = (index + 1) % 0xffffff); 934 | // 4-byte timestamp 935 | buffer[3] = time & 0xff; 936 | buffer[2] = (time >> 8) & 0xff; 937 | buffer[1] = (time >> 16) & 0xff; 938 | buffer[0] = (time >> 24) & 0xff; 939 | // 5-byte process unique 940 | buffer[4] = PROCESS_UNIQUE[0]; 941 | buffer[5] = PROCESS_UNIQUE[1]; 942 | buffer[6] = PROCESS_UNIQUE[2]; 943 | buffer[7] = PROCESS_UNIQUE[3]; 944 | buffer[8] = PROCESS_UNIQUE[4]; 945 | // 3-byte counter 946 | buffer[11] = inc & 0xff; 947 | buffer[10] = (inc >> 8) & 0xff; 948 | buffer[9] = (inc >> 16) & 0xff; 949 | return buffer.toString("hex"); 950 | }; 951 | 952 | const avoidError = (cb: { (): any; (): any; (): void }) => { 953 | try { 954 | cb(); 955 | return true; 956 | } catch (error) { 957 | return false; 958 | } 959 | }; 960 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { db, user, works } from "./db.ts"; 2 | 3 | // await db.raw(await user.getSql()); 4 | 5 | // await db.raw(works.toString()); 6 | const time = Date.now(); 7 | await db.executeJsonQuery({ 8 | action: "insert", 9 | data: { 10 | name: "John Doe", 11 | username: "JohnDoe@" + time, 12 | age: 10, 13 | }, 14 | from: "users", 15 | }); 16 | 17 | const a = await user.query.where("username = ? ", "JohnDoe@" + time).count(); 18 | const c = await user.query.count(); 19 | console.log({ a, c }); 20 | const result = await db.executeJsonQuery({ 21 | action: "getOne", 22 | where: [{ condition: "username =? ", params: ["JohnDoe@" + time] }], 23 | from: "users", 24 | }); 25 | 26 | // console.log({ result, a }); 27 | await db.executeJsonQuery({ 28 | action: "insert", 29 | where: [{ condition: "username = ? ", params: ["JohnDoe"] }], 30 | data: { 31 | name: "John Doe's work", 32 | user: result.id, 33 | }, 34 | from: "works", 35 | }); 36 | const b = await db.executeJsonQuery({ 37 | action: "get", 38 | where: [{ condition: "name = ? ", params: ["John Doe's work"] }], 39 | from: "works", 40 | }); 41 | 42 | // console.log({ b, a }); 43 | 44 | const gh = await user.query.select("*").getOne(); 45 | // console.log(gh); 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // enable latest features 4 | "lib": ["es2022", "esnext", "dom", "dom.iterable"], 5 | "target": "ESNext", 6 | "module": "NodeNext", 7 | "moduleDetection": "force", 8 | "allowJs": true, 9 | "removeComments": true, 10 | // Bundler mode 11 | "declaration": true, 12 | // Best practices 13 | "strict": true, 14 | // Some stricter flags 15 | "useUnknownInCatchVariables": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "stripInternal": true, 18 | "outDir": "./dist", 19 | "skipDefaultLibCheck": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": ["src"] 23 | } 24 | --------------------------------------------------------------------------------