First Commit

This commit is contained in:
Benjamin Toby 2026-03-08 06:23:30 +01:00
commit df53cdb4e5
101 changed files with 9048 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# dependencies (bun install)
node_modules
# output
out
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
/test
.vscode

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
//git.tben.me/api/packages/moduletrace/npm/:_authToken=${GITBEN_NPM_TOKEN}

106
CLAUDE.md Normal file
View File

@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

789
README.md Normal file
View File

@ -0,0 +1,789 @@
# Bun SQLite
@moduletrace/bun-sqlite
A schema-driven SQLite manager for [Bun](https://bun.sh), featuring automatic schema synchronization, type-safe CRUD operations, vector embedding support (via `sqlite-vec`), and TypeScript type definition generation.
---
## Table of Contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Schema Definition](#schema-definition)
- [CLI Commands](#cli-commands)
- [`schema`](#schema--sync-database-to-schema)
- [`typedef`](#typedef--generate-typescript-types-only)
- [`backup`](#backup--back-up-the-database)
- [`restore`](#restore--restore-the-database-from-a-backup)
- [CRUD API](#crud-api)
- [Select](#select)
- [Insert](#insert)
- [Update](#update)
- [Delete](#delete)
- [Raw SQL](#raw-sql)
- [Query API Reference](#query-api-reference)
- [Vector Table Support](#vector-table-support)
- [TypeScript Type Generation](#typescript-type-generation)
- [Default Fields](#default-fields)
- [Project Structure](#project-structure)
---
## Features
- **Schema-first design** — define your database in TypeScript; the library syncs your SQLite file to match
- **Automatic migrations** — adds new columns, recreates tables for complex changes, drops removed tables
- **Type-safe CRUD** — fully generic `select`, `insert`, `update`, `delete` functions with TypeScript generics
- **Rich query DSL** — filtering, ordering, pagination, joins, grouping, full-text search, sub-query counts
- **Vector table support** — create and manage `sqlite-vec` virtual tables for AI/ML embeddings
- **TypeScript codegen** — generate `.ts` type definitions from your schema automatically
- **Zero-config defaults**`id`, `created_at`, and `updated_at` fields are added to every table automatically
---
## Prerequisites
`@moduletrace/bun-sqlite` is published to a private Gitea npm registry. You must configure your package manager to resolve the `@moduletrace` scope from that registry before installing.
Add the following to your project's `.npmrc` file (create it at the root of your project if it doesn't exist):
```ini
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
```
This works for both `bun` and `npm`.
---
## Installation
```bash
bun add @moduletrace/bun-sqlite
```
---
## Quick Start
### 1. Create the config file
Create `bun-sqlite.config.ts` at your project root:
```ts
import type { BunSQLiteConfig } from "@moduletrace/bun-sqlite";
const config: BunSQLiteConfig = {
db_name: "my-app.db",
db_schema_file_name: "schema.ts",
db_dir: "./db", // optional: where to store the db file
db_backup_dir: ".backups", // optional: name of backups directory. Relative to the db dir.
typedef_file_path: "./db/types/db.ts", // optional: where to write generated types
};
export default config;
```
### 2. Define your schema
Create `./db/schema.ts` (matching `db_schema_file_name` above):
```ts
import type { BUN_SQLITE_DatabaseSchemaType } from "@moduletrace/bun-sqlite";
const schema: BUN_SQLITE_DatabaseSchemaType = {
dbName: "my-app",
tables: [
{
tableName: "users",
fields: [
{ fieldName: "first_name", dataType: "TEXT" },
{ fieldName: "last_name", dataType: "TEXT" },
{ fieldName: "email", dataType: "TEXT", unique: true },
],
},
],
};
export default schema;
```
### 3. Sync the schema to SQLite
```bash
bunx bun-sqlite schema
```
This creates the SQLite database file and creates/updates all tables to match your schema.
### 4. Use the CRUD API
```ts
import BunSQLite from "@moduletrace/bun-sqlite";
// Insert
await BunSQLite.insert({
table: "users",
data: [{ first_name: "Alice", email: "alice@example.com" }],
});
// Select
const result = await BunSQLite.select({ table: "users" });
console.log(result.payload); // Alice's row
// Update
await BunSQLite.update({
table: "users",
targetId: 1,
data: { first_name: "Alicia" },
});
// Delete
await BunSQLite.delete({ table: "users", targetId: 1 });
```
---
## Configuration
The config file must be named `bun-sqlite.config.ts` and placed at the root of your project.
| Field | Type | Required | Description |
| --------------------- | -------- | -------- | ------------------------------------------------------------------------------------------ |
| `db_name` | `string` | Yes | Filename for the SQLite database (e.g. `"app.db"`) |
| `db_schema_file_name` | `string` | Yes | Filename of the schema file relative to `db_dir` (or root if `db_dir` is not set) |
| `db_backup_dir` | `string` | No | Directory for database backups, relative to `db_dir` |
| `db_dir` | `string` | No | Root directory for the database file and schema. Defaults to project root |
| `typedef_file_path` | `string` | No | Output path for generated TypeScript types, relative to project root |
| `max_backups` | `number` | No | Maximum number of backup files to keep. Oldest are deleted automatically. Defaults to `10` |
---
## Schema Definition
### Database Schema
```ts
interface BUN_SQLITE_DatabaseSchemaType {
dbName?: string;
tables: BUN_SQLITE_TableSchemaType[];
}
```
### Table Schema
```ts
interface BUN_SQLITE_TableSchemaType {
tableName: string;
tableDescription?: string;
fields: BUN_SQLITE_FieldSchemaType[];
indexes?: BUN_SQLITE_IndexSchemaType[];
uniqueConstraints?: BUN_SQLITE_UniqueConstraintSchemaType[];
parentTableName?: string; // inherit fields from another table in the schema
tableNameOld?: string; // rename: set this to the old name to trigger ALTER TABLE RENAME
isVector?: boolean; // mark this as a sqlite-vec virtual table
vectorType?: string; // virtual table type, defaults to "vec0"
}
```
### Field Schema
```ts
type BUN_SQLITE_FieldSchemaType = {
fieldName?: string;
dataType: "TEXT" | "INTEGER";
primaryKey?: boolean;
autoIncrement?: boolean;
notNullValue?: boolean;
unique?: boolean;
defaultValue?: string | number;
defaultValueLiteral?: string; // raw SQL literal, e.g. "CURRENT_TIMESTAMP"
foreignKey?: BUN_SQLITE_ForeignKeyType;
isVector?: boolean; // vector embedding column
vectorSize?: number; // embedding dimensions (default: 1536)
sideCar?: boolean; // sqlite-vec "+" prefix for side-car columns
updatedField?: boolean; // flag that this field definition has changed
};
```
### Foreign Key
```ts
interface BUN_SQLITE_ForeignKeyType {
destinationTableName: string;
destinationTableColumnName: string;
cascadeDelete?: boolean;
cascadeUpdate?: boolean;
}
```
### Index
```ts
interface BUN_SQLITE_IndexSchemaType {
indexName?: string;
indexType?: "regular" | "full_text" | "vector";
indexTableFields?: { value: string; dataType: string }[];
}
```
### Unique Constraint
```ts
interface BUN_SQLITE_UniqueConstraintSchemaType {
constraintName?: string;
constraintTableFields?: { value: string }[];
}
```
---
## CLI Commands
The package provides a `bun-sqlite` CLI binary.
### `schema` — Sync database to schema
```bash
bunx bun-sqlite schema [options]
```
| Option | Description |
| ----------------- | ---------------------------------------------------------- |
| `-v`, `--vector` | Drop and recreate all vector (`sqlite-vec`) virtual tables |
| `-t`, `--typedef` | Also generate TypeScript type definitions after syncing |
**Examples:**
```bash
# Sync schema only
bunx bun-sqlite schema
# Sync schema and regenerate types
bunx bun-sqlite schema --typedef
# Sync schema, recreate vector tables, and regenerate types
bunx bun-sqlite schema --vector --typedef
```
### `typedef` — Generate TypeScript types only
```bash
bunx bun-sqlite typedef
```
Reads the schema and writes TypeScript type definitions to the path configured in `typedef_file_path`.
---
### `backup` — Back up the database
```bash
bunx bun-sqlite backup
```
Copies the current database file into `db_backup_dir` with a timestamped filename. After copying, the oldest backups are automatically pruned so the number of stored backups never exceeds `max_backups` (default: 10).
**Example:**
```bash
bunx bun-sqlite backup
# Backing up database ...
# DB Backup Success!
```
---
### `restore` — Restore the database from a backup
```bash
bunx bun-sqlite restore
```
Presents an interactive list of available backups sorted by date (newest first). Select a backup to overwrite the current database file with it.
**Example:**
```bash
bunx bun-sqlite restore
# Restoring up database ...
# ? Select a backup: (Use arrow keys)
# Backup #1: Mon Mar 02 2026 14:30:00
# Backup #2: Sun Mar 01 2026 09:15:42
# DB Restore Success!
```
> If no backups exist, the command exits with an error and a reminder to run `backup` first.
---
## CRUD API
Import the default export:
```ts
import BunSQLite from "@moduletrace/bun-sqlite";
```
All methods return an `APIResponseObject<T>`:
```ts
{
success: boolean;
payload?: T[]; // array of rows (select)
singleRes?: T; // first row (select)
count?: number; // total count (when count: true)
postInsertReturn?: {
affectedRows?: number;
insertId?: number;
};
error?: string;
msg?: string;
debug?: any;
}
```
---
### Select
```ts
BunSQLite.select<T>({ table, query?, count?, targetId? })
```
| Parameter | Type | Description |
| ---------- | --------------------- | ------------------------------------------------------------ |
| `table` | `string` | Table name |
| `query` | `ServerQueryParam<T>` | Query/filter options (see [Query API](#query-api-reference)) |
| `count` | `boolean` | Return row count instead of rows |
| `targetId` | `string \| number` | Shorthand to filter by `id` |
**Examples:**
```ts
// Get all users
const res = await BunSQLite.select({ table: "users" });
// Get by ID
const res = await BunSQLite.select({ table: "users", targetId: 42 });
// Filter with LIKE
const res = await BunSQLite.select<UserType>({
table: "users",
query: {
query: {
first_name: { value: "Ali", equality: "LIKE" },
},
},
});
// Count rows
const res = await BunSQLite.select({ table: "users", count: true });
console.log(res.count);
// Pagination
const res = await BunSQLite.select({
table: "users",
query: { limit: 10, page: 2 },
});
```
---
### Insert
```ts
BunSQLite.insert<T>({ table, data });
```
| Parameter | Type | Description |
| --------- | -------- | ------------------------------ |
| `table` | `string` | Table name |
| `data` | `T[]` | Array of row objects to insert |
`created_at` and `updated_at` are set automatically to `Date.now()`.
**Example:**
```ts
const res = await BunSQLite.insert({
table: "users",
data: [
{ first_name: "Alice", last_name: "Smith", email: "alice@example.com" },
{ first_name: "Bob", last_name: "Jones", email: "bob@example.com" },
],
});
console.log(res.postInsertReturn?.insertId); // last inserted row ID
```
---
### Update
```ts
BunSQLite.update<T>({ table, data, query?, targetId? })
```
| Parameter | Type | Description |
| ---------- | --------------------- | --------------------------- |
| `table` | `string` | Table name |
| `data` | `Partial<T>` | Fields to update |
| `query` | `ServerQueryParam<T>` | WHERE clause conditions |
| `targetId` | `string \| number` | Shorthand to filter by `id` |
A WHERE clause is required. If no condition matches, `success` is `false`.
`updated_at` is set automatically to `Date.now()`.
**Examples:**
```ts
// Update by ID
await BunSQLite.update({
table: "users",
targetId: 1,
data: { first_name: "Alicia" },
});
// Update with custom query
await BunSQLite.update({
table: "users",
data: { last_name: "Doe" },
query: {
query: {
email: { value: "alice@example.com", equality: "EQUAL" },
},
},
});
```
---
### Delete
```ts
BunSQLite.delete<T>({ table, query?, targetId? })
```
| Parameter | Type | Description |
| ---------- | --------------------- | --------------------------- |
| `table` | `string` | Table name |
| `query` | `ServerQueryParam<T>` | WHERE clause conditions |
| `targetId` | `string \| number` | Shorthand to filter by `id` |
A WHERE clause is required. If no condition is provided, `success` is `false`.
**Examples:**
```ts
// Delete by ID
await BunSQLite.delete({ table: "users", targetId: 1 });
// Delete with condition
await BunSQLite.delete({
table: "users",
query: {
query: {
first_name: { value: "Ben", equality: "LIKE" },
},
},
});
```
---
### Raw SQL
```ts
BunSQLite.sql<T>({ sql, values? })
```
| Parameter | Type | Description |
| --------- | ---------------------- | -------------------- |
| `sql` | `string` | Raw SQL statement |
| `values` | `(string \| number)[]` | Parameterized values |
SELECT statements return rows; all other statements return `postInsertReturn`.
**Examples:**
```ts
// SELECT
const res = await BunSQLite.sql<UserType>({ sql: "SELECT * FROM users" });
console.log(res.payload);
// INSERT with params
await BunSQLite.sql({
sql: "INSERT INTO users (first_name, email) VALUES (?, ?)",
values: ["Charlie", "charlie@example.com"],
});
```
---
## Query API Reference
The `query` parameter on `select`, `update`, and `delete` accepts a `ServerQueryParam<T>` object:
```ts
type ServerQueryParam<T> = {
query?: { [key in keyof T]: ServerQueryObject };
selectFields?: (keyof T | { fieldName: keyof T; alias?: string })[];
omitFields?: (keyof T)[];
limit?: number;
page?: number;
offset?: number;
order?:
| { field: keyof T; strategy: "ASC" | "DESC" }
| { field: keyof T; strategy: "ASC" | "DESC" }[];
searchOperator?: "AND" | "OR"; // how multiple query fields are joined (default: AND)
join?: ServerQueryParamsJoin[];
group?:
| keyof T
| { field: keyof T; table?: string }
| (keyof T | { field: keyof T; table?: string })[];
countSubQueries?: ServerQueryParamsCount[];
fullTextSearch?: {
fields: (keyof T)[];
searchTerm: string;
scoreAlias: string;
};
};
```
### Equality Operators
Set `equality` on any query field to control the comparison:
| Equality | SQL Equivalent |
| ----------------------- | ------------------------------------------------------ |
| `EQUAL` (default) | `=` |
| `NOT EQUAL` | `!=` |
| `LIKE` | `LIKE '%value%'` |
| `LIKE_RAW` | `LIKE 'value'` (no auto-wrapping) |
| `LIKE_LOWER` | `LOWER(field) LIKE '%value%'` |
| `NOT LIKE` | `NOT LIKE '%value%'` |
| `GREATER THAN` | `>` |
| `GREATER THAN OR EQUAL` | `>=` |
| `LESS THAN` | `<` |
| `LESS THAN OR EQUAL` | `<=` |
| `IN` | `IN (val1, val2, ...)` — pass array as value |
| `NOT IN` | `NOT IN (...)` |
| `BETWEEN` | `BETWEEN val1 AND val2` — pass `[val1, val2]` as value |
| `IS NULL` | `IS NULL` |
| `IS NOT NULL` | `IS NOT NULL` |
| `MATCH` | sqlite-vec vector nearest-neighbor search |
**Example:**
```ts
// Find users with email NOT NULL, ordered by created_at DESC, limit 20
const res = await BunSQLite.select<UserType>({
table: "users",
query: {
query: {
email: { equality: "IS NOT NULL" },
},
order: { field: "created_at", strategy: "DESC" },
limit: 20,
},
});
```
### JOIN
```ts
const res = await BunSQLite.select({
table: "posts",
query: {
join: [
{
joinType: "LEFT JOIN",
tableName: "users",
match: { source: "user_id", target: "id" },
selectFields: ["first_name", "email"],
},
],
},
});
```
---
## Vector Table Support
`@moduletrace/bun-sqlite` integrates with [`sqlite-vec`](https://github.com/asg017/sqlite-vec) for storing and querying vector embeddings.
### Define a vector table in the schema
```ts
{
tableName: "documents",
isVector: true,
vectorType: "vec0", // defaults to "vec0"
fields: [
{
fieldName: "embedding",
dataType: "TEXT",
isVector: true,
vectorSize: 1536, // embedding dimensions
},
{
fieldName: "title",
dataType: "TEXT",
sideCar: true, // stored as a side-car column (+title) for efficiency
},
{
fieldName: "body",
dataType: "TEXT",
sideCar: true,
},
],
}
```
> **Side-car columns** (`sideCar: true`) use sqlite-vec's `+column` syntax. They are stored separately from the vector index, keeping the index lean and fast while still being queryable alongside vector results.
### Sync vector tables
```bash
# Initial sync
bunx bun-sqlite schema
# Recreate vector tables (e.g. after changing vectorSize)
bunx bun-sqlite schema --vector
```
### Query vectors
```ts
const res = await BunSQLite.select({
table: "documents",
query: {
query: {
embedding: {
equality: "MATCH",
value: "<serialized-vector>",
vector: true,
vectorFunction: "vec_f32",
},
},
limit: 5,
},
});
```
---
## TypeScript Type Generation
Run the `typedef` command (or pass `--typedef` to `schema`) to generate a `.ts` file containing:
- A `const` array of all table names (`BunSQLiteTables`)
- A `type` for each table (named `BUN_SQLITE_<DB_NAME>_<TABLE_NAME>`)
- A union type `BUN_SQLITE_<DB_NAME>_ALL_TYPEDEFS`
**Example output** (`db/types/db.ts`):
```ts
export const BunSQLiteTables = ["users"] as const;
export type BUN_SQLITE_MY_APP_USERS = {
/** The unique identifier of the record. */
id?: number;
/** The time when the record was created. (Unix Timestamp) */
created_at?: number;
/** The time when the record was updated. (Unix Timestamp) */
updated_at?: number;
first_name?: string;
last_name?: string;
email?: string;
};
export type BUN_SQLITE_MY_APP_ALL_TYPEDEFS = BUN_SQLITE_MY_APP_USERS;
```
Use the generated types with the CRUD API for full type safety:
```ts
import BunSQLite from "@moduletrace/bun-sqlite";
import { BUN_SQLITE_MY_APP_USERS, BunSQLiteTables } from "./db/types/db";
const res = await BunSQLite.select<BUN_SQLITE_MY_APP_USERS>({
table: "users" as (typeof BunSQLiteTables)[number],
});
```
---
## Default Fields
Every table automatically receives the following fields — you do not need to declare them in your schema:
| Field | Type | Description |
| ------------ | -------------------------------------------- | -------------------------------------- |
| `id` | `INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL` | Unique row identifier |
| `created_at` | `INTEGER` | Unix timestamp set on insert |
| `updated_at` | `INTEGER` | Unix timestamp updated on every update |
---
## Project Structure
```
bun-sqlite/
├── src/
│ ├── index.ts # Main export (BunSQLite object)
│ ├── commands/
│ │ ├── index.ts # CLI entry point
│ │ ├── schema.ts # `schema` command
│ │ ├── typedef.ts # `typedef` command
│ │ ├── backup.ts # `backup` command
│ │ └── restore.ts # `restore` command
│ ├── functions/
│ │ └── init.ts # Config + schema loader
│ ├── lib/sqlite/
│ │ ├── index.ts # Database client (bun:sqlite + sqlite-vec)
│ │ ├── db-schema-manager.ts # Schema synchronization engine
│ │ ├── db-select.ts # Select implementation
│ │ ├── db-insert.ts # Insert implementation
│ │ ├── db-update.ts # Update implementation
│ │ ├── db-delete.ts # Delete implementation
│ │ ├── db-sql.ts # Raw SQL implementation
│ │ ├── db-generate-type-defs.ts # Type def generator
│ │ └── schema-to-typedef.ts # Schema-to-TypeScript converter
│ ├── types/
│ │ └── index.ts # All TypeScript types and interfaces
│ └── utils/
│ ├── sql-generator.ts # SELECT query builder
│ ├── sql-insert-generator.ts # INSERT query builder
│ ├── sql-gen-operator-gen.ts # Equality operator mapper
│ ├── sql-equality-parser.ts # Equality string parser
│ ├── append-default-fields-to-db-schema.ts
│ ├── grab-db-dir.ts # Resolve db/backup directory paths
│ ├── grab-db-backup-file-name.ts # Generate timestamped backup filename
│ ├── grab-sorted-backups.ts # List backups sorted newest-first
│ ├── grab-backup-data.ts # Parse metadata from a backup filename
│ └── trim-backups.ts # Prune oldest backups over max_backups
└── test/
└── test-01/ # Example project using the library
├── bun-sqlite.config.ts
├── db/
│ ├── bun-sqlite-schema.ts
│ └── types/bun-sqlite.ts # Generated types
└── src/
├── insert.ts
├── select.ts
├── delete.ts
└── sql.ts
```
---
## License
MIT

2
dist/commands/backup.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

22
dist/commands/backup.js vendored Normal file
View File

@ -0,0 +1,22 @@
import { Command } from "commander";
import init from "../functions/init";
import path from "path";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk";
import trimBackups from "../utils/trim-backups";
export default function () {
return new Command("backup")
.description("Backup Database")
.action(async (opts) => {
console.log(`Backing up database ...`);
const { config } = await init();
const { backup_dir, db_file_path } = grabDBDir({ config });
const new_db_file_name = grabDBBackupFileName({ config });
fs.cpSync(db_file_path, path.join(backup_dir, new_db_file_name));
trimBackups({ config });
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
process.exit();
});
}

6
dist/commands/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bun
/**
* # Declare Global Variables
*/
declare global { }
export {};

31
dist/commands/index.js vendored Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env bun
import { program } from "commander";
import schema from "./schema";
import typedef from "./typedef";
import backup from "./backup";
import restore from "./restore";
/**
* # Describe Program
*/
program
.name(`bun-sqlite`)
.description(`SQLite manager for Bun`)
.version(`1.0.0`);
/**
* # Declare Commands
*/
program.addCommand(schema());
program.addCommand(typedef());
program.addCommand(backup());
program.addCommand(restore());
/**
* # Handle Unavailable Commands
*/
program.on("command:*", () => {
console.error("Invalid command: %s\nSee --help for a list of available commands.", program.args.join(" "));
process.exit(1);
});
/**
* # Parse Arguments
*/
program.parse(Bun.argv);

2
dist/commands/restore.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

44
dist/commands/restore.js vendored Normal file
View File

@ -0,0 +1,44 @@
import { Command } from "commander";
import init from "../functions/init";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import chalk from "chalk";
import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data";
import path from "path";
export default function () {
return new Command("restore")
.description("Restore Database")
.action(async (opts) => {
console.log(`Restoring up database ...`);
const { config } = await init();
const { backup_dir, db_file_path } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
if (!backups?.[0]) {
console.error(`No Backups to restore. Use the \`backup\` command to create a backup`);
process.exit(1);
}
try {
const selected_backup = await select({
message: "Select a backup:",
choices: backups.map((b, i) => {
const { backup_date } = grabBackupData({
backup_name: b,
});
return {
name: `Backup #${i + 1}: ${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`,
value: b,
};
}),
});
fs.cpSync(path.join(backup_dir, selected_backup), db_file_path);
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}`);
process.exit();
}
catch (error) {
console.error(`Backup Restore ERROR => ${error.message}`);
process.exit();
}
});
}

2
dist/commands/schema.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

38
dist/commands/schema.js vendored Normal file
View File

@ -0,0 +1,38 @@
import { Command } from "commander";
import { SQLiteSchemaManager } from "../lib/sqlite/db-schema-manager";
import init from "../functions/init";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import _ from "lodash";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
export default function () {
return new Command("schema")
.description("Build DB From Schema")
.option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables")
.option("-t, --typedef", "Generate typescript type definitions")
.action(async (opts) => {
console.log(`Starting process ...`);
const { config, dbSchema } = await init();
const { ROOT_DIR } = grabDirNames();
const isVector = Boolean(opts.vector || opts.v);
const isTypeDef = Boolean(opts.typedef || opts.t);
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
const manager = new SQLiteSchemaManager({
schema: finaldbSchema,
recreate_vector_table: isVector,
});
await manager.syncSchema();
manager.close();
if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
});
}
console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`);
process.exit();
});
}

2
dist/commands/typedef.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

30
dist/commands/typedef.js vendored Normal file
View File

@ -0,0 +1,30 @@
import { Command } from "commander";
import init from "../functions/init";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
export default function () {
return new Command("typedef")
.description("Build DB From Schema")
.action(async (opts) => {
console.log(`Creating Type Definition From DB Schema ...`);
const { config, dbSchema } = await init();
const { ROOT_DIR } = grabDirNames();
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
});
}
else {
console.error(``);
process.exit(1);
}
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
process.exit();
});
}

5
dist/data/app-data.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export declare const AppData: {
readonly ConfigFileName: "bun-sqlite.config.ts";
readonly MaxBackups: 10;
readonly DefaultBackupDirName: ".backups";
};

5
dist/data/app-data.js vendored Normal file
View File

@ -0,0 +1,5 @@
export const AppData = {
ConfigFileName: "bun-sqlite.config.ts",
MaxBackups: 10,
DefaultBackupDirName: ".backups",
};

3
dist/data/grab-dir-names.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export default function grabDirNames(): {
ROOT_DIR: string;
};

7
dist/data/grab-dir-names.js vendored Normal file
View File

@ -0,0 +1,7 @@
import path from "path";
export default function grabDirNames() {
const ROOT_DIR = process.cwd();
return {
ROOT_DIR,
};
}

2
dist/functions/init.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { BunSQLiteConfigReturn } from "../types";
export default function init(): Promise<BunSQLiteConfigReturn>;

46
dist/functions/init.js vendored Normal file
View File

@ -0,0 +1,46 @@
import path from "path";
import fs from "fs";
import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names";
export default async function init() {
try {
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
if (!fs.existsSync(ConfigFilePath)) {
console.log("ConfigFilePath", ConfigFilePath);
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
process.exit(1);
}
const ConfigImport = await import(ConfigFilePath);
const Config = ConfigImport["default"];
if (!Config.db_name) {
console.error(`\`db_name\` is required in your config`);
process.exit(1);
}
if (!Config.db_schema_file_name) {
console.error(`\`db_schema_file_name\` is required in your config`);
process.exit(1);
}
let db_dir = ROOT_DIR;
if (Config.db_dir) {
db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (!fs.existsSync(Config.db_dir)) {
fs.mkdirSync(Config.db_dir, { recursive: true });
}
}
const DBSchemaFilePath = path.join(db_dir, Config.db_schema_file_name);
const DbSchemaImport = await import(DBSchemaFilePath);
const DbSchema = DbSchemaImport["default"];
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
const BackupDir = path.resolve(db_dir, backup_dir);
if (!fs.existsSync(BackupDir)) {
fs.mkdirSync(BackupDir, { recursive: true });
}
return { config: Config, dbSchema: DbSchema };
}
catch (error) {
console.error(`Initialization ERROR => ` + error.message);
process.exit(1);
}
}

13
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import DbDelete from "./lib/sqlite/db-delete";
import DbInsert from "./lib/sqlite/db-insert";
import DbSelect from "./lib/sqlite/db-select";
import DbSQL from "./lib/sqlite/db-sql";
import DbUpdate from "./lib/sqlite/db-update";
declare const BunSQLite: {
readonly select: typeof DbSelect;
readonly insert: typeof DbInsert;
readonly update: typeof DbUpdate;
readonly delete: typeof DbDelete;
readonly sql: typeof DbSQL;
};
export default BunSQLite;

13
dist/index.js vendored Normal file
View File

@ -0,0 +1,13 @@
import DbDelete from "./lib/sqlite/db-delete";
import DbInsert from "./lib/sqlite/db-insert";
import DbSelect from "./lib/sqlite/db-select";
import DbSQL from "./lib/sqlite/db-sql";
import DbUpdate from "./lib/sqlite/db-update";
const BunSQLite = {
select: DbSelect,
insert: DbInsert,
update: DbUpdate,
delete: DbDelete,
sql: DbSQL,
};
export default BunSQLite;

16
dist/lib/sqlite/db-delete.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
};
export default function DbDelete<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};

49
dist/lib/sqlite/db-delete.js vendored Normal file
View File

@ -0,0 +1,49 @@
import DbClient from ".";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbDelete({ table, query, targetId, }) {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const sqlQueryObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
});
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (whereClause) {
let sql = `DELETE FROM ${table} ${whereClause}`;
const res = DbClient.run(sql, sqlQueryObj.values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sql,
values: sqlQueryObj.values,
},
};
}
else {
return {
success: false,
msg: `No WHERE clause`,
};
}
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

View File

@ -0,0 +1,15 @@
import type { BUN_SQLITE_TableSchemaType } from "../../types";
type Param = {
paradigm: "JavaScript" | "TypeScript" | undefined;
table: BUN_SQLITE_TableSchemaType;
query?: any;
typeDefName?: string;
allValuesOptional?: boolean;
addExport?: boolean;
dbName?: string;
};
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }: Param): {
typeDefinition: string | null;
tdName: string;
};
export {};

View File

@ -0,0 +1,61 @@
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }) {
let typeDefinition = ``;
let tdName = ``;
try {
tdName = typeDefName
? typeDefName
: dbName
? `BUN_SQLITE_${dbName}_${table.tableName}`.toUpperCase()
: `BUN_SQLITE_${query.single}_${query.single_table}`.toUpperCase();
const fields = table.fields;
function typeMap(schemaType) {
if (schemaType.options && schemaType.options.length > 0) {
return schemaType.options
.map((opt) => schemaType.dataType?.match(/int/i) ||
typeof opt == "number"
? `${opt}`
: `"${opt}"`)
.join(" | ");
}
if (schemaType.dataType?.match(/int|double|decimal/i)) {
return "number";
}
if (schemaType.dataType?.match(/text|varchar|timestamp/i)) {
return "string";
}
if (schemaType.dataType?.match(/boolean/i)) {
return "0 | 1";
}
return "string";
}
const typesArrayTypeScript = [];
const typesArrayJavascript = [];
typesArrayTypeScript.push(`${addExport ? "export " : ""}type ${tdName} = {`);
typesArrayJavascript.push(`/**\n * @typedef {object} ${tdName}`);
fields.forEach((field) => {
if (field.fieldDescription) {
typesArrayTypeScript.push(` /** \n * ${field.fieldDescription}\n */`);
}
const nullValue = allValuesOptional
? "?"
: field.notNullValue
? ""
: "?";
typesArrayTypeScript.push(` ${field.fieldName}${nullValue}: ${typeMap(field)};`);
typesArrayJavascript.push(` * @property {${typeMap(field)}${nullValue}} ${field.fieldName}`);
});
typesArrayTypeScript.push(`}`);
typesArrayJavascript.push(` */`);
if (paradigm?.match(/javascript/i)) {
typeDefinition = typesArrayJavascript.join("\n");
}
if (paradigm?.match(/typescript/i)) {
typeDefinition = typesArrayTypeScript.join("\n");
}
}
catch (error) {
console.log(error.message);
typeDefinition = null;
}
return { typeDefinition, tdName };
}

15
dist/lib/sqlite/db-insert.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import type { APIResponseObject } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
data: Schema[];
};
export default function DbInsert<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};

32
dist/lib/sqlite/db-insert.js vendored Normal file
View File

@ -0,0 +1,32 @@
import DbClient from ".";
import sqlInsertGenerator from "../../utils/sql-insert-generator";
export default async function DbInsert({ table, data }) {
try {
const finalData = data.map((d) => ({
...d,
created_at: Date.now(),
updated_at: Date.now(),
}));
const sqlObj = sqlInsertGenerator({
tableName: table,
data: finalData,
});
const res = DbClient.run(sqlObj?.query || "", sqlObj?.values || []);
return {
success: Boolean(Number(res.lastInsertRowid)),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

72
dist/lib/sqlite/db-schema-manager.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env bun
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
declare class SQLiteSchemaManager {
private db;
private db_manager_table_name;
private recreate_vector_table;
private db_schema;
constructor({ schema, recreate_vector_table, }: {
schema: BUN_SQLITE_DatabaseSchemaType;
recreate_vector_table?: boolean;
});
private createDbManagerTable;
private insertDbManagerTable;
private removeDbManagerTable;
/**
* Main synchronization method
*/
syncSchema(): Promise<void>;
/**
* Get list of existing tables in the database
*/
private getExistingTables;
/**
* Drop tables that are no longer in the schema
*/
private dropRemovedTables;
/**
* Sync a single table (create or update)
*/
private syncTable;
/**
* Create a new table
*/
private createTable;
/**
* Update an existing table
*/
private updateTable;
/**
* Get existing columns for a table
*/
private getTableColumns;
/**
* Add a new column to existing table
*/
private addColumn;
/**
* Recreate table (for complex schema changes)
*/
private recreateTable;
/**
* Build column definition SQL
*/
private buildColumnDefinition;
/**
* Map DSQL data types to SQLite types
*/
private mapDataType;
/**
* Build foreign key constraint
*/
private buildForeignKeyConstraint;
/**
* Sync indexes for a table
*/
private syncIndexes;
/**
* Close database connection
*/
close(): void;
}
export { SQLiteSchemaManager };

456
dist/lib/sqlite/db-schema-manager.js vendored Normal file
View File

@ -0,0 +1,456 @@
#!/usr/bin/env bun
import { Database } from "bun:sqlite";
import _ from "lodash";
import DbClient from ".";
// Schema Manager Class
class SQLiteSchemaManager {
db;
db_manager_table_name;
recreate_vector_table;
db_schema;
constructor({ schema, recreate_vector_table = false, }) {
this.db = DbClient;
this.db_manager_table_name = "__db_schema_manager__";
this.db.run("PRAGMA foreign_keys = ON;");
this.recreate_vector_table = recreate_vector_table;
this.createDbManagerTable();
this.db_schema = schema;
}
createDbManagerTable() {
this.db.run(`
CREATE TABLE IF NOT EXISTS ${this.db_manager_table_name} (
table_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
}
insertDbManagerTable(tableName) {
this.db.run(`INSERT INTO ${this.db_manager_table_name} (table_name,created_at,updated_at) VALUES (?, ?, ?)`, [tableName, Date.now(), Date.now()]);
}
removeDbManagerTable(tableName) {
this.db.run(`DELETE FROM ${this.db_manager_table_name} WHERE table_name = ?`, [tableName]);
}
/**
* Main synchronization method
*/
async syncSchema() {
console.log("Starting schema synchronization...");
const existingTables = this.getExistingTables();
const schemaTables = this.db_schema.tables.map((t) => t.tableName);
// 2. Create or update tables
for (const table of this.db_schema.tables) {
await this.syncTable(table, existingTables);
}
// 1. Drop tables that no longer exist in schema
await this.dropRemovedTables(existingTables, schemaTables);
console.log("Schema synchronization complete!");
}
/**
* Get list of existing tables in the database
*/
getExistingTables() {
let sql = `SELECT table_name FROM ${this.db_manager_table_name}`;
const query = this.db.query(sql);
const results = query.all();
return results.map((r) => r.table_name);
}
/**
* Drop tables that are no longer in the schema
*/
async dropRemovedTables(existingTables, schemaTables) {
const tablesToDrop = existingTables.filter((t) => !schemaTables.includes(t) &&
!schemaTables.find((scT) => t.startsWith(scT + "_")));
for (const tableName of tablesToDrop) {
console.log(`Dropping table: ${tableName}`);
this.db.run(`DROP TABLE IF EXISTS "${tableName}"`);
this.db.run(`DELETE FROM ${this.db_manager_table_name} WHERE table_name = "${tableName}"`);
}
}
/**
* Sync a single table (create or update)
*/
async syncTable(table, existingTables) {
let tableExists = existingTables.includes(table.tableName);
// Handle table rename
if (table.tableNameOld && table.tableNameOld !== table.tableName) {
if (existingTables.includes(table.tableNameOld)) {
console.log(`Renaming table: ${table.tableNameOld} -> ${table.tableName}`);
this.db.run(`ALTER TABLE "${table.tableNameOld}" RENAME TO "${table.tableName}"`);
this.insertDbManagerTable(table.tableName);
this.removeDbManagerTable(table.tableNameOld);
tableExists = true;
}
}
if (!tableExists) {
// Create new table
await this.createTable(table);
this.insertDbManagerTable(table.tableName);
}
else {
// Update existing table
await this.updateTable(table);
}
// Sync indexes
await this.syncIndexes(table);
}
/**
* Create a new table
*/
async createTable(table) {
console.log(`Creating table: ${table.tableName}`);
let new_table = _.cloneDeep(table);
if (new_table.parentTableName) {
const parent_table = this.db_schema.tables.find((t) => t.tableName === new_table.parentTableName);
if (!parent_table) {
throw new Error(`Parent table \`${new_table.parentTableName}\` not found for \`${new_table.tableName}\``);
}
new_table = _.merge(parent_table, {
tableName: new_table.tableName,
tableDescription: new_table.tableDescription,
});
}
const columns = [];
const foreignKeys = [];
for (const field of new_table.fields) {
const columnDef = this.buildColumnDefinition(field);
columns.push(columnDef);
if (field.foreignKey) {
foreignKeys.push(this.buildForeignKeyConstraint(field));
}
}
// Add unique constraints
if (new_table.uniqueConstraints) {
for (const constraint of new_table.uniqueConstraints) {
if (constraint.constraintTableFields &&
constraint.constraintTableFields.length > 0) {
const fields = constraint.constraintTableFields
.map((f) => `"${f.value}"`)
.join(", ");
const constraintName = constraint.constraintName ||
`unique_${fields.replace(/"/g, "")}`;
columns.push(`CONSTRAINT "${constraintName}" UNIQUE (${fields})`);
}
}
}
const allConstraints = [...columns, ...foreignKeys];
const sql = new_table.isVector
? `CREATE VIRTUAL TABLE "${new_table.tableName}" USING ${new_table.vectorType || "vec0"}(${allConstraints.join(", ")})`
: `CREATE TABLE "${new_table.tableName}" (${allConstraints.join(", ")})`;
this.db.run(sql);
}
/**
* Update an existing table
*/
async updateTable(table) {
console.log(`Updating table: ${table.tableName}`);
const existingColumns = this.getTableColumns(table.tableName);
const schemaColumns = table.fields.map((f) => f.fieldName || "");
// SQLite has limited ALTER TABLE support
// We need to use the recreation strategy for complex changes
const columnsToAdd = table.fields.filter((f) => f.fieldName &&
!existingColumns.find((c) => c.name == f.fieldName && c.type == this.mapDataType(f)));
const columnsToRemove = existingColumns.filter((c) => !schemaColumns.includes(c.name));
const columnsToUpdate = table.fields.filter((f) => f.fieldName &&
f.updatedField &&
existingColumns.find((c) => c.name == f.fieldName && c.type == this.mapDataType(f)));
// Simple case: only adding columns
if (columnsToRemove.length === 0 && columnsToUpdate.length === 0) {
for (const field of columnsToAdd) {
await this.addColumn(table.tableName, field);
}
}
else {
// Complex case: need to recreate table
await this.recreateTable(table);
}
}
/**
* Get existing columns for a table
*/
getTableColumns(tableName) {
const query = this.db.query(`PRAGMA table_info("${tableName}")`);
const results = query.all();
return results;
}
/**
* Add a new column to existing table
*/
async addColumn(tableName, field) {
console.log(`Adding column: ${tableName}.${field.fieldName}`);
const columnDef = this.buildColumnDefinition(field);
// Remove PRIMARY KEY and UNIQUE constraints for ALTER TABLE ADD COLUMN
const cleanDef = columnDef
.replace(/PRIMARY KEY/gi, "")
.replace(/AUTOINCREMENT/gi, "")
.replace(/UNIQUE/gi, "")
.trim();
const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${cleanDef}`;
this.db.run(sql);
}
/**
* Recreate table (for complex schema changes)
*/
async recreateTable(table) {
if (table.isVector) {
if (!this.recreate_vector_table) {
return;
}
console.log(`Recreating vector table: ${table.tableName}`);
const existingRows = this.db
.query(`SELECT * FROM "${table.tableName}"`)
.all();
this.db.run(`DROP TABLE "${table.tableName}"`);
await this.createTable(table);
if (existingRows.length > 0) {
for (let i = 0; i < existingRows.length; i++) {
const row = existingRows[i];
if (!row)
continue;
const columns = Object.keys(row);
const placeholders = columns.map(() => "?").join(", ");
this.db.run(`INSERT INTO "${table.tableName}" (${columns.join(", ")}) VALUES (${placeholders})`, Object.values(row));
}
}
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
// Get existing data
const existingColumns = this.getTableColumns(table.tableName);
const columnsToKeep = table.fields
.filter((f) => f.fieldName &&
existingColumns.find((c) => c.name == f.fieldName &&
c.type == this.mapDataType(f)))
.map((f) => f.fieldName);
// Create temp table with new schema
const tempTable = { ...table, tableName: tempTableName };
await this.createTable(tempTable);
// Copy data if there are common columns
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep.map((c) => `"${c}"`).join(", ");
this.db.run(`INSERT INTO "${tempTableName}" (${columnList}) SELECT ${columnList} FROM "${table.tableName}"`);
}
// Drop old table
this.db.run(`DROP TABLE "${table.tableName}"`);
// Rename temp table
this.db.run(`ALTER TABLE "${tempTableName}" RENAME TO "${table.tableName}"`);
}
/**
* Build column definition SQL
*/
buildColumnDefinition(field) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const fieldName = field.sideCar
? `+${field.fieldName}`
: `${field.fieldName}`;
const parts = [fieldName];
// Data type mapping
const dataType = this.mapDataType(field);
parts.push(dataType);
// Primary key
if (field.primaryKey) {
parts.push("PRIMARY KEY");
if (field.autoIncrement) {
parts.push("AUTOINCREMENT");
}
}
// Not null
if (field.notNullValue || field.primaryKey) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
// Unique
if (field.unique && !field.primaryKey) {
parts.push("UNIQUE");
}
// Default value
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(
// Escape single quotes by doubling them to prevent SQL injection and wrap in single quotes
`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
return parts.join(" ");
}
/**
* Map DSQL data types to SQLite types
*/
mapDataType(field) {
const dataType = field.dataType?.toLowerCase() || "text";
const vectorSize = field.vectorSize || 1536;
// Vector Embeddings
if (field.isVector) {
return `FLOAT[${vectorSize}]`;
}
// Integer types
if (dataType.includes("int") ||
dataType === "bigint" ||
dataType === "smallint" ||
dataType === "tinyint") {
return "INTEGER";
}
// Real/Float types
if (dataType.includes("real") ||
dataType.includes("float") ||
dataType.includes("double") ||
dataType === "decimal" ||
dataType === "numeric") {
return "REAL";
}
// Blob types
if (dataType.includes("blob") || dataType.includes("binary")) {
return "BLOB";
}
// Boolean
if (dataType === "boolean" || dataType === "bool") {
return "INTEGER"; // SQLite uses INTEGER for boolean (0/1)
}
// Date/Time types
if (dataType.includes("date") || dataType.includes("time")) {
return "TEXT"; // SQLite stores dates as TEXT or INTEGER
}
// Default to TEXT for all text-based types
return "TEXT";
}
/**
* Build foreign key constraint
*/
buildForeignKeyConstraint(field) {
const fk = field.foreignKey;
let constraint = `FOREIGN KEY ("${field.fieldName}") REFERENCES "${fk.destinationTableName}"("${fk.destinationTableColumnName}")`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}
/**
* Sync indexes for a table
*/
async syncIndexes(table) {
if (!table.indexes || table.indexes.length === 0) {
return;
}
// Get existing indexes
const query = this.db.query(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='${table.tableName}' AND name NOT LIKE 'sqlite_%'`);
const existingIndexes = query.all().map((r) => r.name);
// Drop indexes not in schema
for (const indexName of existingIndexes) {
const stillExists = table.indexes.some((idx) => idx.indexName === indexName);
if (!stillExists) {
console.log(`Dropping index: ${indexName}`);
this.db.run(`DROP INDEX IF EXISTS "${indexName}"`);
}
}
// Create new indexes
for (const index of table.indexes) {
if (!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0) {
continue;
}
if (!existingIndexes.includes(index.indexName)) {
console.log(`Creating index: ${index.indexName}`);
const fields = index.indexTableFields
.map((f) => `"${f.value}"`)
.join(", ");
const unique = index.indexType === "regular" ? "" : ""; // SQLite doesn't have FULLTEXT in CREATE INDEX
this.db.run(`CREATE ${unique}INDEX "${index.indexName}" ON "${table.tableName}" (${fields})`);
}
}
}
/**
* Close database connection
*/
close() {
this.db.close();
}
}
// Example usage
async function main() {
const schema = {
dbName: "example_db",
tables: [
{
tableName: "users",
tableDescription: "User accounts",
fields: [
{
fieldName: "id",
dataType: "INTEGER",
primaryKey: true,
autoIncrement: true,
},
{
fieldName: "username",
dataType: "TEXT",
notNullValue: true,
unique: true,
},
{
fieldName: "email",
dataType: "TEXT",
notNullValue: true,
},
{
fieldName: "created_at",
dataType: "TEXT",
defaultValueLiteral: "CURRENT_TIMESTAMP",
},
],
indexes: [
{
indexName: "idx_users_email",
indexType: "regular",
indexTableFields: [
{ value: "email", dataType: "TEXT" },
],
},
],
},
{
tableName: "posts",
fields: [
{
fieldName: "id",
dataType: "INTEGER",
primaryKey: true,
autoIncrement: true,
},
{
fieldName: "user_id",
dataType: "INTEGER",
notNullValue: true,
foreignKey: {
destinationTableName: "users",
destinationTableColumnName: "id",
cascadeDelete: true,
},
},
{
fieldName: "title",
dataType: "TEXT",
notNullValue: true,
},
{
fieldName: "content",
dataType: "TEXT",
},
],
},
],
};
}
export { SQLiteSchemaManager };

View File

@ -0,0 +1,6 @@
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
type Params = {
dbSchema?: BUN_SQLITE_DatabaseSchemaType;
};
export default function dbSchemaToType(params?: Params): string[] | undefined;
export {};

44
dist/lib/sqlite/db-schema-to-typedef.js vendored Normal file
View File

@ -0,0 +1,44 @@
import _ from "lodash";
import generateTypeDefinition from "./db-generate-type-defs";
export default function dbSchemaToType(params) {
let datasquirelSchema = params?.dbSchema;
if (!datasquirelSchema)
return;
let tableNames = `export const BunSQLiteTables = [\n${datasquirelSchema.tables
.map((tbl) => ` "${tbl.tableName}",`)
.join("\n")}\n] as const`;
const dbTablesSchemas = datasquirelSchema.tables;
const defDbName = datasquirelSchema.dbName
?.toUpperCase()
.replace(/ |\-/g, "_");
const defNames = [];
const schemas = dbTablesSchemas
.map((table) => {
let final_table = _.cloneDeep(table);
if (final_table.parentTableName) {
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
if (parent_table) {
final_table = _.merge(parent_table, {
tableName: final_table.tableName,
tableDescription: final_table.tableDescription,
});
}
}
const defObj = generateTypeDefinition({
paradigm: "TypeScript",
table: final_table,
typeDefName: `BUN_SQLITE_${defDbName}_${final_table.tableName.toUpperCase()}`,
allValuesOptional: true,
addExport: true,
});
if (defObj.tdName?.match(/./)) {
defNames.push(defObj.tdName);
}
return defObj.typeDefinition;
})
.filter((schm) => typeof schm == "string");
const allTd = defNames?.[0]
? `export type BUN_SQLITE_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}`
: ``;
return [tableNames, ...schemas, allTd];
}

17
dist/lib/sqlite/db-select.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
query?: ServerQueryParam<Schema>;
table: Table;
count?: boolean;
targetId?: number | string;
};
export default function DbSelect<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, count, targetId, }: Params<Schema, Table>): Promise<APIResponseObject<Schema>>;
export {};

48
dist/lib/sqlite/db-select.js vendored Normal file
View File

@ -0,0 +1,48 @@
import mysql from "mysql";
import DbClient from ".";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbSelect({ table, query, count, targetId, }) {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const sqlObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
count,
});
const sql = mysql.format(sqlObj.string, sqlObj.values);
const res = DbClient.query(sql);
const batchRes = res.all();
let resp = {
success: Boolean(batchRes[0]),
payload: batchRes,
singleRes: batchRes[0],
debug: {
sqlObj,
sql,
},
};
if (count) {
const count_val = count ? batchRes[0]?.["COUNT(*)"] : undefined;
resp["count"] = Number(count_val);
delete resp.payload;
delete resp.singleRes;
}
return resp;
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

11
dist/lib/sqlite/db-sql.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import type { APIResponseObject } from "../../types";
type Params = {
sql: string;
values?: (string | number)[];
};
export default function DbSQL<T extends {
[k: string]: any;
} = {
[k: string]: any;
}>({ sql, values }: Params): Promise<APIResponseObject<T>>;
export {};

33
dist/lib/sqlite/db-sql.js vendored Normal file
View File

@ -0,0 +1,33 @@
import DbClient from ".";
import _ from "lodash";
export default async function DbSQL({ sql, values }) {
try {
const res = sql.match(/^select/i)
? DbClient.query(sql).all(...(values || []))
: DbClient.run(sql, values || []);
return {
success: true,
payload: Array.isArray(res) ? res : undefined,
singleRes: Array.isArray(res) ? res?.[0] : undefined,
postInsertReturn: Array.isArray(res)
? undefined
: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj: {
sql,
values,
},
sql,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

17
dist/lib/sqlite/db-update.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
data: Schema;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
};
export default function DbUpdate<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};

68
dist/lib/sqlite/db-update.js vendored Normal file
View File

@ -0,0 +1,68 @@
import DbClient from ".";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbUpdate({ table, data, query, targetId, }) {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const sqlQueryObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
});
let values = [];
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (whereClause) {
let sql = `UPDATE ${table} SET`;
const finalData = {
...data,
updated_at: Date.now(),
};
const keys = Object.keys(finalData);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
const isLast = i == keys.length - 1;
sql += ` ${key}=?`;
values.push(String(finalData[key]));
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
const res = DbClient.run(sql, values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sql,
values,
},
};
}
else {
return {
success: false,
msg: `No WHERE clause`,
};
}
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

3
dist/lib/sqlite/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import { Database } from "bun:sqlite";
declare const DbClient: Database;
export default DbClient;

17
dist/lib/sqlite/index.js vendored Normal file
View File

@ -0,0 +1,17 @@
import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import grabDirNames from "../../data/grab-dir-names";
import init from "../../functions/init";
import grabDBDir from "../../utils/grab-db-dir";
const { ROOT_DIR } = grabDirNames();
const { config } = await init();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
}
const { db_file_path } = grabDBDir({ config });
const DbClient = new Database(db_file_path, {
create: true,
});
sqliteVec.load(DbClient);
export default DbClient;

View File

@ -0,0 +1,7 @@
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
type Params = {
dbSchema: BUN_SQLITE_DatabaseSchemaType;
dst_file: string;
};
export default function dbSchemaToTypeDef({ dbSchema, dst_file }: Params): void;
export {};

18
dist/lib/sqlite/schema-to-typedef.js vendored Normal file
View File

@ -0,0 +1,18 @@
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import dbSchemaToType from "./db-schema-to-typedef";
export default function dbSchemaToTypeDef({ dbSchema, dst_file }) {
try {
if (!dbSchema)
throw new Error("No schema found");
const definitions = dbSchemaToType({ dbSchema });
const ourfileDir = path.dirname(dst_file);
if (!existsSync(ourfileDir)) {
mkdirSync(ourfileDir, { recursive: true });
}
writeFileSync(dst_file, definitions?.join("\n\n") || "", "utf-8");
}
catch (error) {
console.log(`Schema to Typedef Error =>`, error.message);
}
}

2
dist/lib/sqlite/schema.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
export declare const DbSchema: BUN_SQLITE_DatabaseSchemaType;

5
dist/lib/sqlite/schema.js vendored Normal file
View File

@ -0,0 +1,5 @@
import _ from "lodash";
export const DbSchema = {
dbName: "travis-ai",
tables: [],
};

1015
dist/types/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

123
dist/types/index.js vendored Normal file
View File

@ -0,0 +1,123 @@
export const UsersOmitedFields = [
"password",
"social_id",
"verification_status",
"date_created",
"date_created_code",
"date_created_timestamp",
"date_updated",
"date_updated_code",
"date_updated_timestamp",
];
export const MariaDBCollations = [
"utf8mb4_bin",
"utf8mb4_unicode_520_ci",
];
export const TextFieldTypesArray = [
{ title: "Plain Text", value: "plain" },
{ title: "Rich Text", value: "richText" },
{ title: "Markdown", value: "markdown" },
{ title: "JSON", value: "json" },
{ title: "YAML", value: "yaml" },
{ title: "HTML", value: "html" },
{ title: "CSS", value: "css" },
{ title: "Javascript", value: "javascript" },
{ title: "Shell", value: "shell" },
{ title: "Code", value: "code" },
];
export const BUN_SQLITE_DATATYPES = [
{ value: "TEXT" },
{ value: "INTEGER" },
];
export const ServerQueryOperators = ["AND", "OR"];
export const ServerQueryEqualities = [
"EQUAL",
"LIKE",
"LIKE_RAW",
"LIKE_LOWER",
"LIKE_LOWER_RAW",
"NOT LIKE",
"NOT LIKE_RAW",
"NOT_LIKE_LOWER",
"NOT_LIKE_LOWER_RAW",
"NOT EQUAL",
"REGEXP",
"FULLTEXT",
"IN",
"NOT IN",
"BETWEEN",
"NOT BETWEEN",
"IS NULL",
"IS NOT NULL",
"EXISTS",
"NOT EXISTS",
"GREATER THAN",
"GREATER THAN OR EQUAL",
"LESS THAN",
"LESS THAN OR EQUAL",
"MATCH",
"MATCH_BOOLEAN",
];
export const DataCrudRequestMethods = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS",
];
export const DataCrudRequestMethodsLowerCase = [
"get",
"post",
"put",
"patch",
"delete",
"options",
];
export const DsqlCrudActions = ["insert", "update", "delete", "get"];
export const QueryFields = [
"duplicate",
"user_id",
"delegated_user_id",
"db_id",
"table_id",
"db_slug",
];
export const DockerComposeServices = [
"setup",
"cron",
"reverse-proxy",
"webapp",
"websocket",
"static",
"db",
"maxscale",
"post-db-setup",
"web-app-post-db-setup",
"post-replica-db-setup",
"db-replica-1",
"db-replica-2",
"db-cron",
"web-app-post-db-setup",
];
export const IndexTypes = ["regular", "full_text", "vector"];
export const DefaultFields = [
{
fieldName: "id",
dataType: "INTEGER",
primaryKey: true,
autoIncrement: true,
notNullValue: true,
fieldDescription: "The unique identifier of the record.",
},
{
fieldName: "created_at",
dataType: "INTEGER",
fieldDescription: "The time when the record was created. (Unix Timestamp)",
},
{
fieldName: "updated_at",
dataType: "INTEGER",
fieldDescription: "The time when the record was updated. (Unix Timestamp)",
},
];

View File

@ -0,0 +1,6 @@
import { type BUN_SQLITE_DatabaseSchemaType } from "../types";
type Params = {
dbSchema: BUN_SQLITE_DatabaseSchemaType;
};
export default function ({ dbSchema }: Params): BUN_SQLITE_DatabaseSchemaType;
export {};

View File

@ -0,0 +1,12 @@
import _ from "lodash";
import { DefaultFields } from "../types";
export default function ({ dbSchema }) {
const finaldbSchema = _.cloneDeep(dbSchema);
finaldbSchema.tables = finaldbSchema.tables.map((t) => {
const newTable = _.cloneDeep(t);
newTable.fields = newTable.fields.filter((f) => !f.fieldName?.match(/^(id|created_at|updated_at)$/));
newTable.fields.unshift(...DefaultFields);
return newTable;
});
return finaldbSchema;
}

9
dist/utils/grab-backup-data.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
type Params = {
backup_name: string;
};
export default function grabBackupData({ backup_name }: Params): {
backup_date: Date;
backup_date_timestamp: number;
origin_backup_name: string;
};
export {};

7
dist/utils/grab-backup-data.js vendored Normal file
View File

@ -0,0 +1,7 @@
export default function grabBackupData({ backup_name }) {
const backup_parts = backup_name.split("-");
const backup_date_timestamp = Number(backup_parts.pop());
const origin_backup_name = backup_parts.join("-");
const backup_date = new Date(backup_date_timestamp);
return { backup_date, backup_date_timestamp, origin_backup_name };
}

View File

@ -0,0 +1,6 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabDBBackupFileName({ config }: Params): string;
export {};

View File

@ -0,0 +1,4 @@
export default function grabDBBackupFileName({ config }) {
const new_db_file_name = `${config.db_name}-${Date.now()}`;
return new_db_file_name;
}

10
dist/utils/grab-db-dir.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabDBDir({ config }: Params): {
db_dir: string;
backup_dir: string;
db_file_path: string;
};
export {};

14
dist/utils/grab-db-dir.js vendored Normal file
View File

@ -0,0 +1,14 @@
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import { AppData } from "../data/app-data";
export default function grabDBDir({ config }) {
const { ROOT_DIR } = grabDirNames();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
}
const backup_dir_name = config.db_backup_dir || AppData["DefaultBackupDirName"];
const backup_dir = path.resolve(db_dir, backup_dir_name);
const db_file_path = path.resolve(db_dir, config.db_name);
return { db_dir, backup_dir, db_file_path };
}

6
dist/utils/grab-sorted-backups.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabSortedBackups({ config }: Params): string[];
export {};

18
dist/utils/grab-sorted-backups.js vendored Normal file
View File

@ -0,0 +1,18 @@
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
export default function grabSortedBackups({ config }) {
const { backup_dir } = grabDBDir({ config });
const backups = fs.readdirSync(backup_dir);
/**
* Order Backups. Most recent first.
*/
const ordered_backups = backups.sort((a, b) => {
const a_date = Number(a.split("-").pop());
const b_date = Number(b.split("-").pop());
if (a_date > b_date) {
return -1;
}
return 1;
});
return ordered_backups;
}

2
dist/utils/sql-equality-parser.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { ServerQueryEqualities } from "../types";
export default function sqlEqualityParser(eq: (typeof ServerQueryEqualities)[number]): string;

39
dist/utils/sql-equality-parser.js vendored Normal file
View File

@ -0,0 +1,39 @@
import { ServerQueryEqualities } from "../types";
export default function sqlEqualityParser(eq) {
switch (eq) {
case "EQUAL":
return "=";
case "LIKE":
return "LIKE";
case "NOT LIKE":
return "NOT LIKE";
case "NOT EQUAL":
return "<>";
case "IN":
return "IN";
case "NOT IN":
return "NOT IN";
case "BETWEEN":
return "BETWEEN";
case "NOT BETWEEN":
return "NOT BETWEEN";
case "IS NULL":
return "IS NULL";
case "IS NOT NULL":
return "IS NOT NULL";
case "EXISTS":
return "EXISTS";
case "NOT EXISTS":
return "NOT EXISTS";
case "GREATER THAN":
return ">";
case "GREATER THAN OR EQUAL":
return ">=";
case "LESS THAN":
return "<";
case "LESS THAN OR EQUAL":
return "<=";
default:
return "=";
}
}

20
dist/utils/sql-gen-operator-gen.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
import type { ServerQueryEqualities, ServerQueryObject } from "../types";
type Params = {
fieldName: string;
value?: string;
equality?: (typeof ServerQueryEqualities)[number];
queryObj: ServerQueryObject<{
[key: string]: any;
}, string>;
isValueFieldValue?: boolean;
};
type Return = {
str?: string;
param?: string;
};
/**
* # SQL Gen Operator Gen
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }: Params): Return;
export {};

127
dist/utils/sql-gen-operator-gen.js vendored Normal file
View File

@ -0,0 +1,127 @@
import sqlEqualityParser from "./sql-equality-parser";
/**
* # SQL Gen Operator Gen
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }) {
if (queryObj.nullValue) {
return { str: `${fieldName} IS NULL` };
}
if (queryObj.notNullValue) {
return { str: `${fieldName} IS NOT NULL` };
}
if (value) {
const finalValue = isValueFieldValue ? value : "?";
const finalParams = isValueFieldValue ? undefined : value;
if (equality == "MATCH") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN NATURAL LANGUAGE MODE)`,
param: finalParams,
};
}
else if (equality == "MATCH_BOOLEAN") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
}
else if (equality == "LIKE_LOWER") {
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
};
}
else if (equality == "LIKE_LOWER_RAW") {
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: finalParams,
};
}
else if (equality == "LIKE") {
return {
str: `${fieldName} LIKE ${finalValue}`,
param: `%${finalParams}%`,
};
}
else if (equality == "LIKE_RAW") {
return {
str: `${fieldName} LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "NOT_LIKE_LOWER") {
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
};
}
else if (equality == "NOT_LIKE_LOWER_RAW") {
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: finalParams,
};
}
else if (equality == "NOT LIKE") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "NOT LIKE_RAW") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "REGEXP") {
return {
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
param: finalParams,
};
}
else if (equality == "FULLTEXT") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
}
else if (equality == "NOT EQUAL") {
return {
str: `${fieldName} != ${finalValue}`,
param: finalParams,
};
}
else if (equality) {
return {
str: `${fieldName} ${sqlEqualityParser(equality)} ${finalValue}`,
param: finalParams,
};
}
else {
return {
str: `${fieldName} = ${finalValue}`,
param: finalParams,
};
}
}
else {
if (equality == "IS NULL") {
return { str: `${fieldName} IS NULL` };
}
else if (equality == "IS NOT NULL") {
return { str: `${fieldName} IS NOT NULL` };
}
else if (equality) {
return {
str: `${fieldName} ${sqlEqualityParser(equality)} ?`,
param: value,
};
}
else {
return {
str: `${fieldName} = ?`,
param: value,
};
}
}
}

25
dist/utils/sql-generator.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
import type { ServerQueryParam } from "../types";
type Param<T extends {
[key: string]: any;
} = {
[key: string]: any;
}> = {
genObject?: ServerQueryParam<T>;
tableName: string;
dbFullName?: string;
count?: boolean;
};
type Return = {
string: string;
values: (string | number)[];
};
/**
* # SQL Query Generator
* @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenerator<T extends {
[key: string]: any;
} = {
[key: string]: any;
}>({ tableName, genObject, dbFullName, count }: Param<T>): Return;
export {};

392
dist/utils/sql-generator.js vendored Normal file
View File

@ -0,0 +1,392 @@
import { isUndefined } from "lodash";
import sqlGenOperatorGen from "./sql-gen-operator-gen";
/**
* # SQL Query Generator
* @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenerator({ tableName, genObject, dbFullName, count }) {
const finalQuery = genObject?.query ? genObject.query : undefined;
const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined;
const sqlSearhValues = [];
const finalDbName = dbFullName ? `${dbFullName}.` : "";
/**
* # Generate Query
*/
function genSqlSrchStr({ queryObj, join, field, }) {
const finalFieldName = (() => {
if (queryObj?.tableName) {
return `${finalDbName}${queryObj.tableName}.${field}`;
}
if (join) {
return `${finalDbName}${tableName}.${field}`;
}
return field;
})();
let str = `${finalFieldName}=?`;
function grabValue(val) {
const valueParsed = val;
if (!valueParsed)
return;
const valueString = typeof valueParsed == "string"
? valueParsed
: valueParsed
? valueParsed.fieldName && valueParsed.tableName
? `${valueParsed.tableName}.${valueParsed.fieldName}`
: valueParsed.value?.toString()
: undefined;
const valueEquality = typeof valueParsed == "object"
? valueParsed.equality || queryObj.equality
: queryObj.equality;
const operatorStrParam = sqlGenOperatorGen({
queryObj,
equality: valueEquality,
fieldName: finalFieldName || "",
value: valueString?.toString() || "",
isValueFieldValue: Boolean(typeof valueParsed == "object" &&
valueParsed.fieldName &&
valueParsed.tableName),
});
return operatorStrParam;
}
if (Array.isArray(queryObj.value)) {
const strArray = [];
queryObj.value.forEach((val) => {
const operatorStrParam = grabValue(val);
if (!operatorStrParam)
return;
if (operatorStrParam.str && operatorStrParam.param) {
strArray.push(operatorStrParam.str);
sqlSearhValues.push(operatorStrParam.param);
}
else if (operatorStrParam.str) {
strArray.push(operatorStrParam.str);
}
});
str = "(" + strArray.join(` ${queryObj.operator || "AND"} `) + ")";
}
else if (typeof queryObj.value == "object") {
const operatorStrParam = grabValue(queryObj.value);
if (operatorStrParam?.str) {
str = operatorStrParam.str;
if (operatorStrParam.param) {
sqlSearhValues.push(operatorStrParam.param);
}
}
}
else {
const valueParsed = queryObj.value
? String(queryObj.value)
: undefined;
const operatorStrParam = sqlGenOperatorGen({
equality: queryObj.equality,
fieldName: finalFieldName || "",
value: valueParsed,
queryObj,
});
if (operatorStrParam.str && operatorStrParam.param) {
str = operatorStrParam.str;
sqlSearhValues.push(operatorStrParam.param);
}
else if (operatorStrParam.str) {
str = operatorStrParam.str;
}
}
return str;
}
function generateJoinStr(mtch, join) {
if (mtch.__batch) {
let btch_mtch = ``;
btch_mtch += `(`;
for (let i = 0; i < mtch.__batch.matches.length; i++) {
const __mtch = mtch.__batch.matches[i];
btch_mtch += `${generateJoinStr(__mtch, join)}`;
if (i < mtch.__batch.matches.length - 1) {
btch_mtch += ` ${mtch.__batch.operator || "OR"} `;
}
}
btch_mtch += `)`;
return btch_mtch;
}
return `${finalDbName}${typeof mtch.source == "object" ? mtch.source.tableName : tableName}.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}=${(() => {
if (mtch.targetLiteral) {
if (typeof mtch.targetLiteral == "number") {
return `${mtch.targetLiteral}`;
}
return `'${mtch.targetLiteral}'`;
}
if (join.alias) {
return `${finalDbName}${typeof mtch.target == "object"
? mtch.target.tableName
: join.alias}.${typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target}`;
}
return `${finalDbName}${typeof mtch.target == "object"
? mtch.target.tableName
: join.tableName}.${typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target}`;
})()}`;
}
let fullTextMatchStr = genObject?.fullTextSearch
? ` MATCH(${genObject.fullTextSearch.fields
.map((f) => genObject.join ? `${tableName}.${String(f)}` : `${String(f)}`)
.join(",")}) AGAINST (? IN BOOLEAN MODE)`
: undefined;
const fullTextSearchStr = genObject?.fullTextSearch
? genObject.fullTextSearch.searchTerm
.split(` `)
.map((t) => `${t}`)
.join(" ")
: undefined;
let queryString = (() => {
let str = "SELECT";
if (count) {
str += ` COUNT(*)`;
}
else if (genObject?.selectFields?.[0]) {
if (genObject.join) {
str += ` ${genObject.selectFields
?.map((fld) => typeof fld == "object"
? `${finalDbName}${tableName}.${fld.fieldName.toString()}` +
(fld.alias ? ` as ${fld.alias}` : ``)
: `${finalDbName}${tableName}.${String(fld)}`)
.join(",")}`;
}
else {
str += ` ${genObject.selectFields
?.map((fld) => typeof fld == "object"
? `${fld.fieldName.toString()}` +
(fld.alias ? ` as ${fld.alias}` : ``)
: fld)
.join(",")}`;
}
}
else {
if (genObject?.join) {
str += ` ${finalDbName}${tableName}.*`;
}
else {
str += " *";
}
}
if (genObject?.countSubQueries) {
let countSqls = [];
for (let i = 0; i < genObject.countSubQueries.length; i++) {
const countSubQuery = genObject.countSubQueries[i];
if (!countSubQuery)
continue;
const tableAlias = countSubQuery.table_alias;
let subQStr = `(SELECT COUNT(*)`;
subQStr += ` FROM ${countSubQuery.table}${tableAlias ? ` ${tableAlias}` : ""}`;
subQStr += ` WHERE (`;
for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) {
const csqSrc = countSubQuery.srcTrgMap[j];
if (!csqSrc)
continue;
subQStr += ` ${tableAlias || countSubQuery.table}.${csqSrc.src}`;
if (typeof csqSrc.trg == "string") {
subQStr += ` = ?`;
sqlSearhValues.push(csqSrc.trg);
}
else if (typeof csqSrc.trg == "object") {
subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`;
}
if (j < countSubQuery.srcTrgMap.length - 1) {
subQStr += ` AND `;
}
}
subQStr += ` )) AS ${countSubQuery.alias}`;
countSqls.push(subQStr);
}
str += `, ${countSqls.join(",")}`;
}
if (genObject?.join && !count) {
const existingJoinTableNames = [tableName];
str +=
"," +
genObject.join
.flat()
.filter((j) => !isUndefined(j))
.map((joinObj) => {
const joinTableName = joinObj.alias
? joinObj.alias
: joinObj.tableName;
if (existingJoinTableNames.includes(joinTableName))
return null;
existingJoinTableNames.push(joinTableName);
if (joinObj.selectFields) {
return joinObj.selectFields
.map((selectField) => {
if (typeof selectField == "string") {
return `${finalDbName}${joinTableName}.${selectField}`;
}
else if (typeof selectField == "object") {
let aliasSelectField = selectField.count
? `COUNT(${finalDbName}${joinTableName}.${selectField.field})`
: `${finalDbName}${joinTableName}.${selectField.field}`;
if (selectField.alias)
aliasSelectField += ` AS ${selectField.alias}`;
return aliasSelectField;
}
})
.join(",");
}
else {
return `${finalDbName}${joinTableName}.*`;
}
})
.filter((_) => Boolean(_))
.join(",");
}
if (genObject?.fullTextSearch &&
fullTextMatchStr &&
fullTextSearchStr) {
str += `, ${fullTextMatchStr} AS ${genObject.fullTextSearch.scoreAlias}`;
sqlSearhValues.push(fullTextSearchStr);
}
str += ` FROM ${finalDbName}${tableName}`;
if (genObject?.join) {
str +=
" " +
genObject.join
.flat()
.filter((j) => !isUndefined(j))
.map((join) => {
return (join.joinType +
" " +
(join.alias
? `${finalDbName}${join.tableName}` +
" " +
join.alias
: `${finalDbName}${join.tableName}`) +
" ON " +
(() => {
if (Array.isArray(join.match)) {
return ("(" +
join.match
.map((mtch) => generateJoinStr(mtch, join))
.join(join.operator
? ` ${join.operator} `
: " AND ") +
")");
}
else if (typeof join.match == "object") {
return generateJoinStr(join.match, join);
}
})());
})
.join(" ");
}
return str;
})();
const sqlSearhString = queryKeys?.map((field) => {
const queryObj = finalQuery?.[field];
if (!queryObj)
return;
if (queryObj.__query) {
const subQueryGroup = queryObj.__query;
const subSearchKeys = Object.keys(subQueryGroup);
const subSearchString = subSearchKeys.map((_field) => {
const newSubQueryObj = subQueryGroup?.[_field];
if (newSubQueryObj) {
return genSqlSrchStr({
queryObj: newSubQueryObj,
field: newSubQueryObj.fieldName || _field,
join: genObject?.join,
});
}
});
return ("(" +
subSearchString.join(` ${queryObj.operator || "AND"} `) +
")");
}
return genSqlSrchStr({
queryObj,
field: queryObj.fieldName || field,
join: genObject?.join,
});
});
const cleanedUpSearchStr = sqlSearhString?.filter((str) => typeof str == "string");
const isSearchStr = cleanedUpSearchStr?.[0] && cleanedUpSearchStr.find((str) => str);
if (isSearchStr) {
const stringOperator = genObject?.searchOperator || "AND";
queryString += ` WHERE ${cleanedUpSearchStr.join(` ${stringOperator} `)}`;
}
if (genObject?.fullTextSearch && fullTextSearchStr && fullTextMatchStr) {
queryString += `${isSearchStr ? " AND" : " WHERE"} ${fullTextMatchStr}`;
sqlSearhValues.push(fullTextSearchStr);
}
if (genObject?.group) {
let group_by_txt = ``;
if (typeof genObject.group == "string") {
group_by_txt = genObject.group;
}
else if (Array.isArray(genObject.group)) {
for (let i = 0; i < genObject.group.length; i++) {
const group = genObject.group[i];
if (typeof group == "string") {
group_by_txt += `\`${group.toString()}\``;
}
else if (typeof group == "object" && group.table) {
group_by_txt += `${group.table}.${String(group.field)}`;
}
else if (typeof group == "object") {
group_by_txt += `${String(group.field)}`;
}
if (i < genObject.group.length - 1) {
group_by_txt += ",";
}
}
}
else if (typeof genObject.group == "object") {
if (genObject.group.table) {
group_by_txt = `${genObject.group.table}.${String(genObject.group.field)}`;
}
else {
group_by_txt = `${String(genObject.group.field)}`;
}
}
queryString += ` GROUP BY ${group_by_txt}`;
}
function grabOrderString(order) {
let orderFields = [];
let orderSrt = ``;
if (genObject?.fullTextSearch && genObject.fullTextSearch.scoreAlias) {
orderFields.push(genObject.fullTextSearch.scoreAlias);
}
else if (genObject?.join) {
orderFields.push(`${finalDbName}${tableName}.${String(order.field)}`);
}
else {
orderFields.push(order.field);
}
orderSrt += ` ${orderFields.join(", ")} ${order.strategy}`;
return orderSrt;
}
if (genObject?.order && !count) {
let orderSrt = ` ORDER BY`;
if (Array.isArray(genObject.order)) {
for (let i = 0; i < genObject.order.length; i++) {
const order = genObject.order[i];
if (order) {
orderSrt +=
grabOrderString(order) +
(i < genObject.order.length - 1 ? `,` : "");
}
}
}
else {
orderSrt += grabOrderString(genObject.order);
}
queryString += ` ${orderSrt}`;
}
if (genObject?.limit && !count)
queryString += ` LIMIT ${genObject.limit}`;
if (genObject?.offset && !count)
queryString += ` OFFSET ${genObject.offset}`;
return {
string: queryString,
values: sqlSearhValues,
};
}

