├── .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 | [](https://www.npmjs.com/package/sqlitebruv)
6 | [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/sqlitebruv) [](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 |
--------------------------------------------------------------------------------