From df53cdb4e57a5fe04b8dcc960919800c531a763c Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 8 Mar 2026 06:23:30 +0100 Subject: [PATCH] First Commit --- .gitignore | 35 + .npmrc | 2 + CLAUDE.md | 106 ++ README.md | 789 +++++++++++ dist/commands/backup.d.ts | 2 + dist/commands/backup.js | 22 + dist/commands/index.d.ts | 6 + dist/commands/index.js | 31 + dist/commands/restore.d.ts | 2 + dist/commands/restore.js | 44 + dist/commands/schema.d.ts | 2 + dist/commands/schema.js | 38 + dist/commands/typedef.d.ts | 2 + dist/commands/typedef.js | 30 + dist/data/app-data.d.ts | 5 + dist/data/app-data.js | 5 + dist/data/grab-dir-names.d.ts | 3 + dist/data/grab-dir-names.js | 7 + dist/functions/init.d.ts | 2 + dist/functions/init.js | 46 + dist/index.d.ts | 13 + dist/index.js | 13 + dist/lib/sqlite/db-delete.d.ts | 16 + dist/lib/sqlite/db-delete.js | 49 + dist/lib/sqlite/db-generate-type-defs.d.ts | 15 + dist/lib/sqlite/db-generate-type-defs.js | 61 + dist/lib/sqlite/db-insert.d.ts | 15 + dist/lib/sqlite/db-insert.js | 32 + dist/lib/sqlite/db-schema-manager.d.ts | 72 + dist/lib/sqlite/db-schema-manager.js | 456 +++++++ dist/lib/sqlite/db-schema-to-typedef.d.ts | 6 + dist/lib/sqlite/db-schema-to-typedef.js | 44 + dist/lib/sqlite/db-select.d.ts | 17 + dist/lib/sqlite/db-select.js | 48 + dist/lib/sqlite/db-sql.d.ts | 11 + dist/lib/sqlite/db-sql.js | 33 + dist/lib/sqlite/db-update.d.ts | 17 + dist/lib/sqlite/db-update.js | 68 + dist/lib/sqlite/index.d.ts | 3 + dist/lib/sqlite/index.js | 17 + dist/lib/sqlite/schema-to-typedef.d.ts | 7 + dist/lib/sqlite/schema-to-typedef.js | 18 + dist/lib/sqlite/schema.d.ts | 2 + dist/lib/sqlite/schema.js | 5 + dist/types/index.d.ts | 1015 ++++++++++++++ dist/types/index.js | 123 ++ .../append-default-fields-to-db-schema.d.ts | 6 + .../append-default-fields-to-db-schema.js | 12 + dist/utils/grab-backup-data.d.ts | 9 + dist/utils/grab-backup-data.js | 7 + dist/utils/grab-db-backup-file-name.d.ts | 6 + dist/utils/grab-db-backup-file-name.js | 4 + dist/utils/grab-db-dir.d.ts | 10 + dist/utils/grab-db-dir.js | 14 + dist/utils/grab-sorted-backups.d.ts | 6 + dist/utils/grab-sorted-backups.js | 18 + dist/utils/sql-equality-parser.d.ts | 2 + dist/utils/sql-equality-parser.js | 39 + dist/utils/sql-gen-operator-gen.d.ts | 20 + dist/utils/sql-gen-operator-gen.js | 127 ++ dist/utils/sql-generator.d.ts | 25 + dist/utils/sql-generator.js | 392 ++++++ dist/utils/sql-insert-generator.d.ts | 5 + dist/utils/sql-insert-generator.js | 56 + dist/utils/trim-backups.d.ts | 6 + dist/utils/trim-backups.js | 19 + package-lock.json | 1196 +++++++++++++++++ package.json | 43 + publish.sh | 14 + src/commands/backup.ts | 29 + src/commands/index.ts | 44 + src/commands/restore.ts | 56 + src/commands/schema.ts | 55 + src/commands/typedef.ts | 38 + src/data/app-data.ts | 5 + src/data/grab-dir-names.ts | 9 + src/functions/init.ts | 69 + src/index.ts | 15 + src/lib/sqlite/db-delete.ts | 74 + src/lib/sqlite/db-generate-type-defs.ts | 112 ++ src/lib/sqlite/db-insert.ts | 47 + src/lib/sqlite/db-schema-manager.ts | 634 +++++++++ src/lib/sqlite/db-schema-to-typedef.ts | 64 + src/lib/sqlite/db-select.ts | 78 ++ src/lib/sqlite/db-sql.ts | 42 + src/lib/sqlite/db-update.ts | 104 ++ src/lib/sqlite/index.ts | 22 + src/lib/sqlite/schema-to-typedef.ts | 27 + src/lib/sqlite/schema.ts | 7 + src/types/index.ts | 1193 ++++++++++++++++ .../append-default-fields-to-db-schema.ts | 20 + src/utils/grab-backup-data.ts | 13 + src/utils/grab-db-backup-file-name.ts | 11 + src/utils/grab-db-dir.ts | 26 + src/utils/grab-sorted-backups.ts | 29 + src/utils/sql-equality-parser.ts | 42 + src/utils/sql-gen-operator-gen.ts | 140 ++ src/utils/sql-generator.ts | 521 +++++++ src/utils/sql-insert-generator.ts | 76 ++ src/utils/trim-backups.ts | 27 + tsconfig.json | 28 + 101 files changed, 9048 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 dist/commands/backup.d.ts create mode 100644 dist/commands/backup.js create mode 100644 dist/commands/index.d.ts create mode 100644 dist/commands/index.js create mode 100644 dist/commands/restore.d.ts create mode 100644 dist/commands/restore.js create mode 100644 dist/commands/schema.d.ts create mode 100644 dist/commands/schema.js create mode 100644 dist/commands/typedef.d.ts create mode 100644 dist/commands/typedef.js create mode 100644 dist/data/app-data.d.ts create mode 100644 dist/data/app-data.js create mode 100644 dist/data/grab-dir-names.d.ts create mode 100644 dist/data/grab-dir-names.js create mode 100644 dist/functions/init.d.ts create mode 100644 dist/functions/init.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/lib/sqlite/db-delete.d.ts create mode 100644 dist/lib/sqlite/db-delete.js create mode 100644 dist/lib/sqlite/db-generate-type-defs.d.ts create mode 100644 dist/lib/sqlite/db-generate-type-defs.js create mode 100644 dist/lib/sqlite/db-insert.d.ts create mode 100644 dist/lib/sqlite/db-insert.js create mode 100644 dist/lib/sqlite/db-schema-manager.d.ts create mode 100644 dist/lib/sqlite/db-schema-manager.js create mode 100644 dist/lib/sqlite/db-schema-to-typedef.d.ts create mode 100644 dist/lib/sqlite/db-schema-to-typedef.js create mode 100644 dist/lib/sqlite/db-select.d.ts create mode 100644 dist/lib/sqlite/db-select.js create mode 100644 dist/lib/sqlite/db-sql.d.ts create mode 100644 dist/lib/sqlite/db-sql.js create mode 100644 dist/lib/sqlite/db-update.d.ts create mode 100644 dist/lib/sqlite/db-update.js create mode 100644 dist/lib/sqlite/index.d.ts create mode 100644 dist/lib/sqlite/index.js create mode 100644 dist/lib/sqlite/schema-to-typedef.d.ts create mode 100644 dist/lib/sqlite/schema-to-typedef.js create mode 100644 dist/lib/sqlite/schema.d.ts create mode 100644 dist/lib/sqlite/schema.js create mode 100644 dist/types/index.d.ts create mode 100644 dist/types/index.js create mode 100644 dist/utils/append-default-fields-to-db-schema.d.ts create mode 100644 dist/utils/append-default-fields-to-db-schema.js create mode 100644 dist/utils/grab-backup-data.d.ts create mode 100644 dist/utils/grab-backup-data.js create mode 100644 dist/utils/grab-db-backup-file-name.d.ts create mode 100644 dist/utils/grab-db-backup-file-name.js create mode 100644 dist/utils/grab-db-dir.d.ts create mode 100644 dist/utils/grab-db-dir.js create mode 100644 dist/utils/grab-sorted-backups.d.ts create mode 100644 dist/utils/grab-sorted-backups.js create mode 100644 dist/utils/sql-equality-parser.d.ts create mode 100644 dist/utils/sql-equality-parser.js create mode 100644 dist/utils/sql-gen-operator-gen.d.ts create mode 100644 dist/utils/sql-gen-operator-gen.js create mode 100644 dist/utils/sql-generator.d.ts create mode 100644 dist/utils/sql-generator.js create mode 100644 dist/utils/sql-insert-generator.d.ts create mode 100644 dist/utils/sql-insert-generator.js create mode 100644 dist/utils/trim-backups.d.ts create mode 100644 dist/utils/trim-backups.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 publish.sh create mode 100644 src/commands/backup.ts create mode 100644 src/commands/index.ts create mode 100644 src/commands/restore.ts create mode 100644 src/commands/schema.ts create mode 100644 src/commands/typedef.ts create mode 100644 src/data/app-data.ts create mode 100644 src/data/grab-dir-names.ts create mode 100644 src/functions/init.ts create mode 100644 src/index.ts create mode 100644 src/lib/sqlite/db-delete.ts create mode 100644 src/lib/sqlite/db-generate-type-defs.ts create mode 100644 src/lib/sqlite/db-insert.ts create mode 100644 src/lib/sqlite/db-schema-manager.ts create mode 100644 src/lib/sqlite/db-schema-to-typedef.ts create mode 100644 src/lib/sqlite/db-select.ts create mode 100644 src/lib/sqlite/db-sql.ts create mode 100644 src/lib/sqlite/db-update.ts create mode 100644 src/lib/sqlite/index.ts create mode 100644 src/lib/sqlite/schema-to-typedef.ts create mode 100644 src/lib/sqlite/schema.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/append-default-fields-to-db-schema.ts create mode 100644 src/utils/grab-backup-data.ts create mode 100644 src/utils/grab-db-backup-file-name.ts create mode 100644 src/utils/grab-db-dir.ts create mode 100644 src/utils/grab-sorted-backups.ts create mode 100644 src/utils/sql-equality-parser.ts create mode 100644 src/utils/sql-gen-operator-gen.ts create mode 100644 src/utils/sql-generator.ts create mode 100644 src/utils/sql-insert-generator.ts create mode 100644 src/utils/trim-backups.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bd9555 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..6bc2539 --- /dev/null +++ b/.npmrc @@ -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} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ee6890 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +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

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dab7d7 --- /dev/null +++ b/README.md @@ -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`: + +```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({ table, query?, count?, targetId? }) +``` + +| Parameter | Type | Description | +| ---------- | --------------------- | ------------------------------------------------------------ | +| `table` | `string` | Table name | +| `query` | `ServerQueryParam` | 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({ + 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({ 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({ table, data, query?, targetId? }) +``` + +| Parameter | Type | Description | +| ---------- | --------------------- | --------------------------- | +| `table` | `string` | Table name | +| `data` | `Partial` | Fields to update | +| `query` | `ServerQueryParam` | 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({ table, query?, targetId? }) +``` + +| Parameter | Type | Description | +| ---------- | --------------------- | --------------------------- | +| `table` | `string` | Table name | +| `query` | `ServerQueryParam` | 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({ 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({ 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` object: + +```ts +type ServerQueryParam = { + 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({ + 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: "", + 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__`) +- A union type `BUN_SQLITE__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({ + 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 diff --git a/dist/commands/backup.d.ts b/dist/commands/backup.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/backup.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/backup.js b/dist/commands/backup.js new file mode 100644 index 0000000..03c7d3c --- /dev/null +++ b/dist/commands/backup.js @@ -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(); + }); +} diff --git a/dist/commands/index.d.ts b/dist/commands/index.d.ts new file mode 100644 index 0000000..4cb206d --- /dev/null +++ b/dist/commands/index.d.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env bun +/** + * # Declare Global Variables + */ +declare global { } +export {}; diff --git a/dist/commands/index.js b/dist/commands/index.js new file mode 100644 index 0000000..8c77c48 --- /dev/null +++ b/dist/commands/index.js @@ -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); diff --git a/dist/commands/restore.d.ts b/dist/commands/restore.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/restore.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/restore.js b/dist/commands/restore.js new file mode 100644 index 0000000..99c4bfd --- /dev/null +++ b/dist/commands/restore.js @@ -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(); + } + }); +} diff --git a/dist/commands/schema.d.ts b/dist/commands/schema.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/schema.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/schema.js b/dist/commands/schema.js new file mode 100644 index 0000000..c05c9bf --- /dev/null +++ b/dist/commands/schema.js @@ -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(); + }); +} diff --git a/dist/commands/typedef.d.ts b/dist/commands/typedef.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/typedef.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/typedef.js b/dist/commands/typedef.js new file mode 100644 index 0000000..df2863c --- /dev/null +++ b/dist/commands/typedef.js @@ -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(); + }); +} diff --git a/dist/data/app-data.d.ts b/dist/data/app-data.d.ts new file mode 100644 index 0000000..c87945c --- /dev/null +++ b/dist/data/app-data.d.ts @@ -0,0 +1,5 @@ +export declare const AppData: { + readonly ConfigFileName: "bun-sqlite.config.ts"; + readonly MaxBackups: 10; + readonly DefaultBackupDirName: ".backups"; +}; diff --git a/dist/data/app-data.js b/dist/data/app-data.js new file mode 100644 index 0000000..5045044 --- /dev/null +++ b/dist/data/app-data.js @@ -0,0 +1,5 @@ +export const AppData = { + ConfigFileName: "bun-sqlite.config.ts", + MaxBackups: 10, + DefaultBackupDirName: ".backups", +}; diff --git a/dist/data/grab-dir-names.d.ts b/dist/data/grab-dir-names.d.ts new file mode 100644 index 0000000..54d9b88 --- /dev/null +++ b/dist/data/grab-dir-names.d.ts @@ -0,0 +1,3 @@ +export default function grabDirNames(): { + ROOT_DIR: string; +}; diff --git a/dist/data/grab-dir-names.js b/dist/data/grab-dir-names.js new file mode 100644 index 0000000..e92b2dc --- /dev/null +++ b/dist/data/grab-dir-names.js @@ -0,0 +1,7 @@ +import path from "path"; +export default function grabDirNames() { + const ROOT_DIR = process.cwd(); + return { + ROOT_DIR, + }; +} diff --git a/dist/functions/init.d.ts b/dist/functions/init.d.ts new file mode 100644 index 0000000..c7c6d0b --- /dev/null +++ b/dist/functions/init.d.ts @@ -0,0 +1,2 @@ +import type { BunSQLiteConfigReturn } from "../types"; +export default function init(): Promise; diff --git a/dist/functions/init.js b/dist/functions/init.js new file mode 100644 index 0000000..4635e0b --- /dev/null +++ b/dist/functions/init.js @@ -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); + } +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..778c289 --- /dev/null +++ b/dist/index.d.ts @@ -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; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..3bff6fb --- /dev/null +++ b/dist/index.js @@ -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; diff --git a/dist/lib/sqlite/db-delete.d.ts b/dist/lib/sqlite/db-delete.d.ts new file mode 100644 index 0000000..c1c63ba --- /dev/null +++ b/dist/lib/sqlite/db-delete.d.ts @@ -0,0 +1,16 @@ +import type { APIResponseObject, ServerQueryParam } from "../../types"; +type Params = { + table: Table; + query?: ServerQueryParam; + targetId?: number | string; +}; +export default function DbDelete({ table, query, targetId, }: Params): Promise; +export {}; diff --git a/dist/lib/sqlite/db-delete.js b/dist/lib/sqlite/db-delete.js new file mode 100644 index 0000000..34c3370 --- /dev/null +++ b/dist/lib/sqlite/db-delete.js @@ -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, + }; + } +} diff --git a/dist/lib/sqlite/db-generate-type-defs.d.ts b/dist/lib/sqlite/db-generate-type-defs.d.ts new file mode 100644 index 0000000..e5f450a --- /dev/null +++ b/dist/lib/sqlite/db-generate-type-defs.d.ts @@ -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 {}; diff --git a/dist/lib/sqlite/db-generate-type-defs.js b/dist/lib/sqlite/db-generate-type-defs.js new file mode 100644 index 0000000..1c45eea --- /dev/null +++ b/dist/lib/sqlite/db-generate-type-defs.js @@ -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 }; +} diff --git a/dist/lib/sqlite/db-insert.d.ts b/dist/lib/sqlite/db-insert.d.ts new file mode 100644 index 0000000..62725f7 --- /dev/null +++ b/dist/lib/sqlite/db-insert.d.ts @@ -0,0 +1,15 @@ +import type { APIResponseObject } from "../../types"; +type Params = { + table: Table; + data: Schema[]; +}; +export default function DbInsert({ table, data }: Params): Promise; +export {}; diff --git a/dist/lib/sqlite/db-insert.js b/dist/lib/sqlite/db-insert.js new file mode 100644 index 0000000..df0b86d --- /dev/null +++ b/dist/lib/sqlite/db-insert.js @@ -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, + }; + } +} diff --git a/dist/lib/sqlite/db-schema-manager.d.ts b/dist/lib/sqlite/db-schema-manager.d.ts new file mode 100644 index 0000000..7a6883a --- /dev/null +++ b/dist/lib/sqlite/db-schema-manager.d.ts @@ -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; + /** + * 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 }; diff --git a/dist/lib/sqlite/db-schema-manager.js b/dist/lib/sqlite/db-schema-manager.js new file mode 100644 index 0000000..d928fc9 --- /dev/null +++ b/dist/lib/sqlite/db-schema-manager.js @@ -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 }; diff --git a/dist/lib/sqlite/db-schema-to-typedef.d.ts b/dist/lib/sqlite/db-schema-to-typedef.d.ts new file mode 100644 index 0000000..43062ab --- /dev/null +++ b/dist/lib/sqlite/db-schema-to-typedef.d.ts @@ -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 {}; diff --git a/dist/lib/sqlite/db-schema-to-typedef.js b/dist/lib/sqlite/db-schema-to-typedef.js new file mode 100644 index 0000000..1739dbb --- /dev/null +++ b/dist/lib/sqlite/db-schema-to-typedef.js @@ -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]; +} diff --git a/dist/lib/sqlite/db-select.d.ts b/dist/lib/sqlite/db-select.d.ts new file mode 100644 index 0000000..ed27786 --- /dev/null +++ b/dist/lib/sqlite/db-select.d.ts @@ -0,0 +1,17 @@ +import type { APIResponseObject, ServerQueryParam } from "../../types"; +type Params = { + query?: ServerQueryParam; + table: Table; + count?: boolean; + targetId?: number | string; +}; +export default function DbSelect({ table, query, count, targetId, }: Params): Promise>; +export {}; diff --git a/dist/lib/sqlite/db-select.js b/dist/lib/sqlite/db-select.js new file mode 100644 index 0000000..dfd50ed --- /dev/null +++ b/dist/lib/sqlite/db-select.js @@ -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, + }; + } +} diff --git a/dist/lib/sqlite/db-sql.d.ts b/dist/lib/sqlite/db-sql.d.ts new file mode 100644 index 0000000..2b1471c --- /dev/null +++ b/dist/lib/sqlite/db-sql.d.ts @@ -0,0 +1,11 @@ +import type { APIResponseObject } from "../../types"; +type Params = { + sql: string; + values?: (string | number)[]; +}; +export default function DbSQL({ sql, values }: Params): Promise>; +export {}; diff --git a/dist/lib/sqlite/db-sql.js b/dist/lib/sqlite/db-sql.js new file mode 100644 index 0000000..d348c2b --- /dev/null +++ b/dist/lib/sqlite/db-sql.js @@ -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, + }; + } +} diff --git a/dist/lib/sqlite/db-update.d.ts b/dist/lib/sqlite/db-update.d.ts new file mode 100644 index 0000000..68bff0c --- /dev/null +++ b/dist/lib/sqlite/db-update.d.ts @@ -0,0 +1,17 @@ +import type { APIResponseObject, ServerQueryParam } from "../../types"; +type Params = { + table: Table; + data: Schema; + query?: ServerQueryParam; + targetId?: number | string; +}; +export default function DbUpdate({ table, data, query, targetId, }: Params): Promise; +export {}; diff --git a/dist/lib/sqlite/db-update.js b/dist/lib/sqlite/db-update.js new file mode 100644 index 0000000..54ae816 --- /dev/null +++ b/dist/lib/sqlite/db-update.js @@ -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, + }; + } +} diff --git a/dist/lib/sqlite/index.d.ts b/dist/lib/sqlite/index.d.ts new file mode 100644 index 0000000..438b68a --- /dev/null +++ b/dist/lib/sqlite/index.d.ts @@ -0,0 +1,3 @@ +import { Database } from "bun:sqlite"; +declare const DbClient: Database; +export default DbClient; diff --git a/dist/lib/sqlite/index.js b/dist/lib/sqlite/index.js new file mode 100644 index 0000000..4287770 --- /dev/null +++ b/dist/lib/sqlite/index.js @@ -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; diff --git a/dist/lib/sqlite/schema-to-typedef.d.ts b/dist/lib/sqlite/schema-to-typedef.d.ts new file mode 100644 index 0000000..bc433e8 --- /dev/null +++ b/dist/lib/sqlite/schema-to-typedef.d.ts @@ -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 {}; diff --git a/dist/lib/sqlite/schema-to-typedef.js b/dist/lib/sqlite/schema-to-typedef.js new file mode 100644 index 0000000..ff0231d --- /dev/null +++ b/dist/lib/sqlite/schema-to-typedef.js @@ -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); + } +} diff --git a/dist/lib/sqlite/schema.d.ts b/dist/lib/sqlite/schema.d.ts new file mode 100644 index 0000000..355768e --- /dev/null +++ b/dist/lib/sqlite/schema.d.ts @@ -0,0 +1,2 @@ +import type { BUN_SQLITE_DatabaseSchemaType } from "../../types"; +export declare const DbSchema: BUN_SQLITE_DatabaseSchemaType; diff --git a/dist/lib/sqlite/schema.js b/dist/lib/sqlite/schema.js new file mode 100644 index 0000000..5778e50 --- /dev/null +++ b/dist/lib/sqlite/schema.js @@ -0,0 +1,5 @@ +import _ from "lodash"; +export const DbSchema = { + dbName: "travis-ai", + tables: [], +}; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts new file mode 100644 index 0000000..732fe9f --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,1015 @@ +import type { RequestOptions } from "https"; +export type BUN_SQLITE_DatabaseFullName = string; +export declare const UsersOmitedFields: readonly ["password", "social_id", "verification_status", "date_created", "date_created_code", "date_created_timestamp", "date_updated", "date_updated_code", "date_updated_timestamp"]; +export interface BUN_SQLITE_DatabaseSchemaType { + id?: string | number; + dbName?: string; + dbSlug?: string; + dbFullName?: string; + dbDescription?: string; + dbImage?: string; + tables: BUN_SQLITE_TableSchemaType[]; + childrenDatabases?: BUN_SQLITE_ChildrenDatabaseObject[]; + childDatabase?: boolean; + childDatabaseDbId?: string | number; + updateData?: boolean; + collation?: (typeof MariaDBCollations)[number]; +} +export interface BUN_SQLITE_ChildrenDatabaseObject { + dbId?: string | number; +} +export declare const MariaDBCollations: readonly ["utf8mb4_bin", "utf8mb4_unicode_520_ci"]; +export interface BUN_SQLITE_TableSchemaType { + id?: string | number; + tableName: string; + tableDescription?: string; + fields: BUN_SQLITE_FieldSchemaType[]; + indexes?: BUN_SQLITE_IndexSchemaType[]; + uniqueConstraints?: BUN_SQLITE_UniqueConstraintSchemaType[]; + childrenTables?: BUN_SQLITE_ChildrenTablesType[]; + /** + * Whether this is a child table + */ + childTable?: boolean; + updateData?: boolean; + /** + * ID of the parent table + */ + childTableId?: string | number; + /** + * ID of the parent table + */ + parentTableId?: string | number; + /** + * ID of the Database of parent table + */ + parentTableDbId?: string | number; + /** + * Name of the Database of parent table + */ + parentTableDbName?: string; + /** + * Name of the parent table + */ + parentTableName?: string; + tableNameOld?: string; + /** + * ID of the Database of parent table + */ + childTableDbId?: string | number; + collation?: (typeof MariaDBCollations)[number]; + isVector?: boolean; + vectorType?: string; +} +export interface BUN_SQLITE_ChildrenTablesType { + tableId?: string | number; + dbId?: string | number; +} +export declare const TextFieldTypesArray: readonly [{ + readonly title: "Plain Text"; + readonly value: "plain"; +}, { + readonly title: "Rich Text"; + readonly value: "richText"; +}, { + readonly title: "Markdown"; + readonly value: "markdown"; +}, { + readonly title: "JSON"; + readonly value: "json"; +}, { + readonly title: "YAML"; + readonly value: "yaml"; +}, { + readonly title: "HTML"; + readonly value: "html"; +}, { + readonly title: "CSS"; + readonly value: "css"; +}, { + readonly title: "Javascript"; + readonly value: "javascript"; +}, { + readonly title: "Shell"; + readonly value: "shell"; +}, { + readonly title: "Code"; + readonly value: "code"; +}]; +export declare const BUN_SQLITE_DATATYPES: readonly [{ + readonly value: "TEXT"; +}, { + readonly value: "INTEGER"; +}]; +export type BUN_SQLITE_FieldSchemaType = { + id?: number | string; + fieldName?: string; + fieldDescription?: string; + originName?: string; + updatedField?: boolean; + dataType: (typeof BUN_SQLITE_DATATYPES)[number]["value"]; + nullValue?: boolean; + notNullValue?: boolean; + primaryKey?: boolean; + encrypted?: boolean; + autoIncrement?: boolean; + defaultValue?: string | number; + defaultValueLiteral?: string; + foreignKey?: BUN_SQLITE_ForeignKeyType; + defaultField?: boolean; + plainText?: boolean; + unique?: boolean; + pattern?: string; + patternFlags?: string; + onUpdate?: string; + onUpdateLiteral?: string; + onDelete?: string; + onDeleteLiteral?: string; + cssFiles?: string[]; + integerLength?: string | number; + decimals?: string | number; + code?: boolean; + options?: (string | number)[]; + isVector?: boolean; + vectorSize?: number; + /** + * ### Adds a `+` prefix to colums + * In sqlite-vec, the + prefix is a specialized syntax for Virtual Table Columns. It essentially tells the database: "Keep this data associated with the vector, but don't try to index it for math." +Here is the breakdown of why they matter and how they work: +1. Performance Separation +In a standard table, adding a massive TEXT column (like a 2,000-word article) slows down full-table scans. In a vec0 virtual table, columns prefixed with + are stored in a separate internal side-car table. +The Vector Index: Stays lean and fast for "Nearest Neighbor" math. +The Content: Is only fetched after the vector search identifies the winning rows. +2. The "No Join" Convenience +Normally, you would store vectors in one table and the actual text content in another, linking them with a FOREIGN KEY. +Without + columns: You must JOIN two tables to get the text after finding the vector. +With + columns: You can SELECT content directly from the virtual table. It handles the "join" logic internally, making your code cleaner. +3. Syntax Example +When defining your schema, the + is only used in the CREATE statement. When querying or inserting, you treat it like a normal name. +```sql +-- SCHEMA DEFINITION +CREATE VIRTUAL TABLE documents USING vec0( + embedding float, -- The vector (indexed) + +title TEXT, -- Side-car metadata (not indexed) + +raw_body TEXT -- Side-car "heavy" data (not indexed) +); + +-- INSERTING (Notice: No '+' here) +INSERT INTO documents(embedding, title, raw_body) +VALUES (vec_f32(?), 'Bun Docs', 'Bun is a fast JavaScript runtime...'); + +-- QUERYING (Notice: No '+' here) +SELECT title, raw_body +FROM documents +WHERE embedding MATCH ? AND k = 1; +``` + */ + sideCar?: boolean; +} & { + [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; +}; +export interface BUN_SQLITE_ForeignKeyType { + foreignKeyName?: string; + destinationTableName?: string; + destinationTableColumnName?: string; + destinationTableColumnType?: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} +export interface BUN_SQLITE_IndexSchemaType { + id?: string | number; + indexName?: string; + indexType?: (typeof IndexTypes)[number]; + indexTableFields?: BUN_SQLITE_IndexTableFieldType[]; + alias?: string; + newTempIndex?: boolean; +} +export interface BUN_SQLITE_UniqueConstraintSchemaType { + id?: string | number; + constraintName?: string; + alias?: string; + constraintTableFields?: BUN_SQLITE_UniqueConstraintFieldType[]; +} +export interface BUN_SQLITE_UniqueConstraintFieldType { + value: string; +} +export interface BUN_SQLITE_IndexTableFieldType { + value: string; + dataType: string; +} +export interface BUN_SQLITE_MYSQL_SHOW_INDEXES_Type { + Key_name: string; + Table: string; + Column_name: string; + Collation: string; + Index_type: string; + Cardinality: string; + Index_comment: string; + Comment: string; +} +export interface BUN_SQLITE_MYSQL_SHOW_COLUMNS_Type { + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; +} +export interface BUN_SQLITE_MARIADB_SHOW_INDEXES_TYPE { + Table: string; + Non_unique: 0 | 1; + Key_name: string; + Seq_in_index: number; + Column_name: string; + Collation: string; + Cardinality: number; + Sub_part?: string; + Packed?: string; + Index_type?: "BTREE"; + Comment?: string; + Index_comment?: string; + Ignored?: "YES" | "NO"; +} +export interface BUN_SQLITE_MYSQL_FOREIGN_KEYS_Type { + CONSTRAINT_NAME: string; + CONSTRAINT_SCHEMA: string; + TABLE_NAME: string; +} +export interface BUN_SQLITE_MYSQL_user_databases_Type { + id: number; + user_id: number; + db_full_name: string; + db_name: string; + db_slug: string; + db_image: string; + db_description: string; + active_clone: number; + active_data: 0 | 1; + active_clone_parent_db: string; + remote_connected?: number; + remote_db_full_name?: string; + remote_connection_host?: string; + remote_connection_key?: string; + remote_connection_type?: string; + user_priviledge?: string; + date_created?: string; + image_thumbnail?: string; + first_name?: string; + last_name?: string; + email?: string; +} +export type ImageInputFileToBase64FunctionReturn = { + imageBase64?: string; + imageBase64Full?: string; + imageName?: string; + imageSize?: number; +}; +export interface GetReqQueryObject { + db: string; + query: string; + queryValues?: string; + tableName?: string; + debug?: boolean; +} +export type DATASQUIREL_LoggedInUser = { + id: number; + uuid?: string; + first_name: string; + last_name: string; + email: string; + phone?: string; + user_type?: string; + username?: string; + image?: string; + image_thumbnail?: string; + social_login?: number; + social_platform?: string; + social_id?: string; + verification_status?: number; + csrf_k: string; + logged_in_status: boolean; + date: number; +} & { + [key: string]: any; +}; +export interface AuthenticatedUser { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + cookieNames?: any; +} +export interface SuccessUserObject { + id: number; + first_name: string; + last_name: string; + email: string; +} +export interface AddUserFunctionReturn { + success: boolean; + payload?: SuccessUserObject | null; + msg?: string; + sqlResult?: any; +} +export interface GoogleIdentityPromptNotification { + getMomentType: () => string; + getDismissedReason: () => string; + getNotDisplayedReason: () => string; + getSkippedReason: () => string; + isDismissedMoment: () => boolean; + isDisplayMoment: () => boolean; + isDisplayed: () => boolean; + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; +} +export type UserDataPayload = { + first_name: string; + last_name: string; + email: string; + password?: string; + username?: string; +} & { + [key: string]: any; +}; +export interface GetUserFunctionReturn { + success: boolean; + payload: { + id: number; + first_name: string; + last_name: string; + username: string; + email: string; + phone: string; + social_id: [string]; + image: string; + image_thumbnail: string; + verification_status: [number]; + } | null; +} +export interface ReauthUserFunctionReturn { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + token?: string; +} +export interface UpdateUserFunctionReturn { + success: boolean; + payload?: Object[] | string; +} +export interface GetReturn { + success: boolean; + payload?: R; + msg?: string; + error?: string; + schema?: BUN_SQLITE_TableSchemaType; + finalQuery?: string; +} +export interface GetSchemaRequestQuery { + database?: string; + table?: string; + field?: string; + user_id?: string | number; + env?: { + [k: string]: string; + }; +} +export interface GetSchemaAPICredentialsParam { + key: string; +} +export type GetSchemaAPIParam = GetSchemaRequestQuery & GetSchemaAPICredentialsParam; +export interface PostReturn { + success: boolean; + payload?: Object[] | string | PostInsertReturn; + msg?: string; + error?: any; + schema?: BUN_SQLITE_TableSchemaType; +} +export interface PostDataPayload { + action: "insert" | "update" | "delete"; + table: string; + data?: object; + identifierColumnName?: string; + identifierValue?: string; + duplicateColumnName?: string; + duplicateColumnValue?: string; + update?: boolean; +} +export interface LocalPostReturn { + success: boolean; + payload?: any; + msg?: string; + error?: string; +} +export interface LocalPostQueryObject { + query: string | PostDataPayload; + tableName?: string; + queryValues?: string[]; +} +export interface PostInsertReturn { + fieldCount?: number; + affectedRows?: number; + insertId?: number; + serverStatus?: number; + warningCount?: number; + message?: string; + protocol41?: boolean; + changedRows?: number; + error?: string; +} +export type UserType = DATASQUIREL_LoggedInUser & { + isSuperUser?: boolean; + staticHost?: string; + appHost?: string; + appName?: string; +}; +export interface ApiKeyDef { + name: string; + scope: string; + date_created: string; + apiKeyPayload: string; +} +export interface MetricsType { + dbCount: number; + tablesCount: number; + mediaCount: number; + apiKeysCount: number; +} +export interface MYSQL_mariadb_users_table_def { + id?: number; + user_id?: number; + username?: string; + host?: string; + password?: string; + primary?: number; + grants?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} +export interface MariaDBUserCredType { + mariadb_user?: string; + mariadb_host?: string; + mariadb_pass?: string; +} +export declare const ServerQueryOperators: readonly ["AND", "OR"]; +export declare const ServerQueryEqualities: readonly ["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 type ServerQueryParam = { + selectFields?: (keyof T | TableSelectFieldsObject)[]; + omitFields?: (keyof T)[]; + query?: ServerQueryQueryObject; + limit?: number; + page?: number; + offset?: number; + order?: ServerQueryParamOrder | ServerQueryParamOrder[]; + searchOperator?: (typeof ServerQueryOperators)[number]; + searchEquality?: (typeof ServerQueryEqualities)[number]; + addUserId?: { + fieldName: keyof T; + }; + join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[]; + group?: keyof T | ServerQueryParamGroupBy | (keyof T | ServerQueryParamGroupBy)[]; + countSubQueries?: ServerQueryParamsCount[]; + fullTextSearch?: ServerQueryParamFullTextSearch; + [key: string]: any; +}; +export type ServerQueryParamGroupBy = { + field: keyof T; + table?: string; +}; +export type ServerQueryParamOrder = { + field: keyof T; + strategy: "ASC" | "DESC"; +}; +export type ServerQueryParamFullTextSearch = { + fields: (keyof T)[]; + searchTerm: string; + /** Field Name to user to Rank the Score of Search Results */ + scoreAlias: string; +}; +export type ServerQueryParamsCount = { + table: string; + /** Alias for the Table From which the count is fetched */ + table_alias?: string; + srcTrgMap: { + src: string; + trg: string | ServerQueryParamsCountSrcTrgMap; + }[]; + alias: string; +}; +export type ServerQueryParamsCountSrcTrgMap = { + table: string; + field: string; +}; +export type TableSelectFieldsObject = { + fieldName: keyof T; + alias?: string; +}; +export type ServerQueryValuesObject = { + value?: string | number; + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: string; + fieldName?: string; +}; +export type ServerQueryObjectValue = string | (string | ServerQueryValuesObject | undefined | null) | (string | ServerQueryValuesObject | undefined | null)[]; +export type ServerQueryObject = { + value?: ServerQueryObjectValue; + nullValue?: boolean; + notNullValue?: boolean; + operator?: (typeof ServerQueryOperators)[number]; + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: K; + /** + * This will replace the top level field name if + * provided + */ + fieldName?: string; + __query?: { + [key in keyof T]: Omit, "__query">; + }; + vector?: boolean; + /** + * ### The Function to be used to generate the vector. + * Eg. `vec_f32`. This will come out as `vec_f32(?)` + * instead of just `?` + */ + vectorFunction?: string; +}; +export type ServerQueryQueryObject = { + [key in keyof T]: ServerQueryObject; +}; +export type FetchDataParams = { + path: string; + method?: (typeof DataCrudRequestMethods)[number]; + body?: object | string; + query?: AuthFetchQuery; + tableName?: string; +}; +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: any; +}; +export type ServerQueryParamsJoin = { + joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; + alias?: string; + tableName: Table; + match?: ServerQueryParamsJoinMatchObject | ServerQueryParamsJoinMatchObject[]; + selectFields?: (keyof Field | { + field: keyof Field; + alias?: string; + count?: boolean; + })[]; + omitFields?: (keyof Field | { + field: keyof Field; + alias?: string; + count?: boolean; + })[]; + operator?: (typeof ServerQueryOperators)[number]; +}; +export type ServerQueryParamsJoinMatchObject = { + /** Field name from the **Root Table** */ + source?: string | ServerQueryParamsJoinMatchSourceTargetObject; + /** Field name from the **Join Table** */ + target?: keyof Field | ServerQueryParamsJoinMatchSourceTargetObject; + /** A literal value: No source and target Needed! */ + targetLiteral?: string | number; + __batch?: { + matches: Omit, "__batch">[]; + operator: "AND" | "OR"; + }; +}; +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; +export type ApiConnectBody = { + url: string; + key: string; + database: BUN_SQLITE_MYSQL_user_databases_Type; + dbSchema: BUN_SQLITE_DatabaseSchemaType; + type: "pull" | "push"; + user_id?: string | number; +}; +export type SuUserType = { + email: string; + password: string; + authKey: string; + logged_in_status: boolean; + date: number; +}; +export type MariadbRemoteServerObject = { + host: string; + port: number; + primary?: boolean; + loadBalanced?: boolean; + users?: MariadbRemoteServerUserObject[]; +}; +export type MariadbRemoteServerUserObject = { + name: string; + password: string; + host: string; +}; +export type APILoginFunctionReturn = { + success: boolean; + msg?: string; + payload?: DATASQUIREL_LoggedInUser | null; + userId?: number | string; + key?: string; + token?: string; + csrf?: string; + cookieNames?: any; +}; +export type APICreateUserFunctionParams = { + encryptionKey?: string; + payload: any; + database: string; + dsqlUserID?: string | number; + verify?: boolean; +}; +export type APICreateUserFunction = (params: APICreateUserFunctionParams) => Promise; +export type HandleSocialDbFunctionReturn = { + success: boolean; + user?: DATASQUIREL_LoggedInUser | null; + msg?: string; + social_id?: string | number; + social_platform?: string; + payload?: any; + alert?: boolean; + newUser?: any; + error?: any; +} | null; +export type CookieObject = { + name: string; + value: string; + domain?: string; + path?: string; + expires?: Date; + maxAge?: number; + secure?: boolean; + httpOnly?: boolean; + sameSite?: "Strict" | "Lax" | "None"; + priority?: "Low" | "Medium" | "High"; +}; +export type HttpRequestParams = RequestOptions & { + scheme?: "http" | "https"; + body?: ReqObj; + query?: ReqObj; + urlEncodedFormBody?: boolean; +}; +export type HttpRequestFunction = (param: HttpRequestParams) => Promise>; +export type HttpFunctionResponse = { + status: number; + data?: ResObj; + error?: string; + str?: string; + requestedPath?: string; +}; +export type ApiGetQueryObject = { + query: ServerQueryParam; + table: string; + dbFullName?: string; +}; +export declare const DataCrudRequestMethods: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]; +export declare const DataCrudRequestMethodsLowerCase: readonly ["get", "post", "put", "patch", "delete", "options"]; +export type DsqlMethodCrudParam = { + method: (typeof DataCrudRequestMethods)[number]; + body?: T; + query?: DsqlCrudQueryObject; + tableName: string; + addUser?: { + field: keyof T; + }; + user?: DATASQUIREL_LoggedInUser; + extraData?: T; + transformData?: DsqlCrudTransformDataFunction; + transformQuery?: DsqlCrudTransformQueryFunction; + existingData?: T; + targetId?: string | number; + sanitize?: ({ data, batchData }: { + data?: T; + batchData?: T[]; + }) => T | T[]; + debug?: boolean; +}; +export type DsqlCrudTransformDataFunction = (params: { + data: T; + user?: DATASQUIREL_LoggedInUser; + existingData?: T; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise; +export type DsqlCrudTransformQueryFunction = (params: { + query: DsqlCrudQueryObject; + user?: DATASQUIREL_LoggedInUser; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise>; +export declare const DsqlCrudActions: readonly ["insert", "update", "delete", "get"]; +export type DsqlCrudQueryObject = ServerQueryParam & { + query?: ServerQueryQueryObject; +}; +export type SQLDeleteGeneratorParams = { + tableName: string; + deleteKeyValues?: SQLDeleteData[]; + deleteKeyValuesOperator?: "AND" | "OR"; + dbFullName?: string; + data?: any; +}; +export type SQLDeleteData = { + key: keyof T; + value: string | number | null | undefined; + operator?: (typeof ServerQueryEqualities)[number]; +}; +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; +export type ErrorCallback = (title: string, error: Error, data?: any) => void; +export interface MariaDBUser { + Host?: string; + User?: string; + Password?: string; + Select_priv?: string; + Insert_priv?: string; + Update_priv?: string; + Delete_priv?: string; + Create_priv?: string; + Drop_priv?: string; + Reload_priv?: string; + Shutdown_priv?: string; + Process_priv?: string; + File_priv?: string; + Grant_priv?: string; + References_priv?: string; + Index_priv?: string; + Alter_priv?: string; + Show_db_priv?: string; + Super_priv?: string; + Create_tmp_table_priv?: string; + Lock_tables_priv?: string; + Execute_priv?: string; + Repl_slave_priv?: string; + Repl_client_priv?: string; + Create_view_priv?: string; + Show_view_priv?: string; + Create_routine_priv?: string; + Alter_routine_priv?: string; + Create_user_priv?: string; + Event_priv?: string; + Trigger_priv?: string; + Create_tablespace_priv?: string; + Delete_history_priv?: string; + ssl_type?: string; + ssl_cipher?: string; + x509_issuer?: string; + x509_subject?: string; + max_questions?: number; + max_updates?: number; + max_connections?: number; + max_user_connections?: number; + plugin?: string; + authentication_string?: string; + password_expired?: string; + is_role?: string; + default_role?: string; + max_statement_time?: number; +} +export declare const QueryFields: readonly ["duplicate", "user_id", "delegated_user_id", "db_id", "table_id", "db_slug"]; +export type LocalFolderType = { + name: string; + isPrivate: boolean; +}; +export type ResponseQueryObject = { + sql?: string; + params?: (string | number)[]; +}; +export type APIResponseObject = { + success: boolean; + payload?: T[] | null; + singleRes?: T | null; + stringRes?: string | null; + numberRes?: number | null; + postInsertReturn?: PostInsertReturn | null; + payloadBase64?: string; + payloadThumbnailBase64?: string; + payloadURL?: string; + payloadThumbnailURL?: string; + error?: any; + msg?: string; + queryObject?: ResponseQueryObject; + countQueryObject?: ResponseQueryObject; + status?: number; + count?: number; + errors?: BUNSQLITEErrorObject[]; + debug?: any; + batchPayload?: any[][] | null; + errorData?: any; + token?: string; + csrf?: string; + cookieNames?: any; + key?: string; + userId?: string | number; + code?: string; + createdAt?: number; + email?: string; + requestOptions?: RequestOptions; + logoutUser?: boolean; + redirect?: string; +}; +/** + * # Docker Compose Types + */ +export type DockerCompose = { + services: DockerComposeServicesType; + networks: DockerComposeNetworks; + name: string; +}; +export declare const DockerComposeServices: readonly ["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 type DockerComposeServicesType = { + [key in (typeof DockerComposeServices)[number]]: DockerComposeServiceWithBuildObject; +}; +export type DockerComposeNetworks = { + [k: string]: { + driver?: "bridge"; + ipam?: { + config: DockerComposeNetworkConfigObject[]; + }; + external?: boolean; + }; +}; +export type DockerComposeNetworkConfigObject = { + subnet: string; + gateway: string; +}; +export type DockerComposeServiceWithBuildObject = { + build: DockerComposeServicesBuildObject; + env_file: string; + container_name: string; + hostname: string; + volumes: string[]; + environment: string[]; + ports?: string[]; + networks?: DockerComposeServiceNetworkObject; + restart?: string; + depends_on?: { + [k: string]: { + condition: string; + }; + }; + user?: string; +}; +export type DockerComposeServiceWithImage = Omit & { + image: string; +}; +export type DockerComposeServicesBuildObject = { + context: string; + dockerfile: string; +}; +export type DockerComposeServiceNetworkObject = { + [k: string]: { + ipv4_address: string; + }; +}; +export type ClonedTableInfo = { + dbId?: string | number; + tableId?: string | number; + keepUpdated?: boolean; + keepDataUpdated?: boolean; +}; +export type DefaultEntryType = { + id?: number; + uuid?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} & { + [k: string]: string | number | null; +}; +export declare const IndexTypes: readonly ["regular", "full_text", "vector"]; +export type BUNSQLITEErrorObject = { + sql?: string; + sqlValues?: any[]; + error?: string; +}; +export interface SQLInsertGenReturn { + query: string; + values: (string | number)[]; +} +export type SQLInsertGenDataFn = () => { + placeholder: string; + value: string | number | Float32Array; +}; +export type SQLInsertGenDataType = { + [k: string]: string | number | SQLInsertGenDataFn | undefined | null; +}; +export type SQLInsertGenParams = { + data: SQLInsertGenDataType[]; + tableName: string; + dbFullName?: string; +}; +export type BunSQLiteConfig = { + db_name: string; + /** + * The Name of the Database Schema File. Eg `db_schema.ts`. This is + * relative to `db_dir`, or root dir if `db_dir` is not provided + */ + db_schema_file_name: string; + /** + * The Directory for backups. Relative to db_dir. + */ + db_backup_dir?: string; + max_backups?: number; + /** + * The Root Directory for the DB file and schema + */ + db_dir?: string; + /** + * The File Path relative to the root(working) directory for the type + * definition export. Example `db_types.ts` or `types/db_types.ts` + */ + typedef_file_path?: string; +}; +export type BunSQLiteConfigReturn = { + config: BunSQLiteConfig; + dbSchema: BUN_SQLITE_DatabaseSchemaType; +}; +export declare const DefaultFields: BUN_SQLITE_FieldSchemaType[]; diff --git a/dist/types/index.js b/dist/types/index.js new file mode 100644 index 0000000..391067f --- /dev/null +++ b/dist/types/index.js @@ -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)", + }, +]; diff --git a/dist/utils/append-default-fields-to-db-schema.d.ts b/dist/utils/append-default-fields-to-db-schema.d.ts new file mode 100644 index 0000000..5c65648 --- /dev/null +++ b/dist/utils/append-default-fields-to-db-schema.d.ts @@ -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 {}; diff --git a/dist/utils/append-default-fields-to-db-schema.js b/dist/utils/append-default-fields-to-db-schema.js new file mode 100644 index 0000000..d1b758a --- /dev/null +++ b/dist/utils/append-default-fields-to-db-schema.js @@ -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; +} diff --git a/dist/utils/grab-backup-data.d.ts b/dist/utils/grab-backup-data.d.ts new file mode 100644 index 0000000..04cd991 --- /dev/null +++ b/dist/utils/grab-backup-data.d.ts @@ -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 {}; diff --git a/dist/utils/grab-backup-data.js b/dist/utils/grab-backup-data.js new file mode 100644 index 0000000..84439d3 --- /dev/null +++ b/dist/utils/grab-backup-data.js @@ -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 }; +} diff --git a/dist/utils/grab-db-backup-file-name.d.ts b/dist/utils/grab-db-backup-file-name.d.ts new file mode 100644 index 0000000..3744de0 --- /dev/null +++ b/dist/utils/grab-db-backup-file-name.d.ts @@ -0,0 +1,6 @@ +import type { BunSQLiteConfig } from "../types"; +type Params = { + config: BunSQLiteConfig; +}; +export default function grabDBBackupFileName({ config }: Params): string; +export {}; diff --git a/dist/utils/grab-db-backup-file-name.js b/dist/utils/grab-db-backup-file-name.js new file mode 100644 index 0000000..30c72fc --- /dev/null +++ b/dist/utils/grab-db-backup-file-name.js @@ -0,0 +1,4 @@ +export default function grabDBBackupFileName({ config }) { + const new_db_file_name = `${config.db_name}-${Date.now()}`; + return new_db_file_name; +} diff --git a/dist/utils/grab-db-dir.d.ts b/dist/utils/grab-db-dir.d.ts new file mode 100644 index 0000000..b01b221 --- /dev/null +++ b/dist/utils/grab-db-dir.d.ts @@ -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 {}; diff --git a/dist/utils/grab-db-dir.js b/dist/utils/grab-db-dir.js new file mode 100644 index 0000000..89f5a77 --- /dev/null +++ b/dist/utils/grab-db-dir.js @@ -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 }; +} diff --git a/dist/utils/grab-sorted-backups.d.ts b/dist/utils/grab-sorted-backups.d.ts new file mode 100644 index 0000000..9c13c1b --- /dev/null +++ b/dist/utils/grab-sorted-backups.d.ts @@ -0,0 +1,6 @@ +import type { BunSQLiteConfig } from "../types"; +type Params = { + config: BunSQLiteConfig; +}; +export default function grabSortedBackups({ config }: Params): string[]; +export {}; diff --git a/dist/utils/grab-sorted-backups.js b/dist/utils/grab-sorted-backups.js new file mode 100644 index 0000000..0318bb7 --- /dev/null +++ b/dist/utils/grab-sorted-backups.js @@ -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; +} diff --git a/dist/utils/sql-equality-parser.d.ts b/dist/utils/sql-equality-parser.d.ts new file mode 100644 index 0000000..cd5a253 --- /dev/null +++ b/dist/utils/sql-equality-parser.d.ts @@ -0,0 +1,2 @@ +import { ServerQueryEqualities } from "../types"; +export default function sqlEqualityParser(eq: (typeof ServerQueryEqualities)[number]): string; diff --git a/dist/utils/sql-equality-parser.js b/dist/utils/sql-equality-parser.js new file mode 100644 index 0000000..207d670 --- /dev/null +++ b/dist/utils/sql-equality-parser.js @@ -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 "="; + } +} diff --git a/dist/utils/sql-gen-operator-gen.d.ts b/dist/utils/sql-gen-operator-gen.d.ts new file mode 100644 index 0000000..d8535ef --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.d.ts @@ -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 {}; diff --git a/dist/utils/sql-gen-operator-gen.js b/dist/utils/sql-gen-operator-gen.js new file mode 100644 index 0000000..5c93273 --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.js @@ -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, + }; + } + } +} diff --git a/dist/utils/sql-generator.d.ts b/dist/utils/sql-generator.d.ts new file mode 100644 index 0000000..30f714a --- /dev/null +++ b/dist/utils/sql-generator.d.ts @@ -0,0 +1,25 @@ +import type { ServerQueryParam } from "../types"; +type Param = { + genObject?: ServerQueryParam; + 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({ tableName, genObject, dbFullName, count }: Param): Return; +export {}; diff --git a/dist/utils/sql-generator.js b/dist/utils/sql-generator.js new file mode 100644 index 0000000..2b90c09 --- /dev/null +++ b/dist/utils/sql-generator.js @@ -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, + }; +} diff --git a/dist/utils/sql-insert-generator.d.ts b/dist/utils/sql-insert-generator.d.ts new file mode 100644 index 0000000..1b43778 --- /dev/null +++ b/dist/utils/sql-insert-generator.d.ts @@ -0,0 +1,5 @@ +import type { SQLInsertGenParams, SQLInsertGenReturn } from "../types"; +/** + * # SQL Insert Generator + */ +export default function sqlInsertGenerator({ tableName, data, dbFullName, }: SQLInsertGenParams): SQLInsertGenReturn | undefined; diff --git a/dist/utils/sql-insert-generator.js b/dist/utils/sql-insert-generator.js new file mode 100644 index 0000000..7b0ddc7 --- /dev/null +++ b/dist/utils/sql-insert-generator.js @@ -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; + } +} diff --git a/dist/utils/trim-backups.d.ts b/dist/utils/trim-backups.d.ts new file mode 100644 index 0000000..49dd159 --- /dev/null +++ b/dist/utils/trim-backups.d.ts @@ -0,0 +1,6 @@ +import type { BunSQLiteConfig } from "../types"; +type Params = { + config: BunSQLiteConfig; +}; +export default function trimBackups({ config }: Params): void; +export {}; diff --git a/dist/utils/trim-backups.js b/dist/utils/trim-backups.js new file mode 100644 index 0000000..a9bd3a7 --- /dev/null +++ b/dist/utils/trim-backups.js @@ -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); + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..24092f4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1196 @@ +{ + "name": "@moduletrace/node-sqlite", + "version": "1.0.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@moduletrace/node-sqlite", + "version": "1.0.8", + "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" + }, + "bin": { + "bun-sqlite": "dist/commands/index.js" + }, + "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" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.3.tgz", + "integrity": "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.0.tgz", + "integrity": "sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.5", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.8.tgz", + "integrity": "sha512-Di6dgmiZ9xCSUxWUReWTqDtbhXCuG2MQm2xmgSAIruzQzBqNf49b8E07/vbCYY506kDe8BiwJbegXweG8M1klw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.5.tgz", + "integrity": "sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.8.tgz", + "integrity": "sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/external-editor": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.8.tgz", + "integrity": "sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.3.tgz", + "integrity": "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz", + "integrity": "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.8.tgz", + "integrity": "sha512-p0IJslw0AmedLEkOU+yrEX3Aj2RTpQq7ZOf8nc1DIhjzaxRWrrgeuE5Kyh39fVRgtcACaMXx/9WNo8+GjgBOfw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.8.tgz", + "integrity": "sha512-uGLiQah9A0F9UIvJBX52m0CnqtLaym0WpT9V4YZrjZ+YRDKZdwwoEPz06N6w8ChE2lrnsdyhY9sL+Y690Kh9gQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.8.tgz", + "integrity": "sha512-zt1sF4lYLdvPqvmvHdmjOzuUUjuCQ897pdUCO8RbXMUDKXJTTyOQgtn23le+jwcb+MpHl3VAFvzIdxRAf6aPlA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.3.0.tgz", + "integrity": "sha512-JAj66kjdH/F1+B7LCigjARbwstt3SNUOSzMdjpsvwJmzunK88gJeXmcm95L9nw1KynvFVuY4SzXh/3Y0lvtgSg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.0", + "@inquirer/confirm": "^6.0.8", + "@inquirer/editor": "^5.0.8", + "@inquirer/expand": "^5.0.8", + "@inquirer/input": "^5.0.8", + "@inquirer/number": "^4.0.8", + "@inquirer/password": "^5.0.8", + "@inquirer/rawlist": "^5.2.4", + "@inquirer/search": "^4.1.4", + "@inquirer/select": "^5.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.4.tgz", + "integrity": "sha512-fTuJ5Cq9W286isLxwj6GGyfTjx1Zdk4qppVEPexFuA6yioCCXS4V1zfKroQqw7QdbDPN73xs2DiIAlo55+kBqg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.4.tgz", + "integrity": "sha512-9yPTxq7LPmYjrGn3DRuaPuPbmC6u3fiWcsE9ggfLcdgO/ICHYgxq7mEy1yJ39brVvgXhtOtvDVjDh9slJxE4LQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.5", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.0.tgz", + "integrity": "sha512-OyYbKnchS1u+zRe14LpYrN8S0wH1vD0p2yKISvSsJdH2TpI87fh4eZdWnpdbrGauCRWDph3NwxRmM4Pcm/hx1Q==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.5", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.3.tgz", + "integrity": "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bun": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.10.tgz", + "integrity": "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.10" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bun-types": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.10.tgz", + "integrity": "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-13.3.0.tgz", + "integrity": "sha512-APTrZe9IhrsshL0u2PgmEMLP3CXDBjZ99xh5dR2+sryOt5R+JGL0KNuaTTT2lW54B9eNQDMutPR05UYTL7Xb1Q==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.5", + "@inquirer/prompts": "^8.3.0", + "@inquirer/type": "^4.0.3", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "license": "MIT", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sqlite-vec": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.7-alpha.2.tgz", + "integrity": "sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.7-alpha.2", + "sqlite-vec-darwin-x64": "0.1.7-alpha.2", + "sqlite-vec-linux-arm64": "0.1.7-alpha.2", + "sqlite-vec-linux-x64": "0.1.7-alpha.2", + "sqlite-vec-windows-x64": "0.1.7-alpha.2" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-jeZEELsQjjRsVojsvU5iKxOvkaVuE+JYC8Y4Ma8U45aAERrDYmqZoHvgSG7cg1PXL3bMlumFTAmHynf1y4pOzA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.7-alpha.2.tgz", + "integrity": "sha512-6Spj4Nfi7tG13jsUG+W7jnT0bCTWbyPImu2M8nWp20fNrd1SZ4g3CSlDAK8GBdavX7wRlbBHCZ+BDa++rbDewA==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-IcgrbHaDccTVhXDf8Orwdc2+hgDLAFORl6OBUhcvlmwswwBP1hqBTSEhovClG4NItwTOBNgpwOoQ7Qp3VDPWLg==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b39363d --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..449b749 --- /dev/null +++ b/publish.sh @@ -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 diff --git a/src/commands/backup.ts b/src/commands/backup.ts new file mode 100644 index 0000000..7a3d80b --- /dev/null +++ b/src/commands/backup.ts @@ -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(); + }); +} diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..ad7e89b --- /dev/null +++ b/src/commands/index.ts @@ -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); diff --git a/src/commands/restore.ts b/src/commands/restore.ts new file mode 100644 index 0000000..fc8ad0b --- /dev/null +++ b/src/commands/restore.ts @@ -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(); + } + }); +} diff --git a/src/commands/schema.ts b/src/commands/schema.ts new file mode 100644 index 0000000..2e543ae --- /dev/null +++ b/src/commands/schema.ts @@ -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(); + }); +} diff --git a/src/commands/typedef.ts b/src/commands/typedef.ts new file mode 100644 index 0000000..65dd52f --- /dev/null +++ b/src/commands/typedef.ts @@ -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(); + }); +} diff --git a/src/data/app-data.ts b/src/data/app-data.ts new file mode 100644 index 0000000..63108af --- /dev/null +++ b/src/data/app-data.ts @@ -0,0 +1,5 @@ +export const AppData = { + ConfigFileName: "bun-sqlite.config.ts", + MaxBackups: 10, + DefaultBackupDirName: ".backups", +} as const; diff --git a/src/data/grab-dir-names.ts b/src/data/grab-dir-names.ts new file mode 100644 index 0000000..8e65d2f --- /dev/null +++ b/src/data/grab-dir-names.ts @@ -0,0 +1,9 @@ +import path from "path"; + +export default function grabDirNames() { + const ROOT_DIR = process.cwd(); + + return { + ROOT_DIR, + }; +} diff --git a/src/functions/init.ts b/src/functions/init.ts new file mode 100644 index 0000000..7c61093 --- /dev/null +++ b/src/functions/init.ts @@ -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 { + 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); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f7996a8 --- /dev/null +++ b/src/index.ts @@ -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; diff --git a/src/lib/sqlite/db-delete.ts b/src/lib/sqlite/db-delete.ts new file mode 100644 index 0000000..690ddb6 --- /dev/null +++ b/src/lib/sqlite/db-delete.ts @@ -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; + targetId?: number | string; +}; + +export default async function DbDelete< + Schema extends { [k: string]: any } = { [k: string]: any }, + Table extends string = string, +>({ + table, + query, + targetId, +}: Params): Promise { + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + 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, + }; + } +} diff --git a/src/lib/sqlite/db-generate-type-defs.ts b/src/lib/sqlite/db-generate-type-defs.ts new file mode 100644 index 0000000..c05d633 --- /dev/null +++ b/src/lib/sqlite/db-generate-type-defs.ts @@ -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 }; +} diff --git a/src/lib/sqlite/db-insert.ts b/src/lib/sqlite/db-insert.ts new file mode 100644 index 0000000..d8e57a2 --- /dev/null +++ b/src/lib/sqlite/db-insert.ts @@ -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): Promise { + 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, + }; + } +} diff --git a/src/lib/sqlite/db-schema-manager.ts b/src/lib/sqlite/db-schema-manager.ts new file mode 100644 index 0000000..a9aec63 --- /dev/null +++ b/src/lib/sqlite/db-schema-manager.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 }; diff --git a/src/lib/sqlite/db-schema-to-typedef.ts b/src/lib/sqlite/db-schema-to-typedef.ts new file mode 100644 index 0000000..978d216 --- /dev/null +++ b/src/lib/sqlite/db-schema-to-typedef.ts @@ -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]; +} diff --git a/src/lib/sqlite/db-select.ts b/src/lib/sqlite/db-select.ts new file mode 100644 index 0000000..da999b0 --- /dev/null +++ b/src/lib/sqlite/db-select.ts @@ -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; + 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): Promise> { + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + 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: APIResponseObject = { + 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, + }; + } +} diff --git a/src/lib/sqlite/db-sql.ts b/src/lib/sqlite/db-sql.ts new file mode 100644 index 0000000..f345f77 --- /dev/null +++ b/src/lib/sqlite/db-sql.ts @@ -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> { + 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, + }; + } +} diff --git a/src/lib/sqlite/db-update.ts b/src/lib/sqlite/db-update.ts new file mode 100644 index 0000000..10be70a --- /dev/null +++ b/src/lib/sqlite/db-update.ts @@ -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; + 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): Promise { + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + 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, + }; + } +} diff --git a/src/lib/sqlite/index.ts b/src/lib/sqlite/index.ts new file mode 100644 index 0000000..e8026b8 --- /dev/null +++ b/src/lib/sqlite/index.ts @@ -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; diff --git a/src/lib/sqlite/schema-to-typedef.ts b/src/lib/sqlite/schema-to-typedef.ts new file mode 100644 index 0000000..9a1e79f --- /dev/null +++ b/src/lib/sqlite/schema-to-typedef.ts @@ -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); + } +} diff --git a/src/lib/sqlite/schema.ts b/src/lib/sqlite/schema.ts new file mode 100644 index 0000000..7a12a02 --- /dev/null +++ b/src/lib/sqlite/schema.ts @@ -0,0 +1,7 @@ +import _ from "lodash"; +import type { BUN_SQLITE_DatabaseSchemaType } from "../../types"; + +export const DbSchema: BUN_SQLITE_DatabaseSchemaType = { + dbName: "travis-ai", + tables: [], +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e194abb --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,1193 @@ +import type { RequestOptions } from "https"; + +export type BUN_SQLITE_DatabaseFullName = string; + +export const UsersOmitedFields = [ + "password", + "social_id", + "verification_status", + "date_created", + "date_created_code", + "date_created_timestamp", + "date_updated", + "date_updated_code", + "date_updated_timestamp", +] as const; + +export interface BUN_SQLITE_DatabaseSchemaType { + id?: string | number; + dbName?: string; + dbSlug?: string; + dbFullName?: string; + dbDescription?: string; + dbImage?: string; + tables: BUN_SQLITE_TableSchemaType[]; + childrenDatabases?: BUN_SQLITE_ChildrenDatabaseObject[]; + childDatabase?: boolean; + childDatabaseDbId?: string | number; + updateData?: boolean; + collation?: (typeof MariaDBCollations)[number]; +} + +export interface BUN_SQLITE_ChildrenDatabaseObject { + dbId?: string | number; +} + +export const MariaDBCollations = [ + "utf8mb4_bin", + "utf8mb4_unicode_520_ci", +] as const; + +export interface BUN_SQLITE_TableSchemaType { + id?: string | number; + tableName: string; + tableDescription?: string; + fields: BUN_SQLITE_FieldSchemaType[]; + indexes?: BUN_SQLITE_IndexSchemaType[]; + uniqueConstraints?: BUN_SQLITE_UniqueConstraintSchemaType[]; + childrenTables?: BUN_SQLITE_ChildrenTablesType[]; + /** + * Whether this is a child table + */ + childTable?: boolean; + updateData?: boolean; + /** + * ID of the parent table + */ + childTableId?: string | number; + /** + * ID of the parent table + */ + parentTableId?: string | number; + /** + * ID of the Database of parent table + */ + parentTableDbId?: string | number; + /** + * Name of the Database of parent table + */ + parentTableDbName?: string; + /** + * Name of the parent table + */ + parentTableName?: string; + tableNameOld?: string; + /** + * ID of the Database of parent table + */ + childTableDbId?: string | number; + collation?: (typeof MariaDBCollations)[number]; + isVector?: boolean; + vectorType?: string; +} + +export interface BUN_SQLITE_ChildrenTablesType { + tableId?: string | number; + dbId?: string | number; +} + +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" }, +] as const; + +export const BUN_SQLITE_DATATYPES = [ + { value: "TEXT" }, + { value: "INTEGER" }, +] as const; + +export type BUN_SQLITE_FieldSchemaType = { + id?: number | string; + fieldName?: string; + fieldDescription?: string; + originName?: string; + updatedField?: boolean; + dataType: (typeof BUN_SQLITE_DATATYPES)[number]["value"]; + nullValue?: boolean; + notNullValue?: boolean; + primaryKey?: boolean; + encrypted?: boolean; + autoIncrement?: boolean; + defaultValue?: string | number; + defaultValueLiteral?: string; + foreignKey?: BUN_SQLITE_ForeignKeyType; + defaultField?: boolean; + plainText?: boolean; + unique?: boolean; + pattern?: string; + patternFlags?: string; + onUpdate?: string; + onUpdateLiteral?: string; + onDelete?: string; + onDeleteLiteral?: string; + cssFiles?: string[]; + integerLength?: string | number; + decimals?: string | number; + code?: boolean; + options?: (string | number)[]; + isVector?: boolean; + vectorSize?: number; + /** + * ### Adds a `+` prefix to colums + * In sqlite-vec, the + prefix is a specialized syntax for Virtual Table Columns. It essentially tells the database: "Keep this data associated with the vector, but don't try to index it for math." +Here is the breakdown of why they matter and how they work: +1. Performance Separation +In a standard table, adding a massive TEXT column (like a 2,000-word article) slows down full-table scans. In a vec0 virtual table, columns prefixed with + are stored in a separate internal side-car table. +The Vector Index: Stays lean and fast for "Nearest Neighbor" math. +The Content: Is only fetched after the vector search identifies the winning rows. +2. The "No Join" Convenience +Normally, you would store vectors in one table and the actual text content in another, linking them with a FOREIGN KEY. +Without + columns: You must JOIN two tables to get the text after finding the vector. +With + columns: You can SELECT content directly from the virtual table. It handles the "join" logic internally, making your code cleaner. +3. Syntax Example +When defining your schema, the + is only used in the CREATE statement. When querying or inserting, you treat it like a normal name. +```sql +-- SCHEMA DEFINITION +CREATE VIRTUAL TABLE documents USING vec0( + embedding float, -- The vector (indexed) + +title TEXT, -- Side-car metadata (not indexed) + +raw_body TEXT -- Side-car "heavy" data (not indexed) +); + +-- INSERTING (Notice: No '+' here) +INSERT INTO documents(embedding, title, raw_body) +VALUES (vec_f32(?), 'Bun Docs', 'Bun is a fast JavaScript runtime...'); + +-- QUERYING (Notice: No '+' here) +SELECT title, raw_body +FROM documents +WHERE embedding MATCH ? AND k = 1; +``` + */ + sideCar?: boolean; +} & { + [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; +}; + +export interface BUN_SQLITE_ForeignKeyType { + foreignKeyName?: string; + destinationTableName?: string; + destinationTableColumnName?: string; + destinationTableColumnType?: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} + +export interface BUN_SQLITE_IndexSchemaType { + id?: string | number; + indexName?: string; + indexType?: (typeof IndexTypes)[number]; + indexTableFields?: BUN_SQLITE_IndexTableFieldType[]; + alias?: string; + newTempIndex?: boolean; +} + +export interface BUN_SQLITE_UniqueConstraintSchemaType { + id?: string | number; + constraintName?: string; + alias?: string; + constraintTableFields?: BUN_SQLITE_UniqueConstraintFieldType[]; +} + +export interface BUN_SQLITE_UniqueConstraintFieldType { + value: string; +} + +export interface BUN_SQLITE_IndexTableFieldType { + value: string; + dataType: string; +} + +export interface BUN_SQLITE_MYSQL_SHOW_INDEXES_Type { + Key_name: string; + Table: string; + Column_name: string; + Collation: string; + Index_type: string; + Cardinality: string; + Index_comment: string; + Comment: string; +} + +export interface BUN_SQLITE_MYSQL_SHOW_COLUMNS_Type { + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; +} + +export interface BUN_SQLITE_MARIADB_SHOW_INDEXES_TYPE { + Table: string; + Non_unique: 0 | 1; + Key_name: string; + Seq_in_index: number; + Column_name: string; + Collation: string; + Cardinality: number; + Sub_part?: string; + Packed?: string; + Index_type?: "BTREE"; + Comment?: string; + Index_comment?: string; + Ignored?: "YES" | "NO"; +} + +export interface BUN_SQLITE_MYSQL_FOREIGN_KEYS_Type { + CONSTRAINT_NAME: string; + CONSTRAINT_SCHEMA: string; + TABLE_NAME: string; +} + +export interface BUN_SQLITE_MYSQL_user_databases_Type { + id: number; + user_id: number; + db_full_name: string; + db_name: string; + db_slug: string; + db_image: string; + db_description: string; + active_clone: number; + active_data: 0 | 1; + active_clone_parent_db: string; + remote_connected?: number; + remote_db_full_name?: string; + remote_connection_host?: string; + remote_connection_key?: string; + remote_connection_type?: string; + user_priviledge?: string; + date_created?: string; + image_thumbnail?: string; + first_name?: string; + last_name?: string; + email?: string; +} + +export type ImageInputFileToBase64FunctionReturn = { + imageBase64?: string; + imageBase64Full?: string; + imageName?: string; + imageSize?: number; +}; + +export interface GetReqQueryObject { + db: string; + query: string; + queryValues?: string; + tableName?: string; + debug?: boolean; +} + +export type DATASQUIREL_LoggedInUser = { + id: number; + uuid?: string; + first_name: string; + last_name: string; + email: string; + phone?: string; + user_type?: string; + username?: string; + image?: string; + image_thumbnail?: string; + social_login?: number; + social_platform?: string; + social_id?: string; + verification_status?: number; + csrf_k: string; + logged_in_status: boolean; + date: number; +} & { + [key: string]: any; +}; + +export interface AuthenticatedUser { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + cookieNames?: any; +} + +export interface SuccessUserObject { + id: number; + first_name: string; + last_name: string; + email: string; +} + +export interface AddUserFunctionReturn { + success: boolean; + payload?: SuccessUserObject | null; + msg?: string; + sqlResult?: any; +} + +export interface GoogleIdentityPromptNotification { + getMomentType: () => string; + getDismissedReason: () => string; + getNotDisplayedReason: () => string; + getSkippedReason: () => string; + isDismissedMoment: () => boolean; + isDisplayMoment: () => boolean; + isDisplayed: () => boolean; + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; +} + +export type UserDataPayload = { + first_name: string; + last_name: string; + email: string; + password?: string; + username?: string; +} & { + [key: string]: any; +}; + +export interface GetUserFunctionReturn { + success: boolean; + payload: { + id: number; + first_name: string; + last_name: string; + username: string; + email: string; + phone: string; + social_id: [string]; + image: string; + image_thumbnail: string; + verification_status: [number]; + } | null; +} + +export interface ReauthUserFunctionReturn { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + token?: string; +} + +export interface UpdateUserFunctionReturn { + success: boolean; + payload?: Object[] | string; +} + +export interface GetReturn { + success: boolean; + payload?: R; + msg?: string; + error?: string; + schema?: BUN_SQLITE_TableSchemaType; + finalQuery?: string; +} + +export interface GetSchemaRequestQuery { + database?: string; + table?: string; + field?: string; + user_id?: string | number; + env?: { [k: string]: string }; +} + +export interface GetSchemaAPICredentialsParam { + key: string; +} + +export type GetSchemaAPIParam = GetSchemaRequestQuery & + GetSchemaAPICredentialsParam; + +export interface PostReturn { + success: boolean; + payload?: Object[] | string | PostInsertReturn; + msg?: string; + error?: any; + schema?: BUN_SQLITE_TableSchemaType; +} + +export interface PostDataPayload { + action: "insert" | "update" | "delete"; + table: string; + data?: object; + identifierColumnName?: string; + identifierValue?: string; + duplicateColumnName?: string; + duplicateColumnValue?: string; + update?: boolean; +} + +export interface LocalPostReturn { + success: boolean; + payload?: any; + msg?: string; + error?: string; +} + +export interface LocalPostQueryObject { + query: string | PostDataPayload; + tableName?: string; + queryValues?: string[]; +} + +export interface PostInsertReturn { + fieldCount?: number; + affectedRows?: number; + insertId?: number; + serverStatus?: number; + warningCount?: number; + message?: string; + protocol41?: boolean; + changedRows?: number; + error?: string; +} + +export type UserType = DATASQUIREL_LoggedInUser & { + isSuperUser?: boolean; + staticHost?: string; + appHost?: string; + appName?: string; +}; + +export interface ApiKeyDef { + name: string; + scope: string; + date_created: string; + apiKeyPayload: string; +} + +export interface MetricsType { + dbCount: number; + tablesCount: number; + mediaCount: number; + apiKeysCount: number; +} + +export interface MYSQL_mariadb_users_table_def { + id?: number; + user_id?: number; + username?: string; + host?: string; + password?: string; + primary?: number; + grants?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} + +export interface MariaDBUserCredType { + mariadb_user?: string; + mariadb_host?: string; + mariadb_pass?: string; +} + +export const ServerQueryOperators = ["AND", "OR"] as const; +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", +] as const; + +export type ServerQueryParam< + T extends { [k: string]: any } = { [k: string]: any }, + K extends string = string, +> = { + selectFields?: (keyof T | TableSelectFieldsObject)[]; + omitFields?: (keyof T)[]; + query?: ServerQueryQueryObject; + limit?: number; + page?: number; + offset?: number; + order?: ServerQueryParamOrder | ServerQueryParamOrder[]; + searchOperator?: (typeof ServerQueryOperators)[number]; + searchEquality?: (typeof ServerQueryEqualities)[number]; + addUserId?: { + fieldName: keyof T; + }; + join?: ( + | ServerQueryParamsJoin + | ServerQueryParamsJoin[] + | undefined + )[]; + group?: + | keyof T + | ServerQueryParamGroupBy + | (keyof T | ServerQueryParamGroupBy)[]; + countSubQueries?: ServerQueryParamsCount[]; + fullTextSearch?: ServerQueryParamFullTextSearch; + [key: string]: any; +}; + +export type ServerQueryParamGroupBy< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + field: keyof T; + table?: string; +}; + +export type ServerQueryParamOrder< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + field: keyof T; + strategy: "ASC" | "DESC"; +}; + +export type ServerQueryParamFullTextSearch< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + fields: (keyof T)[]; + searchTerm: string; + /** Field Name to user to Rank the Score of Search Results */ + scoreAlias: string; +}; + +export type ServerQueryParamsCount = { + table: string; + /** Alias for the Table From which the count is fetched */ + table_alias?: string; + srcTrgMap: { + src: string; + trg: string | ServerQueryParamsCountSrcTrgMap; + }[]; + alias: string; +}; + +export type ServerQueryParamsCountSrcTrgMap = { + table: string; + field: string; +}; + +export type TableSelectFieldsObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + fieldName: keyof T; + alias?: string; +}; + +export type ServerQueryValuesObject = { + value?: string | number; + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: string; + fieldName?: string; +}; + +export type ServerQueryObjectValue = + | string + | (string | ServerQueryValuesObject | undefined | null) + | (string | ServerQueryValuesObject | undefined | null)[]; + +export type ServerQueryObject< + T extends object = { [key: string]: any }, + K extends string = string, +> = { + value?: ServerQueryObjectValue; + nullValue?: boolean; + notNullValue?: boolean; + operator?: (typeof ServerQueryOperators)[number]; + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: K; + /** + * This will replace the top level field name if + * provided + */ + fieldName?: string; + __query?: { + [key in keyof T]: Omit, "__query">; + }; + vector?: boolean; + /** + * ### The Function to be used to generate the vector. + * Eg. `vec_f32`. This will come out as `vec_f32(?)` + * instead of just `?` + */ + vectorFunction?: string; +}; + +export type ServerQueryQueryObject< + T extends object = { [key: string]: any }, + K extends string = string, +> = { + [key in keyof T]: ServerQueryObject; +}; + +export type FetchDataParams = { + path: string; + method?: (typeof DataCrudRequestMethods)[number]; + body?: object | string; + query?: AuthFetchQuery; + tableName?: string; +}; + +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: any; +}; + +export type ServerQueryParamsJoin< + Table extends string = string, + Field extends object = { [key: string]: any }, +> = { + joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; + alias?: string; + tableName: Table; + match?: + | ServerQueryParamsJoinMatchObject + | ServerQueryParamsJoinMatchObject[]; + selectFields?: ( + | keyof Field + | { + field: keyof Field; + alias?: string; + count?: boolean; + } + )[]; + omitFields?: ( + | keyof Field + | { + field: keyof Field; + alias?: string; + count?: boolean; + } + )[]; + operator?: (typeof ServerQueryOperators)[number]; +}; + +export type ServerQueryParamsJoinMatchObject< + Field extends object = { [key: string]: any }, +> = { + /** Field name from the **Root Table** */ + source?: string | ServerQueryParamsJoinMatchSourceTargetObject; + /** Field name from the **Join Table** */ + target?: keyof Field | ServerQueryParamsJoinMatchSourceTargetObject; + /** A literal value: No source and target Needed! */ + targetLiteral?: string | number; + __batch?: { + matches: Omit, "__batch">[]; + operator: "AND" | "OR"; + }; +}; + +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; + +export type ApiConnectBody = { + url: string; + key: string; + database: BUN_SQLITE_MYSQL_user_databases_Type; + dbSchema: BUN_SQLITE_DatabaseSchemaType; + type: "pull" | "push"; + user_id?: string | number; +}; + +export type SuUserType = { + email: string; + password: string; + authKey: string; + logged_in_status: boolean; + date: number; +}; + +export type MariadbRemoteServerObject = { + host: string; + port: number; + primary?: boolean; + loadBalanced?: boolean; + users?: MariadbRemoteServerUserObject[]; +}; + +export type MariadbRemoteServerUserObject = { + name: string; + password: string; + host: string; +}; + +export type APILoginFunctionReturn = { + success: boolean; + msg?: string; + payload?: DATASQUIREL_LoggedInUser | null; + userId?: number | string; + key?: string; + token?: string; + csrf?: string; + cookieNames?: any; +}; + +export type APICreateUserFunctionParams = { + encryptionKey?: string; + payload: any; + database: string; + dsqlUserID?: string | number; + verify?: boolean; +}; + +export type APICreateUserFunction = ( + params: APICreateUserFunctionParams, +) => Promise; + +export type HandleSocialDbFunctionReturn = { + success: boolean; + user?: DATASQUIREL_LoggedInUser | null; + msg?: string; + social_id?: string | number; + social_platform?: string; + payload?: any; + alert?: boolean; + newUser?: any; + error?: any; +} | null; + +export type CookieObject = { + name: string; + value: string; + domain?: string; + path?: string; + expires?: Date; + maxAge?: number; + secure?: boolean; + httpOnly?: boolean; + sameSite?: "Strict" | "Lax" | "None"; + priority?: "Low" | "Medium" | "High"; +}; + +export type HttpRequestParams< + ReqObj extends { [k: string]: any } = { [k: string]: any }, +> = RequestOptions & { + scheme?: "http" | "https"; + body?: ReqObj; + query?: ReqObj; + urlEncodedFormBody?: boolean; +}; + +export type HttpRequestFunction< + ReqObj extends { [k: string]: any } = { [k: string]: any }, + ResObj extends { [k: string]: any } = { [k: string]: any }, +> = (param: HttpRequestParams) => Promise>; + +export type HttpFunctionResponse< + ResObj extends { [k: string]: any } = { [k: string]: any }, +> = { + status: number; + data?: ResObj; + error?: string; + str?: string; + requestedPath?: string; +}; + +export type ApiGetQueryObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + query: ServerQueryParam; + table: string; + dbFullName?: string; +}; + +export const DataCrudRequestMethods = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", +] as const; + +export const DataCrudRequestMethodsLowerCase = [ + "get", + "post", + "put", + "patch", + "delete", + "options", +] as const; + +export type DsqlMethodCrudParam< + T extends { [key: string]: any } = { [key: string]: any }, +> = { + method: (typeof DataCrudRequestMethods)[number]; + body?: T; + query?: DsqlCrudQueryObject; + tableName: string; + addUser?: { + field: keyof T; + }; + user?: DATASQUIREL_LoggedInUser; + extraData?: T; + transformData?: DsqlCrudTransformDataFunction; + transformQuery?: DsqlCrudTransformQueryFunction; + existingData?: T; + targetId?: string | number; + sanitize?: ({ data, batchData }: { data?: T; batchData?: T[] }) => T | T[]; + debug?: boolean; +}; + +export type DsqlCrudTransformDataFunction< + T extends { [key: string]: any } = { [key: string]: any }, +> = (params: { + data: T; + user?: DATASQUIREL_LoggedInUser; + existingData?: T; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise; + +export type DsqlCrudTransformQueryFunction< + T extends { [key: string]: any } = { [key: string]: any }, +> = (params: { + query: DsqlCrudQueryObject; + user?: DATASQUIREL_LoggedInUser; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise>; + +export const DsqlCrudActions = ["insert", "update", "delete", "get"] as const; + +export type DsqlCrudQueryObject< + T extends { [key: string]: any } = { [key: string]: any }, + K extends string = string, +> = ServerQueryParam & { + query?: ServerQueryQueryObject; +}; + +export type SQLDeleteGeneratorParams< + T extends { [key: string]: any } = { [key: string]: any }, +> = { + tableName: string; + deleteKeyValues?: SQLDeleteData[]; + deleteKeyValuesOperator?: "AND" | "OR"; + dbFullName?: string; + data?: any; +}; + +export type SQLDeleteData< + T extends { [key: string]: any } = { [key: string]: any }, +> = { + key: keyof T; + value: string | number | null | undefined; + operator?: (typeof ServerQueryEqualities)[number]; +}; + +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; + +export type ErrorCallback = (title: string, error: Error, data?: any) => void; + +export interface MariaDBUser { + Host?: string; + User?: string; + Password?: string; + Select_priv?: string; + Insert_priv?: string; + Update_priv?: string; + Delete_priv?: string; + Create_priv?: string; + Drop_priv?: string; + Reload_priv?: string; + Shutdown_priv?: string; + Process_priv?: string; + File_priv?: string; + Grant_priv?: string; + References_priv?: string; + Index_priv?: string; + Alter_priv?: string; + Show_db_priv?: string; + Super_priv?: string; + Create_tmp_table_priv?: string; + Lock_tables_priv?: string; + Execute_priv?: string; + Repl_slave_priv?: string; + Repl_client_priv?: string; + Create_view_priv?: string; + Show_view_priv?: string; + Create_routine_priv?: string; + Alter_routine_priv?: string; + Create_user_priv?: string; + Event_priv?: string; + Trigger_priv?: string; + Create_tablespace_priv?: string; + Delete_history_priv?: string; + ssl_type?: string; + ssl_cipher?: string; + x509_issuer?: string; + x509_subject?: string; + max_questions?: number; + max_updates?: number; + max_connections?: number; + max_user_connections?: number; + plugin?: string; + authentication_string?: string; + password_expired?: string; + is_role?: string; + default_role?: string; + max_statement_time?: number; +} + +export const QueryFields = [ + "duplicate", + "user_id", + "delegated_user_id", + "db_id", + "table_id", + "db_slug", +] as const; + +export type LocalFolderType = { + name: string; + isPrivate: boolean; +}; + +export type ResponseQueryObject = { + sql?: string; + params?: (string | number)[]; +}; + +export type APIResponseObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + success: boolean; + payload?: T[] | null; + singleRes?: T | null; + stringRes?: string | null; + numberRes?: number | null; + postInsertReturn?: PostInsertReturn | null; + payloadBase64?: string; + payloadThumbnailBase64?: string; + payloadURL?: string; + payloadThumbnailURL?: string; + error?: any; + msg?: string; + queryObject?: ResponseQueryObject; + countQueryObject?: ResponseQueryObject; + status?: number; + count?: number; + errors?: BUNSQLITEErrorObject[]; + debug?: any; + batchPayload?: any[][] | null; + errorData?: any; + token?: string; + csrf?: string; + cookieNames?: any; + key?: string; + userId?: string | number; + code?: string; + createdAt?: number; + email?: string; + requestOptions?: RequestOptions; + logoutUser?: boolean; + redirect?: string; +}; + +/** + * # Docker Compose Types + */ +export type DockerCompose = { + services: DockerComposeServicesType; + networks: DockerComposeNetworks; + name: string; +}; + +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", +] as const; + +export type DockerComposeServicesType = { + [key in (typeof DockerComposeServices)[number]]: DockerComposeServiceWithBuildObject; +}; + +export type DockerComposeNetworks = { + [k: string]: { + driver?: "bridge"; + ipam?: { + config: DockerComposeNetworkConfigObject[]; + }; + external?: boolean; + }; +}; + +export type DockerComposeNetworkConfigObject = { + subnet: string; + gateway: string; +}; + +export type DockerComposeServiceWithBuildObject = { + build: DockerComposeServicesBuildObject; + env_file: string; + container_name: string; + hostname: string; + volumes: string[]; + environment: string[]; + ports?: string[]; + networks?: DockerComposeServiceNetworkObject; + restart?: string; + depends_on?: { + [k: string]: { + condition: string; + }; + }; + user?: string; +}; + +export type DockerComposeServiceWithImage = Omit< + DockerComposeServiceWithBuildObject, + "build" +> & { + image: string; +}; + +export type DockerComposeServicesBuildObject = { + context: string; + dockerfile: string; +}; + +export type DockerComposeServiceNetworkObject = { + [k: string]: { + ipv4_address: string; + }; +}; + +export type ClonedTableInfo = { + dbId?: string | number; + tableId?: string | number; + keepUpdated?: boolean; + keepDataUpdated?: boolean; +}; + +export type DefaultEntryType = { + id?: number; + uuid?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} & { + [k: string]: string | number | null; +}; + +export const IndexTypes = ["regular", "full_text", "vector"] as const; + +export type BUNSQLITEErrorObject = { + sql?: string; + sqlValues?: any[]; + error?: string; +}; + +export interface SQLInsertGenReturn { + query: string; + values: (string | number)[]; +} + +export type SQLInsertGenDataFn = () => { + placeholder: string; + value: string | number | Float32Array; +}; + +export type SQLInsertGenDataType = { + [k: string]: string | number | SQLInsertGenDataFn | undefined | null; +}; + +export type SQLInsertGenParams = { + data: SQLInsertGenDataType[]; + tableName: string; + dbFullName?: string; +}; + +export type BunSQLiteConfig = { + db_name: string; + /** + * The Name of the Database Schema File. Eg `db_schema.ts`. This is + * relative to `db_dir`, or root dir if `db_dir` is not provided + */ + db_schema_file_name: string; + /** + * The Directory for backups. Relative to db_dir. + */ + db_backup_dir?: string; + max_backups?: number; + /** + * The Root Directory for the DB file and schema + */ + db_dir?: string; + /** + * The File Path relative to the root(working) directory for the type + * definition export. Example `db_types.ts` or `types/db_types.ts` + */ + typedef_file_path?: string; +}; + +export type BunSQLiteConfigReturn = { + config: BunSQLiteConfig; + dbSchema: BUN_SQLITE_DatabaseSchemaType; +}; + +export const DefaultFields: BUN_SQLITE_FieldSchemaType[] = [ + { + 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)", + }, +]; diff --git a/src/utils/append-default-fields-to-db-schema.ts b/src/utils/append-default-fields-to-db-schema.ts new file mode 100644 index 0000000..7e7c769 --- /dev/null +++ b/src/utils/append-default-fields-to-db-schema.ts @@ -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; +} diff --git a/src/utils/grab-backup-data.ts b/src/utils/grab-backup-data.ts new file mode 100644 index 0000000..a44109e --- /dev/null +++ b/src/utils/grab-backup-data.ts @@ -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 }; +} diff --git a/src/utils/grab-db-backup-file-name.ts b/src/utils/grab-db-backup-file-name.ts new file mode 100644 index 0000000..7580cf3 --- /dev/null +++ b/src/utils/grab-db-backup-file-name.ts @@ -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; +} diff --git a/src/utils/grab-db-dir.ts b/src/utils/grab-db-dir.ts new file mode 100644 index 0000000..b668157 --- /dev/null +++ b/src/utils/grab-db-dir.ts @@ -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 }; +} diff --git a/src/utils/grab-sorted-backups.ts b/src/utils/grab-sorted-backups.ts new file mode 100644 index 0000000..aa8fb4f --- /dev/null +++ b/src/utils/grab-sorted-backups.ts @@ -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; +} diff --git a/src/utils/sql-equality-parser.ts b/src/utils/sql-equality-parser.ts new file mode 100644 index 0000000..b5c4358 --- /dev/null +++ b/src/utils/sql-equality-parser.ts @@ -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 "="; + } +} diff --git a/src/utils/sql-gen-operator-gen.ts b/src/utils/sql-gen-operator-gen.ts new file mode 100644 index 0000000..02fcc35 --- /dev/null +++ b/src/utils/sql-gen-operator-gen.ts @@ -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, + }; + } + } +} diff --git a/src/utils/sql-generator.ts b/src/utils/sql-generator.ts new file mode 100644 index 0000000..7f7cfc6 --- /dev/null +++ b/src/utils/sql-generator.ts @@ -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 = { + genObject?: ServerQueryParam; + 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): 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) { + 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, + }; +} diff --git a/src/utils/sql-insert-generator.ts b/src/utils/sql-insert-generator.ts new file mode 100644 index 0000000..5b60de4 --- /dev/null +++ b/src/utils/sql-insert-generator.ts @@ -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; + } +} diff --git a/src/utils/trim-backups.ts b/src/utils/trim-backups.ts new file mode 100644 index 0000000..d53b653 --- /dev/null +++ b/src/utils/trim-backups.ts @@ -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); + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e8f4da1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "declaration": true, + "resolveJsonModule": true, + "maxNodeModuleJsDepth": 10, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}