5
dist/utils/sql-insert-generator.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { SQLInsertGenParams, SQLInsertGenReturn } from "../types";
/**
* # SQL Insert Generator
*/
export default function sqlInsertGenerator({ tableName, data, dbFullName, }: SQLInsertGenParams): SQLInsertGenReturn | undefined;

56
dist/utils/sql-insert-generator.js vendored Normal file
View File

@ -0,0 +1,56 @@
/**
* # SQL Insert Generator
*/
export default function sqlInsertGenerator({ tableName, data, dbFullName, }) {
const finalDbName = dbFullName ? `${dbFullName}.` : "";
try {
if (Array.isArray(data) && data?.[0]) {
let insertKeys = [];
data.forEach((dt) => {
const kys = Object.keys(dt);
kys.forEach((ky) => {
if (!insertKeys.includes(ky)) {
insertKeys.push(ky);
}
});
});
let queryBatches = [];
let queryValues = [];
data.forEach((item) => {
queryBatches.push(`(${insertKeys
.map((ky) => {
const value = item[ky];
const finalValue = typeof value == "string" ||
typeof value == "number"
? value
: value
? String(value().value)
: null;
if (!finalValue) {
queryValues.push("");
return "?";
}
queryValues.push(finalValue);
const placeholder = typeof value == "function"
? value().placeholder
: "?";
return placeholder;
})
.filter((k) => Boolean(k))
.join(",")})`);
});
let query = `INSERT INTO ${finalDbName}${tableName} (${insertKeys.join(",")}) VALUES ${queryBatches.join(",")}`;
return {
query: query,
values: queryValues,
};
}
else {
return undefined;
}
}
catch ( /** @type {any} */error) {
console.log(`SQL insert gen ERROR: ${error.message}`);
return undefined;
}
}

6
dist/utils/trim-backups.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function trimBackups({ config }: Params): void;
export {};

19
dist/utils/trim-backups.js vendored Normal file
View File

@ -0,0 +1,19 @@
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import grabSortedBackups from "./grab-sorted-backups";
import { AppData } from "../data/app-data";
import path from "path";
export default function trimBackups({ config }) {
const { backup_dir } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
const max_backups = config.max_backups || AppData["MaxBackups"];
for (let i = 0; i < backups.length; i++) {
const backup_name = backups[i];
if (!backup_name)
continue;
if (i > max_backups - 1) {
const backup_file_to_unlink = path.join(backup_dir, backup_name);
fs.unlinkSync(backup_file_to_unlink);
}
}
}

1196
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "@moduletrace/nsqlite",
"version": "1.0.8",
"description": "SQLite manager for Bun",
"author": "Benjamin Toby",
"main": "dist/index.js",
"bin": {
"nsqlite": "dist/commands/index.js"
},
"scripts": {
"dev": "tsc --watch",
"compile": "rm -rf dist && tsc"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "latest",
"@types/lodash": "^4.17.24",
"@types/mysql": "^2.15.27",
"@types/node": "^25.3.3"
},
"peerDependencies": {
"typescript": "^5"
},
"files": [
"dist",
"README.md",
"package.json"
],
"repository": {
"type": "git",
"url": "git+https://git.tben.me/Moduletrace/nsqlite.git"
},
"dependencies": {
"@inquirer/prompts": "^8.3.0",
"better-sqlite3": "^12.6.2",
"chalk": "^5.6.2",
"commander": "^14.0.3",
"inquirer": "^13.3.0",
"lodash": "^4.17.23",
"mysql": "^2.18.1",
"sqlite-vec": "^0.1.7-alpha.2"
}
}

14
publish.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
if [ -z "$1" ]; then
msg="Updates"
else
msg="$1"
fi
rm -rf dist
tsc
git add .
git commit -m "$msg"
git push
bun publish

29
src/commands/backup.ts Normal file
View File

@ -0,0 +1,29 @@
import { Command } from "commander";
import init from "../functions/init";
import path from "path";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk";
import trimBackups from "../utils/trim-backups";
export default function () {
return new Command("backup")
.description("Backup Database")
.action(async (opts) => {
console.log(`Backing up database ...`);
const { config } = await init();
const { backup_dir, db_file_path } = grabDBDir({ config });
const new_db_file_name = grabDBBackupFileName({ config });
fs.cpSync(db_file_path, path.join(backup_dir, new_db_file_name));
trimBackups({ config });
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
process.exit();
});
}

44
src/commands/index.ts Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env bun
import { program } from "commander";
import schema from "./schema";
import typedef from "./typedef";
import backup from "./backup";
import restore from "./restore";
/**
* # Declare Global Variables
*/
declare global {}
/**
* # Describe Program
*/
program
.name(`bun-sqlite`)
.description(`SQLite manager for Bun`)
.version(`1.0.0`);
/**
* # Declare Commands
*/
program.addCommand(schema());
program.addCommand(typedef());
program.addCommand(backup());
program.addCommand(restore());
/**
* # Handle Unavailable Commands
*/
program.on("command:*", () => {
console.error(
"Invalid command: %s\nSee --help for a list of available commands.",
program.args.join(" "),
);
process.exit(1);
});
/**
* # Parse Arguments
*/
program.parse(process.argv);

56
src/commands/restore.ts Normal file
View File

@ -0,0 +1,56 @@
import { Command } from "commander";
import init from "../functions/init";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import chalk from "chalk";
import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data";
import path from "path";
export default function () {
return new Command("restore")
.description("Restore Database")
.action(async (opts) => {
console.log(`Restoring up database ...`);
const { config } = await init();
const { backup_dir, db_file_path } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
if (!backups?.[0]) {
console.error(
`No Backups to restore. Use the \`backup\` command to create a backup`,
);
process.exit(1);
}
try {
const selected_backup = await select({
message: "Select a backup:",
choices: backups.map((b, i) => {
const { backup_date } = grabBackupData({
backup_name: b,
});
return {
name: `Backup #${i + 1}: ${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`,
value: b,
};
}),
});
fs.cpSync(path.join(backup_dir, selected_backup), db_file_path);
console.log(
`${chalk.bold(chalk.green(`DB Restore Success!`))}`,
);
process.exit();
} catch (error: any) {
console.error(`Backup Restore ERROR => ${error.message}`);
process.exit();
}
});
}

55
src/commands/schema.ts Normal file
View File

@ -0,0 +1,55 @@
import { Command } from "commander";
import { SQLiteSchemaManager } from "../lib/sqlite/db-schema-manager";
import init from "../functions/init";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import _ from "lodash";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
export default function () {
return new Command("schema")
.description("Build DB From Schema")
.option(
"-v, --vector",
"Recreate Vector Tables. This will drop and rebuild all vector tables",
)
.option("-t, --typedef", "Generate typescript type definitions")
.action(async (opts) => {
console.log(`Starting process ...`);
const { config, dbSchema } = await init();
const { ROOT_DIR } = grabDirNames();
const isVector = Boolean(opts.vector || opts.v);
const isTypeDef = Boolean(opts.typedef || opts.t);
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
const manager = new SQLiteSchemaManager({
schema: finaldbSchema,
recreate_vector_table: isVector,
});
await manager.syncSchema();
manager.close();
if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(
ROOT_DIR,
config.typedef_file_path,
);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
});
}
console.log(
`${chalk.bold(chalk.green(`DB Schema setup success!`))}`,
);
process.exit();
});
}

38
src/commands/typedef.ts Normal file
View File

@ -0,0 +1,38 @@
import { Command } from "commander";
import init from "../functions/init";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
export default function () {
return new Command("typedef")
.description("Build DB From Schema")
.action(async (opts) => {
console.log(`Creating Type Definition From DB Schema ...`);
const { config, dbSchema } = await init();
const { ROOT_DIR } = grabDirNames();
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (config.typedef_file_path) {
const out_file = path.resolve(
ROOT_DIR,
config.typedef_file_path,
);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
});
} else {
console.error(``);
process.exit(1);
}
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
process.exit();
});
}

5
src/data/app-data.ts Normal file
View File

@ -0,0 +1,5 @@
export const AppData = {
ConfigFileName: "bun-sqlite.config.ts",
MaxBackups: 10,
DefaultBackupDirName: ".backups",
} as const;

View File

@ -0,0 +1,9 @@
import path from "path";
export default function grabDirNames() {
const ROOT_DIR = process.cwd();
return {
ROOT_DIR,
};
}

69
src/functions/init.ts Normal file
View File

@ -0,0 +1,69 @@
import path from "path";
import fs from "fs";
import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names";
import type {
BunSQLiteConfig,
BunSQLiteConfigReturn,
BUN_SQLITE_DatabaseSchemaType,
} from "../types";
export default async function init(): Promise<BunSQLiteConfigReturn> {
try {
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
if (!fs.existsSync(ConfigFilePath)) {
console.log("ConfigFilePath", ConfigFilePath);
console.error(
`Please create a \`${ConfigFileName}\` file at the root of your project.`,
);
process.exit(1);
}
const ConfigImport = await import(ConfigFilePath);
const Config = ConfigImport["default"] as BunSQLiteConfig;
if (!Config.db_name) {
console.error(`\`db_name\` is required in your config`);
process.exit(1);
}
if (!Config.db_schema_file_name) {
console.error(`\`db_schema_file_name\` is required in your config`);
process.exit(1);
}
let db_dir = ROOT_DIR;
if (Config.db_dir) {
db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (!fs.existsSync(Config.db_dir)) {
fs.mkdirSync(Config.db_dir, { recursive: true });
}
}
const DBSchemaFilePath = path.join(db_dir, Config.db_schema_file_name);
const DbSchemaImport = await import(DBSchemaFilePath);
const DbSchema = DbSchemaImport[
"default"
] as BUN_SQLITE_DatabaseSchemaType;
const backup_dir =
Config.db_backup_dir || AppData["DefaultBackupDirName"];
const BackupDir = path.resolve(db_dir, backup_dir);
if (!fs.existsSync(BackupDir)) {
fs.mkdirSync(BackupDir, { recursive: true });
}
return { config: Config, dbSchema: DbSchema };
} catch (error: any) {
console.error(`Initialization ERROR => ` + error.message);
process.exit(1);
}
}

15
src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import DbDelete from "./lib/sqlite/db-delete";
import DbInsert from "./lib/sqlite/db-insert";
import DbSelect from "./lib/sqlite/db-select";
import DbSQL from "./lib/sqlite/db-sql";
import DbUpdate from "./lib/sqlite/db-update";
const NodeSQLite = {
select: DbSelect,
insert: DbInsert,
update: DbUpdate,
delete: DbDelete,
sql: DbSQL,
} as const;
export default NodeSQLite;

View File

@ -0,0 +1,74 @@
import DbClient from ".";
import _ from "lodash";
import type { APIResponseObject, ServerQueryParam } from "../../types";
import sqlGenerator from "../../utils/sql-generator";
type Params<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
> = {
table: Table;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
};
export default async function DbDelete<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
>({
table,
query,
targetId,
}: Params<Schema, Table>): Promise<APIResponseObject> {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge<ServerQueryParam<any>, ServerQueryParam<any>>(
finalQuery,
{
query: {
id: {
value: String(targetId),
},
},
},
);
}
const sqlQueryObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
});
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (whereClause) {
let sql = `DELETE FROM ${table} ${whereClause}`;
const res = DbClient.run(sql, sqlQueryObj.values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sql,
values: sqlQueryObj.values,
},
};
} else {
return {
success: false,
msg: `No WHERE clause`,
};
}
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}

View File

@ -0,0 +1,112 @@
import type {
BUN_SQLITE_FieldSchemaType,
BUN_SQLITE_TableSchemaType,
} from "../../types";
type Param = {
paradigm: "JavaScript" | "TypeScript" | undefined;
table: BUN_SQLITE_TableSchemaType;
query?: any;
typeDefName?: string;
allValuesOptional?: boolean;
addExport?: boolean;
dbName?: string;
};
export default function generateTypeDefinition({
paradigm,
table,
query,
typeDefName,
allValuesOptional,
addExport,
dbName,
}: Param) {
let typeDefinition: string | null = ``;
let tdName: string | null = ``;
try {
tdName = typeDefName
? typeDefName
: dbName
? `BUN_SQLITE_${dbName}_${table.tableName}`.toUpperCase()
: `BUN_SQLITE_${query.single}_${query.single_table}`.toUpperCase();
const fields = table.fields;
function typeMap(schemaType: BUN_SQLITE_FieldSchemaType) {
if (schemaType.options && schemaType.options.length > 0) {
return schemaType.options
.map((opt) =>
schemaType.dataType?.match(/int/i) ||
typeof opt == "number"
? `${opt}`
: `"${opt}"`,
)
.join(" | ");
}
if (schemaType.dataType?.match(/int|double|decimal/i)) {
return "number";
}
if (schemaType.dataType?.match(/text|varchar|timestamp/i)) {
return "string";
}
if (schemaType.dataType?.match(/boolean/i)) {
return "0 | 1";
}
return "string";
}
const typesArrayTypeScript = [];
const typesArrayJavascript = [];
typesArrayTypeScript.push(
`${addExport ? "export " : ""}type ${tdName} = {`,
);
typesArrayJavascript.push(`/**\n * @typedef {object} ${tdName}`);
fields.forEach((field) => {
if (field.fieldDescription) {
typesArrayTypeScript.push(
` /** \n * ${field.fieldDescription}\n */`,
);
}
const nullValue = allValuesOptional
? "?"
: field.notNullValue
? ""
: "?";
typesArrayTypeScript.push(
` ${field.fieldName}${nullValue}: ${typeMap(field)};`,
);
typesArrayJavascript.push(
` * @property {${typeMap(field)}${nullValue}} ${
field.fieldName
}`,
);
});
typesArrayTypeScript.push(`}`);
typesArrayJavascript.push(` */`);
if (paradigm?.match(/javascript/i)) {
typeDefinition = typesArrayJavascript.join("\n");
}
if (paradigm?.match(/typescript/i)) {
typeDefinition = typesArrayTypeScript.join("\n");
}
} catch (error: any) {
console.log(error.message);
typeDefinition = null;
}
return { typeDefinition, tdName };
}

View File

@ -0,0 +1,47 @@
import DbClient from ".";
import type { APIResponseObject } from "../../types";
import sqlInsertGenerator from "../../utils/sql-insert-generator";
type Params<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
> = {
table: Table;
data: Schema[];
};
export default async function DbInsert<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
>({ table, data }: Params<Schema, Table>): Promise<APIResponseObject> {
try {
const finalData: { [k: string]: any }[] = data.map((d) => ({
...d,
created_at: Date.now(),
updated_at: Date.now(),
}));
const sqlObj = sqlInsertGenerator({
tableName: table,
data: finalData as any[],
});
const res = DbClient.run(sqlObj?.query || "", sqlObj?.values || []);
return {
success: Boolean(Number(res.lastInsertRowid)),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj,
},
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}

View File

@ -0,0 +1,634 @@
#!/usr/bin/env bun
import { Database } from "bun:sqlite";
import _ from "lodash";
import DbClient from ".";
import type {
BUN_SQLITE_DatabaseSchemaType,
BUN_SQLITE_FieldSchemaType,
BUN_SQLITE_TableSchemaType,
} from "../../types";
// Schema Manager Class
class SQLiteSchemaManager {
private db: Database;
private db_manager_table_name: string;
private recreate_vector_table: boolean;
private db_schema: BUN_SQLITE_DatabaseSchemaType;
constructor({
schema,
recreate_vector_table = false,
}: {
schema: BUN_SQLITE_DatabaseSchemaType;
recreate_vector_table?: boolean;
}) {
this.db = DbClient;
this.db_manager_table_name = "__db_schema_manager__";
this.db.run("PRAGMA foreign_keys = ON;");
this.recreate_vector_table = recreate_vector_table;
this.createDbManagerTable();
this.db_schema = schema;
}
private createDbManagerTable() {
this.db.run(`
CREATE TABLE IF NOT EXISTS ${this.db_manager_table_name} (
table_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
}
private insertDbManagerTable(tableName: string) {
this.db.run(
`INSERT INTO ${this.db_manager_table_name} (table_name,created_at,updated_at) VALUES (?, ?, ?)`,
[tableName, Date.now(), Date.now()],
);
}
private removeDbManagerTable(tableName: string) {
this.db.run(
`DELETE FROM ${this.db_manager_table_name} WHERE table_name = ?`,
[tableName],
);
}
/**
* Main synchronization method
*/
async syncSchema(): Promise<void> {
console.log("Starting schema synchronization...");
const existingTables = this.getExistingTables();
const schemaTables = this.db_schema.tables.map((t) => t.tableName);
// 2. Create or update tables
for (const table of this.db_schema.tables) {
await this.syncTable(table, existingTables);
}
// 1. Drop tables that no longer exist in schema
await this.dropRemovedTables(existingTables, schemaTables);
console.log("Schema synchronization complete!");
}
/**
* Get list of existing tables in the database
*/
private getExistingTables(): string[] {
let sql = `SELECT table_name FROM ${this.db_manager_table_name}`;
const query = this.db.query(sql);
const results = query.all() as { table_name: string }[];
return results.map((r) => r.table_name);
}
/**
* Drop tables that are no longer in the schema
*/
private async dropRemovedTables(
existingTables: string[],
schemaTables: string[],
): Promise<void> {
const tablesToDrop = existingTables.filter(
(t) =>
!schemaTables.includes(t) &&
!schemaTables.find((scT) => t.startsWith(scT + "_")),
);
for (const tableName of tablesToDrop) {
console.log(`Dropping table: ${tableName}`);
this.db.run(`DROP TABLE IF EXISTS "${tableName}"`);
this.db.run(
`DELETE FROM ${this.db_manager_table_name} WHERE table_name = "${tableName}"`,
);
}
}
/**
* Sync a single table (create or update)
*/
private async syncTable(
table: BUN_SQLITE_TableSchemaType,
existingTables: string[],
): Promise<void> {
let tableExists = existingTables.includes(table.tableName);
// Handle table rename
if (table.tableNameOld && table.tableNameOld !== table.tableName) {
if (existingTables.includes(table.tableNameOld)) {
console.log(
`Renaming table: ${table.tableNameOld} -> ${table.tableName}`,
);
this.db.run(
`ALTER TABLE "${table.tableNameOld}" RENAME TO "${table.tableName}"`,
);
this.insertDbManagerTable(table.tableName);
this.removeDbManagerTable(table.tableNameOld);
tableExists = true;
}
}
if (!tableExists) {
// Create new table
await this.createTable(table);
this.insertDbManagerTable(table.tableName);
} else {
// Update existing table
await this.updateTable(table);
}
// Sync indexes
await this.syncIndexes(table);
}
/**
* Create a new table
*/
private async createTable(
table: BUN_SQLITE_TableSchemaType,
): Promise<void> {
console.log(`Creating table: ${table.tableName}`);
let new_table = _.cloneDeep(table);
if (new_table.parentTableName) {
const parent_table = this.db_schema.tables.find(
(t) => t.tableName === new_table.parentTableName,
);
if (!parent_table) {
throw new Error(
`Parent table \`${new_table.parentTableName}\` not found for \`${new_table.tableName}\``,
);
}
new_table = _.merge(parent_table, {
tableName: new_table.tableName,
tableDescription: new_table.tableDescription,
});
}
const columns: string[] = [];
const foreignKeys: string[] = [];
for (const field of new_table.fields) {
const columnDef = this.buildColumnDefinition(field);
columns.push(columnDef);
if (field.foreignKey) {
foreignKeys.push(this.buildForeignKeyConstraint(field));
}
}
// Add unique constraints
if (new_table.uniqueConstraints) {
for (const constraint of new_table.uniqueConstraints) {
if (
constraint.constraintTableFields &&
constraint.constraintTableFields.length > 0
) {
const fields = constraint.constraintTableFields
.map((f) => `"${f.value}"`)
.join(", ");
const constraintName =
constraint.constraintName ||
`unique_${fields.replace(/"/g, "")}`;
columns.push(
`CONSTRAINT "${constraintName}" UNIQUE (${fields})`,
);
}
}
}
const allConstraints = [...columns, ...foreignKeys];
const sql = new_table.isVector
? `CREATE VIRTUAL TABLE "${new_table.tableName}" USING ${new_table.vectorType || "vec0"}(${allConstraints.join(", ")})`
: `CREATE TABLE "${new_table.tableName}" (${allConstraints.join(", ")})`;
this.db.run(sql);
}
/**
* Update an existing table
*/
private async updateTable(
table: BUN_SQLITE_TableSchemaType,
): Promise<void> {
console.log(`Updating table: ${table.tableName}`);
const existingColumns = this.getTableColumns(table.tableName);
const schemaColumns = table.fields.map((f) => f.fieldName || "");
// SQLite has limited ALTER TABLE support
// We need to use the recreation strategy for complex changes
const columnsToAdd = table.fields.filter(
(f) =>
f.fieldName &&
!existingColumns.find(
(c) =>
c.name == f.fieldName && c.type == this.mapDataType(f),
),
);
const columnsToRemove = existingColumns.filter(
(c) => !schemaColumns.includes(c.name),
);
const columnsToUpdate = table.fields.filter(
(f) =>
f.fieldName &&
f.updatedField &&
existingColumns.find(
(c) =>
c.name == f.fieldName && c.type == this.mapDataType(f),
),
);
// Simple case: only adding columns
if (columnsToRemove.length === 0 && columnsToUpdate.length === 0) {
for (const field of columnsToAdd) {
await this.addColumn(table.tableName, field);
}
} else {
// Complex case: need to recreate table
await this.recreateTable(table);
}
}
/**
* Get existing columns for a table
*/
private getTableColumns(
tableName: string,
): { name: string; type: string }[] {
const query = this.db.query(`PRAGMA table_info("${tableName}")`);
const results = query.all() as { name: string; type: string }[];
return results;
}
/**
* Add a new column to existing table
*/
private async addColumn(
tableName: string,
field: BUN_SQLITE_FieldSchemaType,
): Promise<void> {
console.log(`Adding column: ${tableName}.${field.fieldName}`);
const columnDef = this.buildColumnDefinition(field);
// Remove PRIMARY KEY and UNIQUE constraints for ALTER TABLE ADD COLUMN
const cleanDef = columnDef
.replace(/PRIMARY KEY/gi, "")
.replace(/AUTOINCREMENT/gi, "")
.replace(/UNIQUE/gi, "")
.trim();
const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${cleanDef}`;
this.db.run(sql);
}
/**
* Recreate table (for complex schema changes)
*/
private async recreateTable(
table: BUN_SQLITE_TableSchemaType,
): Promise<void> {
if (table.isVector) {
if (!this.recreate_vector_table) {
return;
}
console.log(`Recreating vector table: ${table.tableName}`);
const existingRows = this.db
.query(`SELECT * FROM "${table.tableName}"`)
.all() as { [k: string]: any }[];
this.db.run(`DROP TABLE "${table.tableName}"`);
await this.createTable(table);
if (existingRows.length > 0) {
for (let i = 0; i < existingRows.length; i++) {
const row = existingRows[i];
if (!row) continue;
const columns = Object.keys(row);
const placeholders = columns.map(() => "?").join(", ");
this.db.run(
`INSERT INTO "${table.tableName}" (${columns.join(", ")}) VALUES (${placeholders})`,
Object.values(row),
);
}
}
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
// Get existing data
const existingColumns = this.getTableColumns(table.tableName);
const columnsToKeep = table.fields
.filter(
(f) =>
f.fieldName &&
existingColumns.find(
(c) =>
c.name == f.fieldName &&
c.type == this.mapDataType(f),
),
)
.map((f) => f.fieldName);
// Create temp table with new schema
const tempTable = { ...table, tableName: tempTableName };
await this.createTable(tempTable);
// Copy data if there are common columns
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep.map((c) => `"${c}"`).join(", ");
this.db.run(
`INSERT INTO "${tempTableName}" (${columnList}) SELECT ${columnList} FROM "${table.tableName}"`,
);
}
// Drop old table
this.db.run(`DROP TABLE "${table.tableName}"`);
// Rename temp table
this.db.run(
`ALTER TABLE "${tempTableName}" RENAME TO "${table.tableName}"`,
);
}
/**
* Build column definition SQL
*/
private buildColumnDefinition(field: BUN_SQLITE_FieldSchemaType): string {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const fieldName = field.sideCar
? `+${field.fieldName}`
: `${field.fieldName}`;
const parts: string[] = [fieldName];
// Data type mapping
const dataType = this.mapDataType(field);
parts.push(dataType);
// Primary key
if (field.primaryKey) {
parts.push("PRIMARY KEY");
if (field.autoIncrement) {
parts.push("AUTOINCREMENT");
}
}
// Not null
if (field.notNullValue || field.primaryKey) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
// Unique
if (field.unique && !field.primaryKey) {
parts.push("UNIQUE");
}
// Default value
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(
// Escape single quotes by doubling them to prevent SQL injection and wrap in single quotes
`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`,
);
} else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
} else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
return parts.join(" ");
}
/**
* Map DSQL data types to SQLite types
*/
private mapDataType(field: BUN_SQLITE_FieldSchemaType): string {
const dataType = field.dataType?.toLowerCase() || "text";
const vectorSize = field.vectorSize || 1536;
// Vector Embeddings
if (field.isVector) {
return `FLOAT[${vectorSize}]`;
}
// Integer types
if (
dataType.includes("int") ||
dataType === "bigint" ||
dataType === "smallint" ||
dataType === "tinyint"
) {
return "INTEGER";
}
// Real/Float types
if (
dataType.includes("real") ||
dataType.includes("float") ||
dataType.includes("double") ||
dataType === "decimal" ||
dataType === "numeric"
) {
return "REAL";
}
// Blob types
if (dataType.includes("blob") || dataType.includes("binary")) {
return "BLOB";
}
// Boolean
if (dataType === "boolean" || dataType === "bool") {
return "INTEGER"; // SQLite uses INTEGER for boolean (0/1)
}
// Date/Time types
if (dataType.includes("date") || dataType.includes("time")) {
return "TEXT"; // SQLite stores dates as TEXT or INTEGER
}
// Default to TEXT for all text-based types
return "TEXT";
}
/**
* Build foreign key constraint
*/
private buildForeignKeyConstraint(
field: BUN_SQLITE_FieldSchemaType,
): string {
const fk = field.foreignKey!;
let constraint = `FOREIGN KEY ("${field.fieldName}") REFERENCES "${fk.destinationTableName}"("${fk.destinationTableColumnName}")`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}
/**
* Sync indexes for a table
*/
private async syncIndexes(
table: BUN_SQLITE_TableSchemaType,
): Promise<void> {
if (!table.indexes || table.indexes.length === 0) {
return;
}
// Get existing indexes
const query = this.db.query(
`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='${table.tableName}' AND name NOT LIKE 'sqlite_%'`,
);
const existingIndexes = (query.all() as { name: string }[]).map(
(r) => r.name,
);
// Drop indexes not in schema
for (const indexName of existingIndexes) {
const stillExists = table.indexes.some(
(idx) => idx.indexName === indexName,
);
if (!stillExists) {
console.log(`Dropping index: ${indexName}`);
this.db.run(`DROP INDEX IF EXISTS "${indexName}"`);
}
}
// Create new indexes
for (const index of table.indexes) {
if (
!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0
) {
continue;
}
if (!existingIndexes.includes(index.indexName)) {
console.log(`Creating index: ${index.indexName}`);
const fields = index.indexTableFields
.map((f) => `"${f.value}"`)
.join(", ");
const unique = index.indexType === "regular" ? "" : ""; // SQLite doesn't have FULLTEXT in CREATE INDEX
this.db.run(
`CREATE ${unique}INDEX "${index.indexName}" ON "${table.tableName}" (${fields})`,
);
}
}
}
/**
* Close database connection
*/
close(): void {
this.db.close();
}
}
// Example usage
async function main() {
const schema: BUN_SQLITE_DatabaseSchemaType = {
dbName: "example_db",
tables: [
{
tableName: "users",
tableDescription: "User accounts",
fields: [
{
fieldName: "id",
dataType: "INTEGER",
primaryKey: true,
autoIncrement: true,
},
{
fieldName: "username",
dataType: "TEXT",
notNullValue: true,
unique: true,
},
{
fieldName: "email",
dataType: "TEXT",
notNullValue: true,
},
{
fieldName: "created_at",
dataType: "TEXT",
defaultValueLiteral: "CURRENT_TIMESTAMP",
},
],
indexes: [
{
indexName: "idx_users_email",
indexType: "regular",
indexTableFields: [
{ value: "email", dataType: "TEXT" },
],
},
],
},
{
tableName: "posts",
fields: [
{
fieldName: "id",
dataType: "INTEGER",
primaryKey: true,
autoIncrement: true,
},
{
fieldName: "user_id",
dataType: "INTEGER",
notNullValue: true,
foreignKey: {
destinationTableName: "users",
destinationTableColumnName: "id",
cascadeDelete: true,
},
},
{
fieldName: "title",
dataType: "TEXT",
notNullValue: true,
},
{
fieldName: "content",
dataType: "TEXT",
},
],
},
],
};
}
export { SQLiteSchemaManager };

View File

@ -0,0 +1,64 @@
import _ from "lodash";
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
import generateTypeDefinition from "./db-generate-type-defs";
type Params = {
dbSchema?: BUN_SQLITE_DatabaseSchemaType;
};
export default function dbSchemaToType(params?: Params): string[] | undefined {
let datasquirelSchema = params?.dbSchema;
if (!datasquirelSchema) return;
let tableNames = `export const BunSQLiteTables = [\n${datasquirelSchema.tables
.map((tbl) => ` "${tbl.tableName}",`)
.join("\n")}\n] as const`;
const dbTablesSchemas = datasquirelSchema.tables;
const defDbName = datasquirelSchema.dbName
?.toUpperCase()
.replace(/ |\-/g, "_");
const defNames: string[] = [];
const schemas = dbTablesSchemas
.map((table) => {
let final_table = _.cloneDeep(table);
if (final_table.parentTableName) {
const parent_table = dbTablesSchemas.find(
(t) => t.tableName === final_table.parentTableName,
);
if (parent_table) {
final_table = _.merge(parent_table, {
tableName: final_table.tableName,
tableDescription: final_table.tableDescription,
});
}
}
const defObj = generateTypeDefinition({
paradigm: "TypeScript",
table: final_table,
typeDefName: `BUN_SQLITE_${defDbName}_${final_table.tableName.toUpperCase()}`,
allValuesOptional: true,
addExport: true,
});
if (defObj.tdName?.match(/./)) {
defNames.push(defObj.tdName);
}
return defObj.typeDefinition;
})
.filter((schm) => typeof schm == "string");
const allTd = defNames?.[0]
? `export type BUN_SQLITE_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}`
: ``;
return [tableNames, ...schemas, allTd];
}

View File

@ -0,0 +1,78 @@
import mysql from "mysql";
import DbClient from ".";
import _ from "lodash";
import type { APIResponseObject, ServerQueryParam } from "../../types";
import sqlGenerator from "../../utils/sql-generator";
type Params<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
> = {
query?: ServerQueryParam<Schema>;
table: Table;
count?: boolean;
targetId?: number | string;
};
export default async function DbSelect<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
>({
table,
query,
count,
targetId,
}: Params<Schema, Table>): Promise<APIResponseObject<Schema>> {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge<ServerQueryParam<any>, ServerQueryParam<any>>(
finalQuery,
{
query: {
id: {
value: String(targetId),
},
},
},
);
}
const sqlObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
count,
});
const sql = mysql.format(sqlObj.string, sqlObj.values);
const res = DbClient.query<Schema, Schema[]>(sql);
const batchRes = res.all();
let resp: APIResponseObject<Schema> = {
success: Boolean(batchRes[0]),
payload: batchRes,
singleRes: batchRes[0],
debug: {
sqlObj,
sql,
},
};
if (count) {
const count_val = count ? batchRes[0]?.["COUNT(*)"] : undefined;
resp["count"] = Number(count_val);
delete resp.payload;
delete resp.singleRes;
}
return resp;
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}

42
src/lib/sqlite/db-sql.ts Normal file
View File

@ -0,0 +1,42 @@
import DbClient from ".";
import _ from "lodash";
import type { APIResponseObject } from "../../types";
type Params = {
sql: string;
values?: (string | number)[];
};
export default async function DbSQL<
T extends { [k: string]: any } = { [k: string]: any },
>({ sql, values }: Params): Promise<APIResponseObject<T>> {
try {
const res = sql.match(/^select/i)
? DbClient.query(sql).all(...(values || []))
: DbClient.run(sql, values || []);
return {
success: true,
payload: Array.isArray(res) ? (res as T[]) : undefined,
singleRes: Array.isArray(res) ? (res as T[])?.[0] : undefined,
postInsertReturn: Array.isArray(res)
? undefined
: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sqlObj: {
sql,
values,
},
sql,
},
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}

104
src/lib/sqlite/db-update.ts Normal file
View File

@ -0,0 +1,104 @@
import DbClient from ".";
import _ from "lodash";
import type { APIResponseObject, ServerQueryParam } from "../../types";
import sqlGenerator from "../../utils/sql-generator";
type Params<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
> = {
table: Table;
data: Schema;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
};
export default async function DbUpdate<
Schema extends { [k: string]: any } = { [k: string]: any },
Table extends string = string,
>({
table,
data,
query,
targetId,
}: Params<Schema, Table>): Promise<APIResponseObject> {
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge<ServerQueryParam<any>, ServerQueryParam<any>>(
finalQuery,
{
query: {
id: {
value: String(targetId),
},
},
},
);
}
const sqlQueryObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
});
let values: (string | number)[] = [];
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (whereClause) {
let sql = `UPDATE ${table} SET`;
const finalData: { [k: string]: any } = {
...data,
updated_at: Date.now(),
};
const keys = Object.keys(finalData);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key) continue;
const isLast = i == keys.length - 1;
sql += ` ${key}=?`;
values.push(
String(finalData[key as keyof { [k: string]: any }]),
);
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
const res = DbClient.run(sql, values);
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
debug: {
sql,
values,
},
};
} else {
return {
success: false,
msg: `No WHERE clause`,
};
}
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}

22
src/lib/sqlite/index.ts Normal file
View File

@ -0,0 +1,22 @@
import Database from "better-sqlite3";
import * as sqliteVec from "sqlite-vec";
import grabDirNames from "../../data/grab-dir-names";
import init from "../../functions/init";
import grabDBDir from "../../utils/grab-db-dir";
const { ROOT_DIR } = grabDirNames();
const { config } = await init();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
}
const { db_file_path } = grabDBDir({ config });
const DbClient = new Database(db_file_path, { fileMustExist: false });
sqliteVec.load(DbClient);
export default DbClient;

View File

@ -0,0 +1,27 @@
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
import dbSchemaToType from "./db-schema-to-typedef";
type Params = {
dbSchema: BUN_SQLITE_DatabaseSchemaType;
dst_file: string;
};
export default function dbSchemaToTypeDef({ dbSchema, dst_file }: Params) {
try {
if (!dbSchema) throw new Error("No schema found");
const definitions = dbSchemaToType({ dbSchema });
const ourfileDir = path.dirname(dst_file);
if (!existsSync(ourfileDir)) {
mkdirSync(ourfileDir, { recursive: true });
}
writeFileSync(dst_file, definitions?.join("\n\n") || "", "utf-8");
} catch (error: any) {
console.log(`Schema to Typedef Error =>`, error.message);
}
}

7
src/lib/sqlite/schema.ts Normal file
View File

@ -0,0 +1,7 @@
import _ from "lodash";
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
export const DbSchema: BUN_SQLITE_DatabaseSchemaType = {
dbName: "travis-ai",
tables: [],
};

1193
src/types/index.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
import _ from "lodash";
import { DefaultFields, type BUN_SQLITE_DatabaseSchemaType } from "../types";
type Params = {
dbSchema: BUN_SQLITE_DatabaseSchemaType;
};
export default function ({ dbSchema }: Params): BUN_SQLITE_DatabaseSchemaType {
const finaldbSchema = _.cloneDeep(dbSchema);
finaldbSchema.tables = finaldbSchema.tables.map((t) => {
const newTable = _.cloneDeep(t);
newTable.fields = newTable.fields.filter(
(f) => !f.fieldName?.match(/^(id|created_at|updated_at)$/),
);
newTable.fields.unshift(...DefaultFields);
return newTable;
});
return finaldbSchema;
}

View File

@ -0,0 +1,13 @@
type Params = {
backup_name: string;
};
export default function grabBackupData({ backup_name }: Params) {
const backup_parts = backup_name.split("-");
const backup_date_timestamp = Number(backup_parts.pop());
const origin_backup_name = backup_parts.join("-");
const backup_date = new Date(backup_date_timestamp);
return { backup_date, backup_date_timestamp, origin_backup_name };
}

View File

@ -0,0 +1,11 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabDBBackupFileName({ config }: Params) {
const new_db_file_name = `${config.db_name}-${Date.now()}`;
return new_db_file_name;
}

26
src/utils/grab-db-dir.ts Normal file
View File

@ -0,0 +1,26 @@
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import type { BunSQLiteConfig } from "../types";
import { AppData } from "../data/app-data";
type Params = {
config: BunSQLiteConfig;
};
export default function grabDBDir({ config }: Params) {
const { ROOT_DIR } = grabDirNames();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
}
const backup_dir_name =
config.db_backup_dir || AppData["DefaultBackupDirName"];
const backup_dir = path.resolve(db_dir, backup_dir_name);
const db_file_path = path.resolve(db_dir, config.db_name);
return { db_dir, backup_dir, db_file_path };
}

View File

@ -0,0 +1,29 @@
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabSortedBackups({ config }: Params) {
const { backup_dir } = grabDBDir({ config });
const backups = fs.readdirSync(backup_dir);
/**
* Order Backups. Most recent first.
*/
const ordered_backups = backups.sort((a, b) => {
const a_date = Number(a.split("-").pop());
const b_date = Number(b.split("-").pop());
if (a_date > b_date) {
return -1;
}
return 1;
});
return ordered_backups;
}

View File

@ -0,0 +1,42 @@
import { ServerQueryEqualities } from "../types";
export default function sqlEqualityParser(
eq: (typeof ServerQueryEqualities)[number]
): string {
switch (eq) {
case "EQUAL":
return "=";
case "LIKE":
return "LIKE";
case "NOT LIKE":
return "NOT LIKE";
case "NOT EQUAL":
return "<>";
case "IN":
return "IN";
case "NOT IN":
return "NOT IN";
case "BETWEEN":
return "BETWEEN";
case "NOT BETWEEN":
return "NOT BETWEEN";
case "IS NULL":
return "IS NULL";
case "IS NOT NULL":
return "IS NOT NULL";
case "EXISTS":
return "EXISTS";
case "NOT EXISTS":
return "NOT EXISTS";
case "GREATER THAN":
return ">";
case "GREATER THAN OR EQUAL":
return ">=";
case "LESS THAN":
return "<";
case "LESS THAN OR EQUAL":
return "<=";
default:
return "=";
}
}

View File

@ -0,0 +1,140 @@
import type { ServerQueryEqualities, ServerQueryObject } from "../types";
import sqlEqualityParser from "./sql-equality-parser";
type Params = {
fieldName: string;
value?: string;
equality?: (typeof ServerQueryEqualities)[number];
queryObj: ServerQueryObject<
{
[key: string]: any;
},
string
>;
isValueFieldValue?: boolean;
};
type Return = {
str?: string;
param?: string;
};
/**
* # SQL Gen Operator Gen
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenOperatorGen({
fieldName,
value,
equality,
queryObj,
isValueFieldValue,
}: Params): Return {
if (queryObj.nullValue) {
return { str: `${fieldName} IS NULL` };
}
if (queryObj.notNullValue) {
return { str: `${fieldName} IS NOT NULL` };
}
if (value) {
const finalValue = isValueFieldValue ? value : "?";
const finalParams = isValueFieldValue ? undefined : value;
if (equality == "MATCH") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN NATURAL LANGUAGE MODE)`,
param: finalParams,
};
} else if (equality == "MATCH_BOOLEAN") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
} else if (equality == "LIKE_LOWER") {
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
};
} else if (equality == "LIKE_LOWER_RAW") {
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: finalParams,
};
} else if (equality == "LIKE") {
return {
str: `${fieldName} LIKE ${finalValue}`,
param: `%${finalParams}%`,
};
} else if (equality == "LIKE_RAW") {
return {
str: `${fieldName} LIKE ${finalValue}`,
param: finalParams,
};
} else if (equality == "NOT_LIKE_LOWER") {
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
};
} else if (equality == "NOT_LIKE_LOWER_RAW") {
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: finalParams,
};
} else if (equality == "NOT LIKE") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
} else if (equality == "NOT LIKE_RAW") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
} else if (equality == "REGEXP") {
return {
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
param: finalParams,
};
} else if (equality == "FULLTEXT") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
} else if (equality == "NOT EQUAL") {
return {
str: `${fieldName} != ${finalValue}`,
param: finalParams,
};
} else if (equality) {
return {
str: `${fieldName} ${sqlEqualityParser(
equality,
)} ${finalValue}`,
param: finalParams,
};
} else {
return {
str: `${fieldName} = ${finalValue}`,
param: finalParams,
};
}
} else {
if (equality == "IS NULL") {
return { str: `${fieldName} IS NULL` };
} else if (equality == "IS NOT NULL") {
return { str: `${fieldName} IS NOT NULL` };
} else if (equality) {
return {
str: `${fieldName} ${sqlEqualityParser(equality)} ?`,
param: value,
};
} else {
return {
str: `${fieldName} = ?`,
param: value,
};
}
}
}

521
src/utils/sql-generator.ts Normal file
View File

@ -0,0 +1,521 @@
import { isUndefined } from "lodash";
import sqlGenOperatorGen from "./sql-gen-operator-gen";
import type {
ServerQueryParam,
ServerQueryParamOrder,
ServerQueryParamsJoin,
ServerQueryParamsJoinMatchObject,
ServerQueryQueryObject,
ServerQueryValuesObject,
} from "../types";
type Param<T extends { [key: string]: any } = { [key: string]: any }> = {
genObject?: ServerQueryParam<T>;
tableName: string;
dbFullName?: string;
count?: boolean;
};
type Return = {
string: string;
values: (string | number)[];
};
/**
* # SQL Query Generator
* @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
*/
export default function sqlGenerator<
T extends { [key: string]: any } = { [key: string]: any },
>({ tableName, genObject, dbFullName, count }: Param<T>): Return {
const finalQuery = genObject?.query ? genObject.query : undefined;
const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined;
const sqlSearhValues: string[] = [];
const finalDbName = dbFullName ? `${dbFullName}.` : "";
/**
* # Generate Query
*/
function genSqlSrchStr({
queryObj,
join,
field,
}: {
queryObj: ServerQueryQueryObject[string];
join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[];
field?: string;
}) {
const finalFieldName = (() => {
if (queryObj?.tableName) {
return `${finalDbName}${queryObj.tableName}.${field}`;
}
if (join) {
return `${finalDbName}${tableName}.${field}`;
}
return field;
})();
let str = `${finalFieldName}=?`;
function grabValue(val?: string | ServerQueryValuesObject | null) {
const valueParsed = val;
if (!valueParsed) return;
const valueString =
typeof valueParsed == "string"
? valueParsed
: valueParsed
? valueParsed.fieldName && valueParsed.tableName
? `${valueParsed.tableName}.${valueParsed.fieldName}`
: valueParsed.value?.toString()
: undefined;
const valueEquality =
typeof valueParsed == "object"
? valueParsed.equality || queryObj.equality
: queryObj.equality;
const operatorStrParam = sqlGenOperatorGen({
queryObj,
equality: valueEquality,
fieldName: finalFieldName || "",
value: valueString?.toString() || "",
isValueFieldValue: Boolean(
typeof valueParsed == "object" &&
valueParsed.fieldName &&
valueParsed.tableName,
),
});
return operatorStrParam;
}
if (Array.isArray(queryObj.value)) {
const strArray: string[] = [];
queryObj.value.forEach((val) => {
const operatorStrParam = grabValue(val);
if (!operatorStrParam) return;
if (operatorStrParam.str && operatorStrParam.param) {
strArray.push(operatorStrParam.str);
sqlSearhValues.push(operatorStrParam.param);
} else if (operatorStrParam.str) {
strArray.push(operatorStrParam.str);
}
});
str = "(" + strArray.join(` ${queryObj.operator || "AND"} `) + ")";
} else if (typeof queryObj.value == "object") {
const operatorStrParam = grabValue(queryObj.value);
if (operatorStrParam?.str) {
str = operatorStrParam.str;
if (operatorStrParam.param) {
sqlSearhValues.push(operatorStrParam.param);
}
}
} else {
const valueParsed = queryObj.value
? String(queryObj.value)
: undefined;
const operatorStrParam = sqlGenOperatorGen({
equality: queryObj.equality,
fieldName: finalFieldName || "",
value: valueParsed,
queryObj,
});
if (operatorStrParam.str && operatorStrParam.param) {
str = operatorStrParam.str;
sqlSearhValues.push(operatorStrParam.param);
} else if (operatorStrParam.str) {
str = operatorStrParam.str;
}
}
return str;
}
function generateJoinStr(
mtch: ServerQueryParamsJoinMatchObject,
join: ServerQueryParamsJoin,
) {
if (mtch.__batch) {
let btch_mtch = ``;
btch_mtch += `(`;
for (let i = 0; i < mtch.__batch.matches.length; i++) {
const __mtch = mtch.__batch.matches[
i
] as ServerQueryParamsJoinMatchObject;
btch_mtch += `${generateJoinStr(__mtch, join)}`;
if (i < mtch.__batch.matches.length - 1) {
btch_mtch += ` ${mtch.__batch.operator || "OR"} `;
}
}
btch_mtch += `)`;
return btch_mtch;
}
return `${finalDbName}${
typeof mtch.source == "object" ? mtch.source.tableName : tableName
}.${
typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source
}=${(() => {
if (mtch.targetLiteral) {
if (typeof mtch.targetLiteral == "number") {
return `${mtch.targetLiteral}`;
}
return `'${mtch.targetLiteral}'`;
}
if (join.alias) {
return `${finalDbName}${
typeof mtch.target == "object"
? mtch.target.tableName
: join.alias
}.${
typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target
}`;
}
return `${finalDbName}${
typeof mtch.target == "object"
? mtch.target.tableName
: join.tableName
}.${
typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target
}`;
})()}`;
}
let fullTextMatchStr = genObject?.fullTextSearch
? ` MATCH(${genObject.fullTextSearch.fields
.map((f) =>
genObject.join ? `${tableName}.${String(f)}` : `${String(f)}`,
)
.join(",")}) AGAINST (? IN BOOLEAN MODE)`
: undefined;
const fullTextSearchStr = genObject?.fullTextSearch
? genObject.fullTextSearch.searchTerm
.split(` `)
.map((t) => `${t}`)
.join(" ")
: undefined;
let queryString = (() => {
let str = "SELECT";
if (count) {
str += ` COUNT(*)`;
} else if (genObject?.selectFields?.[0]) {
if (genObject.join) {
str += ` ${genObject.selectFields
?.map((fld) =>
typeof fld == "object"
? `${finalDbName}${tableName}.${fld.fieldName.toString()}` +
(fld.alias ? ` as ${fld.alias}` : ``)
: `${finalDbName}${tableName}.${String(fld)}`,
)
.join(",")}`;
} else {
str += ` ${genObject.selectFields
?.map((fld) =>
typeof fld == "object"
? `${fld.fieldName.toString()}` +
(fld.alias ? ` as ${fld.alias}` : ``)
: fld,
)
.join(",")}`;
}
} else {
if (genObject?.join) {
str += ` ${finalDbName}${tableName}.*`;
} else {
str += " *";
}
}
if (genObject?.countSubQueries) {
let countSqls: string[] = [];
for (let i = 0; i < genObject.countSubQueries.length; i++) {
const countSubQuery = genObject.countSubQueries[i];
if (!countSubQuery) continue;
const tableAlias = countSubQuery.table_alias;
let subQStr = `(SELECT COUNT(*)`;
subQStr += ` FROM ${countSubQuery.table}${
tableAlias ? ` ${tableAlias}` : ""
}`;
subQStr += ` WHERE (`;
for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) {
const csqSrc = countSubQuery.srcTrgMap[j];
if (!csqSrc) continue;
subQStr += ` ${tableAlias || countSubQuery.table}.${
csqSrc.src
}`;
if (typeof csqSrc.trg == "string") {
subQStr += ` = ?`;
sqlSearhValues.push(csqSrc.trg);
} else if (typeof csqSrc.trg == "object") {
subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`;
}
if (j < countSubQuery.srcTrgMap.length - 1) {
subQStr += ` AND `;
}
}
subQStr += ` )) AS ${countSubQuery.alias}`;
countSqls.push(subQStr);
}
str += `, ${countSqls.join(",")}`;
}
if (genObject?.join && !count) {
const existingJoinTableNames: string[] = [tableName];
str +=
"," +
genObject.join
.flat()
.filter((j) => !isUndefined(j))
.map((joinObj) => {
const joinTableName = joinObj.alias
? joinObj.alias
: joinObj.tableName;
if (existingJoinTableNames.includes(joinTableName))
return null;
existingJoinTableNames.push(joinTableName);
if (joinObj.selectFields) {
return joinObj.selectFields
.map((selectField) => {
if (typeof selectField == "string") {
return `${finalDbName}${joinTableName}.${selectField}`;
} else if (typeof selectField == "object") {
let aliasSelectField = selectField.count
? `COUNT(${finalDbName}${joinTableName}.${selectField.field})`
: `${finalDbName}${joinTableName}.${selectField.field}`;
if (selectField.alias)
aliasSelectField += ` AS ${selectField.alias}`;
return aliasSelectField;
}
})
.join(",");
} else {
return `${finalDbName}${joinTableName}.*`;
}
})
.filter((_) => Boolean(_))
.join(",");
}
if (
genObject?.fullTextSearch &&
fullTextMatchStr &&
fullTextSearchStr
) {
str += `, ${fullTextMatchStr} AS ${genObject.fullTextSearch.scoreAlias}`;
sqlSearhValues.push(fullTextSearchStr);
}
str += ` FROM ${finalDbName}${tableName}`;
if (genObject?.join) {
str +=
" " +
genObject.join
.flat()
.filter((j) => !isUndefined(j))
.map((join) => {
return (
join.joinType +
" " +
(join.alias
? `${finalDbName}${join.tableName}` +
" " +
join.alias
: `${finalDbName}${join.tableName}`) +
" ON " +
(() => {
if (Array.isArray(join.match)) {
return (
"(" +
join.match
.map((mtch) =>
generateJoinStr(mtch, join),
)
.join(
join.operator
? ` ${join.operator} `
: " AND ",
) +
")"
);
} else if (typeof join.match == "object") {
return generateJoinStr(join.match, join);
}
})()
);
})
.join(" ");
}
return str;
})();
const sqlSearhString = queryKeys?.map((field) => {
const queryObj = finalQuery?.[field];
if (!queryObj) return;
if (queryObj.__query) {
const subQueryGroup = queryObj.__query;
const subSearchKeys = Object.keys(subQueryGroup);
const subSearchString = subSearchKeys.map((_field) => {
const newSubQueryObj = subQueryGroup?.[_field];
if (newSubQueryObj) {
return genSqlSrchStr({
queryObj: newSubQueryObj,
field: newSubQueryObj.fieldName || _field,
join: genObject?.join,
});
}
});
return (
"(" +
subSearchString.join(` ${queryObj.operator || "AND"} `) +
")"
);
}
return genSqlSrchStr({
queryObj,
field: queryObj.fieldName || field,
join: genObject?.join,
});
});
const cleanedUpSearchStr = sqlSearhString?.filter(
(str) => typeof str == "string",
);
const isSearchStr =
cleanedUpSearchStr?.[0] && cleanedUpSearchStr.find((str) => str);
if (isSearchStr) {
const stringOperator = genObject?.searchOperator || "AND";
queryString += ` WHERE ${cleanedUpSearchStr.join(
` ${stringOperator} `,
)}`;
}
if (genObject?.fullTextSearch && fullTextSearchStr && fullTextMatchStr) {
queryString += `${isSearchStr ? " AND" : " WHERE"} ${fullTextMatchStr}`;
sqlSearhValues.push(fullTextSearchStr);
}
if (genObject?.group) {
let group_by_txt = ``;
if (typeof genObject.group == "string") {
group_by_txt = genObject.group;
} else if (Array.isArray(genObject.group)) {
for (let i = 0; i < genObject.group.length; i++) {
const group = genObject.group[i];
if (typeof group == "string") {
group_by_txt += `\`${group.toString()}\``;
} else if (typeof group == "object" && group.table) {
group_by_txt += `${group.table}.${String(group.field)}`;
} else if (typeof group == "object") {
group_by_txt += `${String(group.field)}`;
}
if (i < genObject.group.length - 1) {
group_by_txt += ",";
}
}
} else if (typeof genObject.group == "object") {
if (genObject.group.table) {
group_by_txt = `${genObject.group.table}.${String(genObject.group.field)}`;
} else {
group_by_txt = `${String(genObject.group.field)}`;
}
}
queryString += ` GROUP BY ${group_by_txt}`;
}
function grabOrderString(order: ServerQueryParamOrder<T>) {
let orderFields = [];
let orderSrt = ``;
if (genObject?.fullTextSearch && genObject.fullTextSearch.scoreAlias) {
orderFields.push(genObject.fullTextSearch.scoreAlias);
} else if (genObject?.join) {
orderFields.push(
`${finalDbName}${tableName}.${String(order.field)}`,
);
} else {
orderFields.push(order.field);
}
orderSrt += ` ${orderFields.join(", ")} ${order.strategy}`;
return orderSrt;
}
if (genObject?.order && !count) {
let orderSrt = ` ORDER BY`;
if (Array.isArray(genObject.order)) {
for (let i = 0; i < genObject.order.length; i++) {
const order = genObject.order[i];
if (order) {
orderSrt +=
grabOrderString(order) +
(i < genObject.order.length - 1 ? `,` : "");
}
}
} else {
orderSrt += grabOrderString(genObject.order);
}
queryString += ` ${orderSrt}`;
}
if (genObject?.limit && !count) queryString += ` LIMIT ${genObject.limit}`;
if (genObject?.offset && !count)
queryString += ` OFFSET ${genObject.offset}`;
return {
string: queryString,
values: sqlSearhValues,
};
}

View File

@ -0,0 +1,76 @@
import type { SQLInsertGenParams, SQLInsertGenReturn } from "../types";
/**
* # SQL Insert Generator
*/
export default function sqlInsertGenerator({
tableName,
data,
dbFullName,
}: SQLInsertGenParams): SQLInsertGenReturn | undefined {
const finalDbName = dbFullName ? `${dbFullName}.` : "";
try {
if (Array.isArray(data) && data?.[0]) {
let insertKeys: string[] = [];
data.forEach((dt) => {
const kys = Object.keys(dt);
kys.forEach((ky) => {
if (!insertKeys.includes(ky)) {
insertKeys.push(ky);
}
});
});
let queryBatches: string[] = [];
let queryValues: (string | number)[] = [];
data.forEach((item) => {
queryBatches.push(
`(${insertKeys
.map((ky) => {
const value = item[ky];
const finalValue =
typeof value == "string" ||
typeof value == "number"
? value
: value
? String(value().value)
: null;
if (!finalValue) {
queryValues.push("");
return "?";
}
queryValues.push(finalValue);
const placeholder =
typeof value == "function"
? value().placeholder
: "?";
return placeholder;
})
.filter((k) => Boolean(k))
.join(",")})`,
);
});
let query = `INSERT INTO ${finalDbName}${tableName} (${insertKeys.join(
",",
)}) VALUES ${queryBatches.join(",")}`;
return {
query: query,
values: queryValues,
};
} else {
return undefined;
}
} catch (/** @type {any} */ error: any) {
console.log(`SQL insert gen ERROR: ${error.message}`);
return undefined;
}
}

27
src/utils/trim-backups.ts Normal file
View File

@ -0,0 +1,27 @@
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import type { BunSQLiteConfig } from "../types";
import grabSortedBackups from "./grab-sorted-backups";
import { AppData } from "../data/app-data";
import path from "path";
type Params = {
config: BunSQLiteConfig;
};
export default function trimBackups({ config }: Params) {
const { backup_dir } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
const max_backups = config.max_backups || AppData["MaxBackups"];
for (let i = 0; i < backups.length; i++) {
const backup_name = backups[i];
if (!backup_name) continue;
if (i > max_backups - 1) {
const backup_file_to_unlink = path.join(backup_dir, backup_name);
fs.unlinkSync(backup_file_to_unlink);
}
}
}

Some files were not shown because too many files have changed in this diff Show More