From 9c8f6e20aa7a3fc5a93179aaa4c9ed0af27cdc46 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 21 Jun 2026 06:18:34 +0100 Subject: [PATCH] First Commit. Based off bun-sqlite --- .gitignore | 35 + .npmrc | 2 + CLAUDE.md | 1 + README.md | 802 +++++++++ bun.lock | 141 ++ dist/commands/admin/index.d.ts | 2 + dist/commands/admin/index.js | 41 + dist/commands/admin/list-tables.d.ts | 3 + dist/commands/admin/list-tables.js | 78 + dist/commands/admin/run-sql.d.ts | 3 + dist/commands/admin/run-sql.js | 26 + dist/commands/admin/show-entries.d.ts | 5 + dist/commands/admin/show-entries.js | 83 + dist/commands/admin/show-fields.d.ts | 5 + dist/commands/admin/show-fields.js | 69 + dist/commands/backup.d.ts | 2 + dist/commands/backup.js | 22 + dist/commands/index.d.ts | 6 + dist/commands/index.js | 33 + dist/commands/restore.d.ts | 2 + dist/commands/restore.js | 44 + dist/commands/schema.d.ts | 2 + dist/commands/schema.js | 54 + dist/commands/typedef.d.ts | 2 + dist/commands/typedef.js | 31 + dist/data/app-data.d.ts | 6 + dist/data/app-data.js | 6 + dist/data/grab-dir-names.d.ts | 12 + dist/data/grab-dir-names.js | 15 + dist/functions/init.d.ts | 2 + dist/functions/init.js | 46 + dist/functions/live-schema.d.ts | 7 + dist/functions/live-schema.js | 15 + dist/index.d.ts | 19 + dist/index.js | 19 + dist/lib/db-handler.d.ts | 20 + dist/lib/db-handler.js | 46 + dist/lib/grab-db-connection.d.ts | 6 + dist/lib/grab-db-connection.js | 15 + dist/lib/grab-db-ssl.d.ts | 7 + dist/lib/grab-db-ssl.js | 20 + dist/lib/grab-dsql-connection-config.d.ts | 6 + dist/lib/grab-dsql-connection-config.js | 29 + dist/lib/grab-duplicate-safe-insert-sql.d.ts | 7 + dist/lib/grab-duplicate-safe-insert-sql.js | 24 + dist/lib/mariadb/db-delete.d.ts | 16 + dist/lib/mariadb/db-delete.js | 56 + dist/lib/mariadb/db-generate-type-defs.d.ts | 15 + dist/lib/mariadb/db-generate-type-defs.js | 63 + dist/lib/mariadb/db-insert.d.ts | 16 + dist/lib/mariadb/db-insert.js | 43 + dist/lib/mariadb/db-schema-manager.d.ts | 39 + dist/lib/mariadb/db-schema-manager.js | 419 +++++ dist/lib/mariadb/db-schema-to-typedef.d.ts | 7 + dist/lib/mariadb/db-schema-to-typedef.js | 44 + dist/lib/mariadb/db-select.d.ts | 17 + dist/lib/mariadb/db-select.js | 58 + dist/lib/mariadb/db-sql.d.ts | 11 + dist/lib/mariadb/db-sql.js | 34 + dist/lib/mariadb/db-update.d.ts | 17 + dist/lib/mariadb/db-update.js | 74 + dist/lib/mariadb/schema-to-typedef.d.ts | 8 + dist/lib/mariadb/schema-to-typedef.js | 18 + dist/lib/mariadb/schema.d.ts | 2 + dist/lib/mariadb/schema.js | 5 + dist/types/index.d.ts | 1417 +++++++++++++++ dist/types/index.js | 187 ++ .../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-db-schema.d.ts | 1 + dist/utils/grab-db-schema.js | 5 + .../grab-join-fields-from-query-object.d.ts | 7 + .../grab-join-fields-from-query-object.js | 55 + dist/utils/grab-sorted-backups.d.ts | 6 + dist/utils/grab-sorted-backups.js | 18 + dist/utils/query-value-parser.d.ts | 6 + dist/utils/query-value-parser.js | 21 + dist/utils/sql-equality-parser.d.ts | 2 + dist/utils/sql-equality-parser.js | 41 + dist/utils/sql-gen-operator-gen.d.ts | 20 + dist/utils/sql-gen-operator-gen.js | 133 ++ dist/utils/sql-generator-gen-join-str.d.ts | 11 + dist/utils/sql-generator-gen-join-str.js | 65 + dist/utils/sql-generator-gen-query-str.d.ts | 22 + dist/utils/sql-generator-gen-query-str.js | 193 ++ dist/utils/sql-generator-gen-search-str.d.ts | 12 + dist/utils/sql-generator-gen-search-str.js | 92 + dist/utils/sql-generator-grab-concat-str.d.ts | 8 + dist/utils/sql-generator-grab-concat-str.js | 13 + .../sql-generator-grab-select-field-sql.d.ts | 16 + .../sql-generator-grab-select-field-sql.js | 55 + dist/utils/sql-generator.d.ts | 25 + dist/utils/sql-generator.js | 303 ++++ dist/utils/sql-insert-generator.d.ts | 5 + dist/utils/sql-insert-generator.js | 58 + dist/utils/trim-backups.d.ts | 6 + dist/utils/trim-backups.js | 19 + package.json | 42 + publish.sh | 17 + src/commands/admin/index.ts | 41 + src/commands/admin/list-tables.ts | 96 + src/commands/admin/run-sql.ts | 32 + src/commands/admin/show-entries.ts | 103 ++ src/commands/admin/show-fields.ts | 92 + src/commands/backup.ts | 28 + src/commands/index.ts | 66 + src/commands/restore.ts | 55 + src/commands/schema.ts | 73 + src/commands/typedef.ts | 40 + src/data/app-data.ts | 6 + src/data/grab-dir-names.ts | 28 + src/functions/init.ts | 73 + src/functions/live-schema.ts | 24 + src/index.ts | 40 + src/lib/db-handler.ts | 69 + src/lib/grab-db-connection.ts | 20 + src/lib/grab-db-ssl.ts | 28 + src/lib/grab-dsql-connection-config.ts | 35 + src/lib/grab-duplicate-safe-insert-sql.ts | 40 + src/lib/mariadb/db-delete.ts | 83 + src/lib/mariadb/db-generate-type-defs.ts | 117 ++ src/lib/mariadb/db-insert.ts | 67 + src/lib/mariadb/db-schema-manager.ts | 656 +++++++ src/lib/mariadb/db-schema-to-typedef.ts | 71 + src/lib/mariadb/db-select.ts | 95 + src/lib/mariadb/db-sql.ts | 44 + src/lib/mariadb/db-update.ts | 114 ++ src/lib/mariadb/schema-to-typedef.ts | 35 + src/lib/mariadb/schema.ts | 7 + src/types/index.ts | 1591 +++++++++++++++++ .../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-db-schema.ts | 6 + .../grab-join-fields-from-query-object.ts | 94 + src/utils/grab-sorted-backups.ts | 29 + src/utils/query-value-parser.ts | 33 + src/utils/sql-equality-parser.ts | 44 + src/utils/sql-gen-operator-gen.ts | 149 ++ src/utils/sql-generator-gen-join-str.ts | 109 ++ src/utils/sql-generator-gen-query-str.ts | 235 +++ src/utils/sql-generator-gen-search-str.ts | 123 ++ src/utils/sql-generator-grab-concat-str.ts | 30 + .../sql-generator-grab-select-field-sql.ts | 65 + src/utils/sql-generator.ts | 378 ++++ src/utils/sql-insert-generator.ts | 82 + src/utils/trim-backups.ts | 27 + tsconfig.json | 28 + 155 files changed, 11103 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 dist/commands/admin/index.d.ts create mode 100644 dist/commands/admin/index.js create mode 100644 dist/commands/admin/list-tables.d.ts create mode 100644 dist/commands/admin/list-tables.js create mode 100644 dist/commands/admin/run-sql.d.ts create mode 100644 dist/commands/admin/run-sql.js create mode 100644 dist/commands/admin/show-entries.d.ts create mode 100644 dist/commands/admin/show-entries.js create mode 100644 dist/commands/admin/show-fields.d.ts create mode 100644 dist/commands/admin/show-fields.js 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/functions/live-schema.d.ts create mode 100644 dist/functions/live-schema.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/lib/db-handler.d.ts create mode 100644 dist/lib/db-handler.js create mode 100644 dist/lib/grab-db-connection.d.ts create mode 100644 dist/lib/grab-db-connection.js create mode 100644 dist/lib/grab-db-ssl.d.ts create mode 100644 dist/lib/grab-db-ssl.js create mode 100644 dist/lib/grab-dsql-connection-config.d.ts create mode 100644 dist/lib/grab-dsql-connection-config.js create mode 100644 dist/lib/grab-duplicate-safe-insert-sql.d.ts create mode 100644 dist/lib/grab-duplicate-safe-insert-sql.js create mode 100644 dist/lib/mariadb/db-delete.d.ts create mode 100644 dist/lib/mariadb/db-delete.js create mode 100644 dist/lib/mariadb/db-generate-type-defs.d.ts create mode 100644 dist/lib/mariadb/db-generate-type-defs.js create mode 100644 dist/lib/mariadb/db-insert.d.ts create mode 100644 dist/lib/mariadb/db-insert.js create mode 100644 dist/lib/mariadb/db-schema-manager.d.ts create mode 100644 dist/lib/mariadb/db-schema-manager.js create mode 100644 dist/lib/mariadb/db-schema-to-typedef.d.ts create mode 100644 dist/lib/mariadb/db-schema-to-typedef.js create mode 100644 dist/lib/mariadb/db-select.d.ts create mode 100644 dist/lib/mariadb/db-select.js create mode 100644 dist/lib/mariadb/db-sql.d.ts create mode 100644 dist/lib/mariadb/db-sql.js create mode 100644 dist/lib/mariadb/db-update.d.ts create mode 100644 dist/lib/mariadb/db-update.js create mode 100644 dist/lib/mariadb/schema-to-typedef.d.ts create mode 100644 dist/lib/mariadb/schema-to-typedef.js create mode 100644 dist/lib/mariadb/schema.d.ts create mode 100644 dist/lib/mariadb/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-db-schema.d.ts create mode 100644 dist/utils/grab-db-schema.js create mode 100644 dist/utils/grab-join-fields-from-query-object.d.ts create mode 100644 dist/utils/grab-join-fields-from-query-object.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/query-value-parser.d.ts create mode 100644 dist/utils/query-value-parser.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-gen-join-str.d.ts create mode 100644 dist/utils/sql-generator-gen-join-str.js create mode 100644 dist/utils/sql-generator-gen-query-str.d.ts create mode 100644 dist/utils/sql-generator-gen-query-str.js create mode 100644 dist/utils/sql-generator-gen-search-str.d.ts create mode 100644 dist/utils/sql-generator-gen-search-str.js create mode 100644 dist/utils/sql-generator-grab-concat-str.d.ts create mode 100644 dist/utils/sql-generator-grab-concat-str.js create mode 100644 dist/utils/sql-generator-grab-select-field-sql.d.ts create mode 100644 dist/utils/sql-generator-grab-select-field-sql.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.json create mode 100755 publish.sh create mode 100644 src/commands/admin/index.ts create mode 100644 src/commands/admin/list-tables.ts create mode 100644 src/commands/admin/run-sql.ts create mode 100644 src/commands/admin/show-entries.ts create mode 100644 src/commands/admin/show-fields.ts 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/functions/live-schema.ts create mode 100644 src/index.ts create mode 100644 src/lib/db-handler.ts create mode 100644 src/lib/grab-db-connection.ts create mode 100644 src/lib/grab-db-ssl.ts create mode 100644 src/lib/grab-dsql-connection-config.ts create mode 100644 src/lib/grab-duplicate-safe-insert-sql.ts create mode 100644 src/lib/mariadb/db-delete.ts create mode 100644 src/lib/mariadb/db-generate-type-defs.ts create mode 100644 src/lib/mariadb/db-insert.ts create mode 100644 src/lib/mariadb/db-schema-manager.ts create mode 100644 src/lib/mariadb/db-schema-to-typedef.ts create mode 100644 src/lib/mariadb/db-select.ts create mode 100644 src/lib/mariadb/db-sql.ts create mode 100644 src/lib/mariadb/db-update.ts create mode 100644 src/lib/mariadb/schema-to-typedef.ts create mode 100644 src/lib/mariadb/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-db-schema.ts create mode 100644 src/utils/grab-join-fields-from-query-object.ts create mode 100644 src/utils/grab-sorted-backups.ts create mode 100644 src/utils/query-value-parser.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-gen-join-str.ts create mode 100644 src/utils/sql-generator-gen-query-str.ts create mode 100644 src/utils/sql-generator-gen-search-str.ts create mode 100644 src/utils/sql-generator-grab-concat-str.ts create mode 100644 src/utils/sql-generator-grab-select-field-sql.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..bacb9e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Default to using Bun instead of Node.js. diff --git a/README.md b/README.md new file mode 100644 index 0000000..83c48ce --- /dev/null +++ b/README.md @@ -0,0 +1,802 @@ +# Bun Mariadb + +@moduletrace/bun-mariadb + +A schema-driven MariaDB manager for [Bun](https://bun.sh), featuring automatic schema synchronization, type-safe CRUD operations, 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 +- **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 + +This works for just `bun`, so `bun` should be installed. + +--- + +## Installation + +There are two ways to install BunMariaDB: + +### Via the official npm registry + +`@moduletrace/bun-mariadb` 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/ +``` + +After this you can run + +```bash +bun add @moduletrace/bun-sqlite +``` + +### Via Github + +To install directly from github simply run + +```bash +bun add github: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_MARIADB_DatabaseSchemaType } from "@moduletrace/bun-sqlite"; + +const schema: BUN_MARIADB_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_MARIADB_DatabaseSchemaType { + dbName?: string; + tables: BUN_MARIADB_TableSchemaType[]; +} +``` + +### Table Schema + +```ts +interface BUN_MARIADB_TableSchemaType { + tableName: string; + tableDescription?: string; + fields: BUN_MARIADB_FieldSchemaType[]; + indexes?: BUN_MARIADB_IndexSchemaType[]; + uniqueConstraints?: BUN_MARIADB_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_MARIADB_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_MARIADB_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_MARIADB_ForeignKeyType { + destinationTableName: string; + destinationTableColumnName: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} +``` + +### Index + +```ts +interface BUN_MARIADB_IndexSchemaType { + indexName?: string; + indexType?: "regular" | "full_text" | "vector"; + indexTableFields?: { value: string; dataType: string }[]; +} +``` + +### Unique Constraint + +```ts +interface BUN_MARIADB_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_MARIADB__`) +- A union type `BUN_MARIADB__ALL_TYPEDEFS` + +**Example output** (`db/types/db.ts`): + +```ts +export const BunSQLiteTables = ["users"] as const; + +export type BUN_MARIADB_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_MARIADB_MY_APP_ALL_TYPEDEFS = BUN_MARIADB_MY_APP_USERS; +``` + +Use the generated types with the CRUD API for full type safety: + +```ts +import BunSQLite from "@moduletrace/bun-sqlite"; +import { BUN_MARIADB_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/bun.lock b/bun.lock new file mode 100644 index 0000000..2533766 --- /dev/null +++ b/bun.lock @@ -0,0 +1,141 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@moduletrace/bun-mariadb", + "dependencies": { + "@inquirer/prompts": "^8.3.0", + "chalk": "^5.6.2", + "commander": "^14.0.3", + "inquirer": "^13.3.2", + "lodash": "^4.17.23", + "mariadb": "^3.5.3", + "mysql": "^2.18.1", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/inquirer": "^9.0.9", + "@types/lodash": "^4.17.24", + "@types/mysql": "^2.15.27", + "@types/node": "^25.3.3", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@inquirer/ansi": ["@inquirer/ansi@2.0.7", "", {}, "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ=="], + + "@inquirer/core": ["@inquirer/core@11.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA=="], + + "@inquirer/editor": ["@inquirer/editor@5.2.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/external-editor": "^3.0.3", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg=="], + + "@inquirer/expand": ["@inquirer/expand@5.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@3.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.7", "", {}, "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw=="], + + "@inquirer/input": ["@inquirer/input@5.1.2", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg=="], + + "@inquirer/number": ["@inquirer/number@4.1.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA=="], + + "@inquirer/password": ["@inquirer/password@5.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg=="], + + "@inquirer/prompts": ["@inquirer/prompts@8.5.2", "", { "dependencies": { "@inquirer/checkbox": "^5.2.1", "@inquirer/confirm": "^6.1.1", "@inquirer/editor": "^5.2.2", "@inquirer/expand": "^5.1.1", "@inquirer/input": "^5.1.2", "@inquirer/number": "^4.1.1", "@inquirer/password": "^5.1.1", "@inquirer/rawlist": "^5.3.1", "@inquirer/search": "^4.2.1", "@inquirer/select": "^5.2.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@5.3.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og=="], + + "@inquirer/search": ["@inquirer/search@4.2.1", "", { "dependencies": { "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g=="], + + "@inquirer/select": ["@inquirer/select@5.2.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.7", "@inquirer/core": "^11.2.1", "@inquirer/figures": "^2.0.7", "@inquirer/type": "^4.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw=="], + + "@inquirer/type": ["@inquirer/type@4.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/inquirer": ["@types/inquirer@9.0.10", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-vFW2WbXwO9eZpRT5GJGFJ/shgyMNnYozmnjakt9jCQSS1lvqX8pZEQMjJ9RdDPct/YxwciQ8+V8OYn9euIrZDA=="], + + "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], + + "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], + + "@types/node": ["@types/node@25.9.4", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g=="], + + "@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="], + + "bignumber.js": ["bignumber.js@9.0.0", "", {}, "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inquirer": ["inquirer@13.4.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/prompts": "^8.4.3", "@inquirer/type": "^4.0.5", "mute-stream": "^3.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + + "mariadb": ["mariadb@3.5.3", "", { "dependencies": { "@types/geojson": "^7946.0.16", "@types/node": ">=20", "denque": "^2.1.0", "iconv-lite": "^0.7.2", "lru-cache": "^11.5.0" } }, "sha512-i053Kc0MgdUv/hu9mCyq67TYfPXFj3/MV8I7ZW5wvJNixIyXC0VztMPUjIVj/449nQo+BsxFD4Fdk/sA/uqKPQ=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + + "mysql": ["mysql@2.18.1", "", { "dependencies": { "bignumber.js": "9.0.0", "readable-stream": "2.3.7", "safe-buffer": "5.1.2", "sqlstring": "2.3.1" } }, "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "readable-stream": ["readable-stream@2.3.7", "", { "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" } }, "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw=="], + + "run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sqlstring": ["sqlstring@2.3.1", "", {}, "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + } +} diff --git a/dist/commands/admin/index.d.ts b/dist/commands/admin/index.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/admin/index.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/admin/index.js b/dist/commands/admin/index.js new file mode 100644 index 0000000..c2c7bdf --- /dev/null +++ b/dist/commands/admin/index.js @@ -0,0 +1,41 @@ +import { Command } from "commander"; +import init from "../../functions/init"; +import grabDBDir from "../../utils/grab-db-dir"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +import listTables from "./list-tables"; +import runSQL from "./run-sql"; +export default function () { + return new Command("admin") + .description("View Tables and Data, Run SQL Queries, Etc.") + .action(async () => { + const { config } = await init(); + const { db_file_path } = grabDBDir({ config }); + console.log(chalk.bold(chalk.blue("\nBun SQLite Admin\n"))); + try { + while (true) { + const paradigm = await select({ + message: "Choose an action:", + choices: [ + { name: "Tables", value: "list_tables" }, + { name: "SQL", value: "run_sql" }, + { name: chalk.dim("✕ Exit"), value: "exit" }, + ], + }); + if (paradigm === "exit") + break; + if (paradigm === "list_tables") { + const result = await listTables(); + if (result === "__exit__") + break; + } + if (paradigm === "run_sql") + await runSQL(); + } + } + catch (error) { + console.error(error.message); + } + process.exit(); + }); +} diff --git a/dist/commands/admin/list-tables.d.ts b/dist/commands/admin/list-tables.d.ts new file mode 100644 index 0000000..32a5441 --- /dev/null +++ b/dist/commands/admin/list-tables.d.ts @@ -0,0 +1,3 @@ +type Params = {}; +export default function listTables(params?: Params): Promise<"__exit__" | void>; +export {}; diff --git a/dist/commands/admin/list-tables.js b/dist/commands/admin/list-tables.js new file mode 100644 index 0000000..8567d37 --- /dev/null +++ b/dist/commands/admin/list-tables.js @@ -0,0 +1,78 @@ +import { Database } from "bun:sqlite"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +import { AppData } from "../../data/app-data"; +import showEntries from "./show-entries"; +import showFields from "./show-fields"; +import dbHandler from "../../lib/db-handler"; +export default async function listTables(params) { + const tables = (await dbHandler({ + query: `SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`, + })).payload; + if (!tables?.length) { + console.log(chalk.yellow("\nNo tables found.\n")); + return; + } + // Level 1: table selection loop + while (true) { + const tableName = await select({ + message: "Select a table:", + choices: [ + ...tables.map((t) => ({ + name: t.table_name, + value: t.table_name, + })), + { name: chalk.dim("← Go Back"), value: "__back__" }, + { name: chalk.dim("✕ Exit"), value: "__exit__" }, + ], + }); + if (tableName === "__back__") + break; + if (tableName === "__exit__") + return "__exit__"; + // Level 2: action loop — stays here until "Go Back" + while (true) { + const action = await select({ + message: `"${tableName}" — choose an action:`, + choices: [ + { name: "Entries", value: "entries" }, + { name: "Fields", value: "fields" }, + { name: "Schema", value: "schema" }, + { name: chalk.dim("← Go Back"), value: "__back__" }, + { name: chalk.dim("✕ Exit"), value: "__exit__" }, + ], + }); + if (action === "__back__") + break; + if (action === "__exit__") + return "__exit__"; + if (action === "entries") { + const result = await showEntries({ tableName }); + if (result === "__exit__") + return "__exit__"; + } + if (action === "fields") { + const result = await showFields({ tableName }); + if (result === "__exit__") + return "__exit__"; + } + // if (action === "schema") { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + // console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`); + // console.table( + // columns.map((c) => ({ + // "#": c.cid, + // Name: c.name, + // Type: c.type, + // "Not Null": c.notnull ? "YES" : "NO", + // Default: c.dflt_value ?? "(none)", + // "Primary Key": c.pk ? "YES" : "NO", + // })), + // ); + // console.log(); + // } + } + } +} diff --git a/dist/commands/admin/run-sql.d.ts b/dist/commands/admin/run-sql.d.ts new file mode 100644 index 0000000..b6f72fd --- /dev/null +++ b/dist/commands/admin/run-sql.d.ts @@ -0,0 +1,3 @@ +type Params = {}; +export default function runSQL(params?: Params): Promise; +export {}; diff --git a/dist/commands/admin/run-sql.js b/dist/commands/admin/run-sql.js new file mode 100644 index 0000000..9e5e45c --- /dev/null +++ b/dist/commands/admin/run-sql.js @@ -0,0 +1,26 @@ +import { Database } from "bun:sqlite"; +import { input } from "@inquirer/prompts"; +import chalk from "chalk"; +import dbHandler from "../../lib/db-handler"; +export default async function runSQL(params) { + const sql = await input({ + message: "Enter SQL query:", + validate: (val) => val.trim().length > 0 || "Query cannot be empty", + }); + try { + const res = await dbHandler({ query: sql }); + if (res.payload) { + console.log(`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`); + if (res.payload.length) + console.table(res.payload); + else + console.log(chalk.yellow("No res returned.\n")); + } + else if (res.single_res) { + console.log(chalk.green(`\nSuccess! Affected rows: ${res.single_res.changes}, Last insert ID: ${res.single_res.lastInsertRowid}\n`)); + } + } + catch (error) { + console.error(chalk.red(`\nSQL Error: ${error.message}\n`)); + } +} diff --git a/dist/commands/admin/show-entries.d.ts b/dist/commands/admin/show-entries.d.ts new file mode 100644 index 0000000..64206e5 --- /dev/null +++ b/dist/commands/admin/show-entries.d.ts @@ -0,0 +1,5 @@ +type Params = { + tableName: string; +}; +export default function showEntries({ tableName }: Params): Promise<"__exit__" | undefined>; +export {}; diff --git a/dist/commands/admin/show-entries.js b/dist/commands/admin/show-entries.js new file mode 100644 index 0000000..4e1315e --- /dev/null +++ b/dist/commands/admin/show-entries.js @@ -0,0 +1,83 @@ +import { Database } from "bun:sqlite"; +import chalk from "chalk"; +import { select, input } from "@inquirer/prompts"; +import dbHandler from "../../lib/db-handler"; +const LIMIT = 50; +export default async function showEntries({ tableName }) { + let page = 0; + let searchField = null; + let searchTerm = null; + while (true) { + const offset = page * LIMIT; + const rows = searchTerm + ? (await dbHandler({ + query: `SELECT * FROM "${tableName}" WHERE "${searchField}" LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`, + values: [`%${searchTerm}%`], + })).payload + : (await dbHandler({ + query: `SELECT * FROM "${tableName}" LIMIT ${LIMIT} OFFSET ${offset}`, + })).payload; + const countRow = searchTerm + ? (await dbHandler({ + query: `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`, + values: [`%${searchTerm}%`], + })).payload + : (await dbHandler({ + query: `SELECT COUNT(*) as count FROM "${tableName}"`, + })).payload; + const total = countRow?.[0]?.count; + const searchInfo = searchTerm + ? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`) + : ""; + if (!rows) { + return; + } + console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`); + if (rows.length) { + console.log(rows); + // if (rows.length) console.table(rows); + } + else { + console.log(chalk.yellow("No rows found.")); + console.log(); + } + const choices = []; + if (page > 0) + choices.push({ name: "← Previous Page", value: "prev" }); + if (offset + rows.length < total) + choices.push({ name: "Next Page →", value: "next" }); + choices.push({ name: "Search by Field", value: "search" }); + if (searchTerm) + choices.push({ name: "Clear Search", value: "clear_search" }); + choices.push({ name: chalk.dim("← Go Back"), value: "__back__" }); + choices.push({ name: chalk.dim("✕ Exit"), value: "__exit__" }); + const action = await select({ message: "Navigate:", choices }); + if (action === "__back__") + break; + if (action === "__exit__") + return "__exit__"; + if (action === "next") + page++; + if (action === "prev") + page--; + if (action === "clear_search") { + searchField = null; + searchTerm = null; + page = 0; + } + // if (action === "search") { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + // searchField = await select({ + // message: "Search by field:", + // choices: columns.map((c) => ({ name: c.name, value: c.name })), + // }); + // searchTerm = await input({ + // message: `Search term for "${searchField}":`, + // validate: (v) => v.trim().length > 0 || "Cannot be empty", + // }); + // page = 0; + // } + } +} diff --git a/dist/commands/admin/show-fields.d.ts b/dist/commands/admin/show-fields.d.ts new file mode 100644 index 0000000..34beb3b --- /dev/null +++ b/dist/commands/admin/show-fields.d.ts @@ -0,0 +1,5 @@ +type Params = { + tableName: string; +}; +export default function showFields({ tableName, }: Params): Promise<"__exit__" | void>; +export {}; diff --git a/dist/commands/admin/show-fields.js b/dist/commands/admin/show-fields.js new file mode 100644 index 0000000..48c5f7b --- /dev/null +++ b/dist/commands/admin/show-fields.js @@ -0,0 +1,69 @@ +import { Database } from "bun:sqlite"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +export default async function showFields({ tableName, }) { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + // const indexes = db + // .query(`PRAGMA index_list("${tableName}")`) + // .all(); + // const foreignKeys = db + // .query(`PRAGMA foreign_key_list("${tableName}")`) + // .all(); + // const indexedFields = new Map(); + // for (const idx of indexes) { + // const cols = db + // .query(`PRAGMA index_info("${idx.name}")`) + // .all(); + // for (const col of cols) { + // indexedFields.set(col.name, { unique: idx.unique === 1 }); + // } + // } + // while (true) { + // const fieldName = await select({ + // message: `"${tableName}" — select a field:`, + // choices: [ + // ...columns.map((c) => ({ name: c.name, value: c.name })), + // { name: chalk.dim("← Go Back"), value: "__back__" }, + // { name: chalk.dim("✕ Exit"), value: "__exit__" }, + // ], + // }); + // if (fieldName === "__back__") break; + // if (fieldName === "__exit__") return "__exit__"; + // const col = columns.find((c) => c.name === fieldName)!; + // const idx = indexedFields.get(fieldName); + // const fk = foreignKeys.find((f) => f.from === fieldName); + // console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`); + // console.log(` ${chalk.dim("Table")} ${tableName}`); + // console.log(` ${chalk.dim("Column #")} ${col.cid}`); + // console.log( + // ` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`, + // ); + // console.log( + // ` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`, + // ); + // console.log( + // ` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`, + // ); + // if (fk) { + // console.log( + // ` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`, + // ); + // console.log(` ${chalk.dim("On Update")} ${fk.on_update}`); + // console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`); + // } else { + // console.log(` ${chalk.dim("Foreign Key")} NO`); + // } + // console.log(); + // } +} 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..7458842 --- /dev/null +++ b/dist/commands/index.js @@ -0,0 +1,33 @@ +#!/usr/bin/env bun +import { program } from "commander"; +import schema from "./schema"; +import typedef from "./typedef"; +import backup from "./backup"; +import restore from "./restore"; +import admin from "./admin"; +/** + * # Describe Program + */ +program + .name(`bun-mariadb`) + .description(`MariaDB manager for Bun`) + .version(`1.0.0`); +/** + * # Declare Commands + */ +program.addCommand(schema()); +program.addCommand(typedef()); +program.addCommand(backup()); +program.addCommand(restore()); +program.addCommand(admin()); +/** + * # 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..4dadc36 --- /dev/null +++ b/dist/commands/schema.js @@ -0,0 +1,54 @@ +import { Command } from "commander"; +import { MariaDBSchemaManager } from "../lib/mariadb/db-schema-manager"; +import init from "../functions/init"; +import grabDirNames from "../data/grab-dir-names"; +import path from "path"; +import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef"; +import _ from "lodash"; +import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema"; +import chalk from "chalk"; +import { writeLiveSchema } from "../functions/live-schema"; +import grabDBDir from "../utils/grab-db-dir"; +import { cpSync } from "fs"; +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, BUN_MARIADB_TEMP_DB_FILE_PATH } = grabDirNames(); + const { db_file_path } = grabDBDir({ config }); + cpSync(db_file_path, BUN_MARIADB_TEMP_DB_FILE_PATH); + try { + const isVector = Boolean(opts.vector || opts.v); + const isTypeDef = Boolean(opts.typedef || opts.t); + const finaldbSchema = appendDefaultFieldsToDbSchema({ + dbSchema, + }); + const manager = new MariaDBSchemaManager({ + 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, + config, + }); + } + writeLiveSchema({ schema: finaldbSchema }); + console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`); + process.exit(); + } + catch (error) { + console.log(error); + cpSync(BUN_MARIADB_TEMP_DB_FILE_PATH, db_file_path); + process.exit(1); + } + }); +} 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..87d97d2 --- /dev/null +++ b/dist/commands/typedef.js @@ -0,0 +1,31 @@ +import { Command } from "commander"; +import init from "../functions/init"; +import dbSchemaToTypeDef from "../lib/mariadb/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, + config, + }); + } + 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..f3eb051 --- /dev/null +++ b/dist/data/app-data.d.ts @@ -0,0 +1,6 @@ +export declare const AppData: { + readonly ConfigFileName: "bun-sqlite.config.ts"; + readonly MaxBackups: 10; + readonly DefaultBackupDirName: ".backups"; + readonly DbSchemaManagerTableName: "__db_schema_manager__"; +}; diff --git a/dist/data/app-data.js b/dist/data/app-data.js new file mode 100644 index 0000000..439bcb7 --- /dev/null +++ b/dist/data/app-data.js @@ -0,0 +1,6 @@ +export const AppData = { + ConfigFileName: "bun-sqlite.config.ts", + MaxBackups: 10, + DefaultBackupDirName: ".backups", + DbSchemaManagerTableName: "__db_schema_manager__", +}; diff --git a/dist/data/grab-dir-names.d.ts b/dist/data/grab-dir-names.d.ts new file mode 100644 index 0000000..33d56a5 --- /dev/null +++ b/dist/data/grab-dir-names.d.ts @@ -0,0 +1,12 @@ +import type { BunSQLiteConfig } from "../types"; +type Params = { + config?: BunSQLiteConfig; +}; +export default function grabDirNames(params?: Params): { + ROOT_DIR: string; + BUN_MARIADB_DIR: string; + BUN_MARIADB_TEMP_DIR: string; + BUN_MARIADB_LIVE_SCHEMA: string; + BUN_MARIADB_TEMP_DB_FILE_PATH: string; +}; +export {}; diff --git a/dist/data/grab-dir-names.js b/dist/data/grab-dir-names.js new file mode 100644 index 0000000..e2b5e6b --- /dev/null +++ b/dist/data/grab-dir-names.js @@ -0,0 +1,15 @@ +import path from "path"; +export default function grabDirNames(params) { + const ROOT_DIR = process.cwd(); + const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-sqlite"); + const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp"); + const BUN_MARIADB_TEMP_DB_FILE_PATH = path.join(BUN_MARIADB_TEMP_DIR, "temp.db"); + const BUN_MARIADB_LIVE_SCHEMA = path.join(BUN_MARIADB_DIR, "live-schema.json"); + return { + ROOT_DIR, + BUN_MARIADB_DIR, + BUN_MARIADB_TEMP_DIR, + BUN_MARIADB_LIVE_SCHEMA, + BUN_MARIADB_TEMP_DB_FILE_PATH, + }; +} 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/functions/live-schema.d.ts b/dist/functions/live-schema.d.ts new file mode 100644 index 0000000..5e9fcc9 --- /dev/null +++ b/dist/functions/live-schema.d.ts @@ -0,0 +1,7 @@ +import type { BUN_MARIADB_DatabaseSchemaType } from "../types"; +type Params = { + schema: BUN_MARIADB_DatabaseSchemaType; +}; +export declare function writeLiveSchema({ schema }: Params): void; +export declare function readLiveSchema(): BUN_MARIADB_DatabaseSchemaType | undefined; +export {}; diff --git a/dist/functions/live-schema.js b/dist/functions/live-schema.js new file mode 100644 index 0000000..13c282b --- /dev/null +++ b/dist/functions/live-schema.js @@ -0,0 +1,15 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import grabDirNames from "../data/grab-dir-names"; +import path from "path"; +const { BUN_MARIADB_LIVE_SCHEMA } = grabDirNames(); +export function writeLiveSchema({ schema }) { + mkdirSync(path.dirname(BUN_MARIADB_LIVE_SCHEMA), { recursive: true }); + writeFileSync(BUN_MARIADB_LIVE_SCHEMA, JSON.stringify(schema)); +} +export function readLiveSchema() { + if (!existsSync(BUN_MARIADB_LIVE_SCHEMA)) { + return undefined; + } + const live_schema = readFileSync(BUN_MARIADB_LIVE_SCHEMA, "utf-8"); + return JSON.parse(live_schema); +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..c6f746e --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,19 @@ +import DbDelete from "./lib/mariadb/db-delete"; +import DbInsert from "./lib/mariadb/db-insert"; +import DbSelect from "./lib/mariadb/db-select"; +import DbSQL from "./lib/mariadb/db-sql"; +import DbUpdate from "./lib/mariadb/db-update"; +import grabDbSchema from "./utils/grab-db-schema"; +import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object"; +declare const BunMariaDB: { + readonly select: typeof DbSelect; + readonly insert: typeof DbInsert; + readonly update: typeof DbUpdate; + readonly delete: typeof DbDelete; + readonly sql: typeof DbSQL; + readonly utils: { + readonly grab_db_schema: typeof grabDbSchema; + readonly grab_join_fields_from_query_object: typeof grabJoinFieldsFromQueryObject; + }; +}; +export default BunMariaDB; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..d2d757f --- /dev/null +++ b/dist/index.js @@ -0,0 +1,19 @@ +import DbDelete from "./lib/mariadb/db-delete"; +import DbInsert from "./lib/mariadb/db-insert"; +import DbSelect from "./lib/mariadb/db-select"; +import DbSQL from "./lib/mariadb/db-sql"; +import DbUpdate from "./lib/mariadb/db-update"; +import grabDbSchema from "./utils/grab-db-schema"; +import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object"; +const BunMariaDB = { + select: DbSelect, + insert: DbInsert, + update: DbUpdate, + delete: DbDelete, + sql: DbSQL, + utils: { + grab_db_schema: grabDbSchema, + grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject, + }, +}; +export default BunMariaDB; diff --git a/dist/lib/db-handler.d.ts b/dist/lib/db-handler.d.ts new file mode 100644 index 0000000..c4e02c1 --- /dev/null +++ b/dist/lib/db-handler.d.ts @@ -0,0 +1,20 @@ +import type { ConnectionConfig } from "mariadb"; +import type { BUN_MARIADB_TableSchemaType, DBResponseObject } from "../types"; +type Param = { + query: string; + values?: string[] | object; + noErrorLogs?: boolean; + database?: string; + tableSchema?: BUN_MARIADB_TableSchemaType; + config?: ConnectionConfig; +}; +/** + * # Main DB Handler Function + * @requires DSQL_DB_CONN - Gobal Variable for Datasquirel Database + */ +export default function dbHandler({ query, values, noErrorLogs, database, config, }: Param): Promise; +export {}; diff --git a/dist/lib/db-handler.js b/dist/lib/db-handler.js new file mode 100644 index 0000000..d881eed --- /dev/null +++ b/dist/lib/db-handler.js @@ -0,0 +1,46 @@ +import grabDBConnection from "./grab-db-connection"; +/** + * # Main DB Handler Function + * @requires DSQL_DB_CONN - Gobal Variable for Datasquirel Database + */ +export default async function dbHandler({ query, values, noErrorLogs, database, config, }) { + let CONNECTION; + let results = null; + try { + CONNECTION = await grabDBConnection({ database, config }); + if (query && values) { + const queryResults = await CONNECTION.query(query, values); + results = queryResults[0]; + } + else { + const queryResults = await CONNECTION.query(query); + results = queryResults[0]; + } + } + catch (error) { + if (error.message && + typeof error.message == "string" && + error.message.match(/Access denied for user.*password/i)) { + throw new Error("Authentication Failed!"); + } + if (!noErrorLogs) { + console.log(error); + } + results = null; + } + finally { + await CONNECTION?.end(); + } + if (results) { + return { + success: true, + payload: Array.isArray(results) ? results : undefined, + single_res: Array.isArray(results) ? undefined : results, + }; + } + else { + return { + success: false, + }; + } +} diff --git a/dist/lib/grab-db-connection.d.ts b/dist/lib/grab-db-connection.d.ts new file mode 100644 index 0000000..c9cc96d --- /dev/null +++ b/dist/lib/grab-db-connection.d.ts @@ -0,0 +1,6 @@ +import type { Connection } from "mariadb"; +import type { DsqlConnectionParam } from "../types"; +/** + * # Grab General CONNECTION for DSQL + */ +export default function grabDBConnection(param?: DsqlConnectionParam): Promise; diff --git a/dist/lib/grab-db-connection.js b/dist/lib/grab-db-connection.js new file mode 100644 index 0000000..93e7cca --- /dev/null +++ b/dist/lib/grab-db-connection.js @@ -0,0 +1,15 @@ +import * as mariadb from "mariadb"; +import grabDSQLConnectionConfig from "./grab-dsql-connection-config"; +/** + * # Grab General CONNECTION for DSQL + */ +export default async function grabDBConnection(param) { + const config = grabDSQLConnectionConfig(param); + try { + return await mariadb.createConnection(config); + } + catch (error) { + console.log(`Error Grabbing DSQL Connection =>`, config); + throw error; + } +} diff --git a/dist/lib/grab-db-ssl.d.ts b/dist/lib/grab-db-ssl.d.ts new file mode 100644 index 0000000..d507388 --- /dev/null +++ b/dist/lib/grab-db-ssl.d.ts @@ -0,0 +1,7 @@ +import type { ConnectionConfig } from "mariadb"; +type Return = ConnectionConfig["ssl"] | undefined; +/** + * # Grab SSL + */ +export default function grabDbSSL(): Return; +export {}; diff --git a/dist/lib/grab-db-ssl.js b/dist/lib/grab-db-ssl.js new file mode 100644 index 0000000..6fae64d --- /dev/null +++ b/dist/lib/grab-db-ssl.js @@ -0,0 +1,20 @@ +import fs from "fs"; +import path from "path"; +/** + * # Grab SSL + */ +export default function grabDbSSL() { + const caProivdedPath = process.env.DSQL_SSL_CA_CERT; + if (!caProivdedPath?.match(/./)) { + return undefined; + } + const caFilePath = path.resolve(process.cwd(), caProivdedPath); + if (!fs.existsSync(caFilePath)) { + console.log(`${caFilePath} does not exist`); + return undefined; + } + return { + ca: fs.readFileSync(caFilePath), + rejectUnauthorized: false, + }; +} diff --git a/dist/lib/grab-dsql-connection-config.d.ts b/dist/lib/grab-dsql-connection-config.d.ts new file mode 100644 index 0000000..2a1f175 --- /dev/null +++ b/dist/lib/grab-dsql-connection-config.d.ts @@ -0,0 +1,6 @@ +import { type ConnectionConfig } from "mariadb"; +import type { DsqlConnectionParam } from "../types"; +/** + * # Grab General CONNECTION for DSQL + */ +export default function grabDSQLConnectionConfig(param?: DsqlConnectionParam): ConnectionConfig; diff --git a/dist/lib/grab-dsql-connection-config.js b/dist/lib/grab-dsql-connection-config.js new file mode 100644 index 0000000..52fe0db --- /dev/null +++ b/dist/lib/grab-dsql-connection-config.js @@ -0,0 +1,29 @@ +import {} from "mariadb"; +import grabDbSSL from "./grab-db-ssl"; +/** + * # Grab General CONNECTION for DSQL + */ +export default function grabDSQLConnectionConfig(param) { + const CONN_TIMEOUT = 10000; + const config = { + host: process.env.DSQL_DB_HOST, + user: process.env.DSQL_DB_USERNAME, + password: process.env.DSQL_DB_PASSWORD, + // database: param?.dbFullName, + port: process.env.DSQL_DB_PORT + ? Number(process.env.DSQL_DB_PORT) + : undefined, + charset: "utf8mb4", + ssl: grabDbSSL(), + bigIntAsNumber: true, + supportBigNumbers: true, + bigNumberStrings: false, + dateStrings: true, + metaAsArray: true, + socketTimeout: CONN_TIMEOUT, + connectTimeout: CONN_TIMEOUT, + compress: true, + ...param?.config, + }; + return config; +} diff --git a/dist/lib/grab-duplicate-safe-insert-sql.d.ts b/dist/lib/grab-duplicate-safe-insert-sql.d.ts new file mode 100644 index 0000000..8e6211e --- /dev/null +++ b/dist/lib/grab-duplicate-safe-insert-sql.d.ts @@ -0,0 +1,7 @@ +type Params = { + sql: string; + table: string; + data: any | any[]; +}; +export default function ({ sql: passed_sql, table, data }: Params): Promise; +export {}; diff --git a/dist/lib/grab-duplicate-safe-insert-sql.js b/dist/lib/grab-duplicate-safe-insert-sql.js new file mode 100644 index 0000000..c13fd87 --- /dev/null +++ b/dist/lib/grab-duplicate-safe-insert-sql.js @@ -0,0 +1,24 @@ +import init from "../functions/init"; +export default async function ({ sql: passed_sql, table, data }) { + let sql = passed_sql; + const { dbSchema } = await init(); + const table_schema = dbSchema.tables.find((t) => t.tableName == table); + const now = Date.now(); + if (table_schema?.tableName) { + const set_sql_arr = Object.keys(Array.isArray(data) ? data[0] : data).map((field) => `${field} = excluded.${field}`); + set_sql_arr.push(`updated_at = ${now}`); + const set_sql = set_sql_arr.join(", "); + const unique_fields = table_schema.fields.filter((f) => f.unique); + for (let i = 0; i < unique_fields.length; i++) { + const field = unique_fields[i]; + sql += ` ON CONFLICT(${field?.fieldName}) DO UPDATE SET ${set_sql}`; + } + if (table_schema.uniqueConstraints?.[0]) { + for (let i = 0; i < table_schema.uniqueConstraints.length; i++) { + const constraint = table_schema.uniqueConstraints[i]; + sql += ` ON CONFLICT(${constraint?.constraintTableFields?.map((c) => c.value)?.join(", ")}) DO UPDATE SET ${set_sql}`; + } + } + } + return sql; +} diff --git a/dist/lib/mariadb/db-delete.d.ts b/dist/lib/mariadb/db-delete.d.ts new file mode 100644 index 0000000..c1c63ba --- /dev/null +++ b/dist/lib/mariadb/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/mariadb/db-delete.js b/dist/lib/mariadb/db-delete.js new file mode 100644 index 0000000..a23a327 --- /dev/null +++ b/dist/lib/mariadb/db-delete.js @@ -0,0 +1,56 @@ +import DbClient from "."; +import _ from "lodash"; +import sqlGenerator from "../../utils/sql-generator"; +export default async function DbDelete({ table, query, targetId, }) { + let sqlObj = null; + try { + let finalQuery = query || {}; + if (targetId) { + finalQuery = _.merge(finalQuery, { + query: { + id: { + value: String(targetId), + }, + }, + }); + } + sqlObj = sqlGenerator({ + tableName: table, + genObject: finalQuery, + }); + const whereClause = sqlObj.string.match(/WHERE .*/)?.[0]; + if (whereClause) { + let sql = `DELETE FROM ${table} ${whereClause}`; + sqlObj.string = sql; + const res = DbClient.run(sql, sqlObj.values); + return { + success: Boolean(res.changes), + postInsertReturn: { + affectedRows: res.changes, + insertId: Number(res.lastInsertRowid), + }, + debug: { + sqlObj, + }, + }; + } + else { + return { + success: false, + msg: `No WHERE clause`, + debug: { + sqlObj, + }, + }; + } + } + catch (error) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-generate-type-defs.d.ts b/dist/lib/mariadb/db-generate-type-defs.d.ts new file mode 100644 index 0000000..81892ca --- /dev/null +++ b/dist/lib/mariadb/db-generate-type-defs.d.ts @@ -0,0 +1,15 @@ +import type { BUN_MARIADB_TableSchemaType } from "../../types"; +type Param = { + paradigm: "JavaScript" | "TypeScript" | undefined; + table: BUN_MARIADB_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/mariadb/db-generate-type-defs.js b/dist/lib/mariadb/db-generate-type-defs.js new file mode 100644 index 0000000..decabea --- /dev/null +++ b/dist/lib/mariadb/db-generate-type-defs.js @@ -0,0 +1,63 @@ +export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }) { + let typeDefinition = ``; + let tdName = ``; + try { + tdName = typeDefName + ? typeDefName + : dbName + ? `BUN_MARIADB_${dbName}_${table.tableName}`.toUpperCase() + : `BUN_MARIADB_${query.single}_${query.single_table}`.toUpperCase(); + const fields = table.fields; + function typeMap(schemaType) { + if (schemaType.options && schemaType.options.length > 0) { + let opts = schemaType.options.map((opt) => schemaType.dataType?.match(/int/i) || typeof opt == "number" + ? `${opt}` + : `"${opt}"`); + opts.push(`""`); + return opts.join(" | "); + } + if (schemaType.dataType?.match(/blob/i)) { + return `Float32Array | Buffer | null`; + } + if (schemaType.dataType?.match(/int|double|decimal|real/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/mariadb/db-insert.d.ts b/dist/lib/mariadb/db-insert.d.ts new file mode 100644 index 0000000..d5101ec --- /dev/null +++ b/dist/lib/mariadb/db-insert.d.ts @@ -0,0 +1,16 @@ +import type { APIResponseObject } from "../../types"; +type Params = { + table: Table; + data: Schema[]; + update_on_duplicate?: boolean; +}; +export default function DbInsert({ table, data, update_on_duplicate, }: Params): Promise; +export {}; diff --git a/dist/lib/mariadb/db-insert.js b/dist/lib/mariadb/db-insert.js new file mode 100644 index 0000000..f5d37e8 --- /dev/null +++ b/dist/lib/mariadb/db-insert.js @@ -0,0 +1,43 @@ +import DbClient from "."; +import sqlInsertGenerator from "../../utils/sql-insert-generator"; +import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql"; +export default async function DbInsert({ table, data, update_on_duplicate, }) { + let sqlObj = null; + try { + const finalData = data.map((d) => ({ + created_at: Date.now(), + updated_at: Date.now(), + ...d, + })); + sqlObj = + sqlInsertGenerator({ + tableName: table, + data: finalData, + }) || null; + let sql = sqlObj?.query || ""; + if (update_on_duplicate && data[0]) { + sql = await grabDuplicateSafeInsertSql({ data, table, sql }); + } + (sqlObj || {}).query = sql; + const res = DbClient.run(sql, 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, + debug: { + sqlObj, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-schema-manager.d.ts b/dist/lib/mariadb/db-schema-manager.d.ts new file mode 100644 index 0000000..30f7dc6 --- /dev/null +++ b/dist/lib/mariadb/db-schema-manager.d.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env bun +import type { BUN_MARIADB_DatabaseSchemaType } from "../../types"; +declare class MariaDBSchemaManager { + private db_manager_table_name; + private recreate_vector_table; + private db_schema; + constructor({ schema, recreate_vector_table, }: { + schema: BUN_MARIADB_DatabaseSchemaType; + recreate_vector_table?: boolean; + }); + syncSchema(): Promise; + private getDatabaseName; + private quoteIdentifier; + private tableSchemaWhere; + private run; + private query; + private createDbManagerTable; + private insertDbManagerTable; + private removeDbManagerTable; + private getExistingTables; + private getLiveTableNames; + private dropRemovedTables; + private syncTable; + private resolveTable; + private createTable; + private buildTableOptions; + private updateTable; + private getTableColumns; + private addColumn; + private checkIfTableExists; + private recreateTable; + private insertRows; + private buildColumnDefinition; + private mapDataType; + private buildForeignKeyConstraint; + private syncIndexes; + close(): void; +} +export { MariaDBSchemaManager }; diff --git a/dist/lib/mariadb/db-schema-manager.js b/dist/lib/mariadb/db-schema-manager.js new file mode 100644 index 0000000..9fcb6aa --- /dev/null +++ b/dist/lib/mariadb/db-schema-manager.js @@ -0,0 +1,419 @@ +#!/usr/bin/env bun +import _ from "lodash"; +import dbHandler from "../db-handler"; +import { AppData } from "../../data/app-data"; +import { readLiveSchema } from "../../functions/live-schema"; +class MariaDBSchemaManager { + db_manager_table_name; + recreate_vector_table; + db_schema; + constructor({ schema, recreate_vector_table = false, }) { + this.db_manager_table_name = AppData["DbSchemaManagerTableName"]; + this.recreate_vector_table = recreate_vector_table; + this.db_schema = schema; + } + async syncSchema() { + console.log("Starting schema synchronization..."); + await this.createDbManagerTable(); + const existingTables = await this.getExistingTables(); + const schemaTables = this.db_schema.tables.map((t) => t.tableName); + for (const table of this.db_schema.tables) { + await this.syncTable(table, existingTables); + } + await this.dropRemovedTables(existingTables, schemaTables); + console.log("Schema synchronization complete!"); + } + getDatabaseName() { + return this.db_schema.dbName || this.db_schema.dbSlug; + } + quoteIdentifier(identifier) { + return `\`${identifier.replace(/`/g, "``")}\``; + } + tableSchemaWhere(tableName) { + const databaseName = this.getDatabaseName(); + if (databaseName) { + return { + where: "TABLE_SCHEMA = ?", + values: [databaseName, tableName], + }; + } + return { + where: "TABLE_SCHEMA = DATABASE()", + values: [tableName], + }; + } + async run(query, values) { + const res = await dbHandler({ + query, + values: values, + config: this.getDatabaseName() + ? { database: this.getDatabaseName() } + : undefined, + }); + if (!res.success) { + throw new Error(`Database query failed: ${query}`); + } + } + async query(query, values) { + const res = await dbHandler({ + query, + values: values, + config: this.getDatabaseName() + ? { database: this.getDatabaseName() } + : undefined, + }); + if (!res.success) { + throw new Error(`Database query failed: ${query}`); + } + return (res.payload || []); + } + async createDbManagerTable() { + await this.run(` + CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(this.db_manager_table_name)} ( + table_name VARCHAR(255) NOT NULL PRIMARY KEY, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci + `); + } + async insertDbManagerTable(tableName) { + const now = Date.now(); + await this.run(`INSERT INTO ${this.quoteIdentifier(this.db_manager_table_name)} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`, [tableName, now, now]); + } + async removeDbManagerTable(tableName) { + await this.run(`DELETE FROM ${this.quoteIdentifier(this.db_manager_table_name)} WHERE table_name = ?`, [tableName]); + } + async getExistingTables() { + const rows = await this.query(`SELECT table_name FROM ${this.quoteIdentifier(this.db_manager_table_name)}`); + return rows.map((row) => row.table_name); + } + async getLiveTableNames() { + const tableSchemaWhere = this.tableSchemaWhere(""); + const rows = await this.query(`SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_TYPE = 'BASE TABLE'`, tableSchemaWhere.values); + return rows.map((row) => row.TABLE_NAME); + } + async dropRemovedTables(existingTables, schemaTables) { + console.log(`Cleaning up tables ...`); + const tablesToDrop = existingTables.filter((tableName) => !schemaTables.includes(tableName) && + !schemaTables.some((schemaTable) => tableName.startsWith(`${schemaTable}_`))); + const currentSchema = readLiveSchema(); + if (currentSchema?.tables?.[0]) { + for (const table of currentSchema.tables) { + if (!table?.tableName) + continue; + const doesTableExist = schemaTables.find((tableName) => tableName === table.tableName); + if (!doesTableExist) { + tablesToDrop.push(table.tableName); + } + } + } + for (const tableName of tablesToDrop) { + console.log(`Dropping table: ${tableName}`); + await this.run(`DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`); + await this.removeDbManagerTable(tableName); + } + } + async syncTable(table, existingTables) { + let tableExists = existingTables.includes(table.tableName); + const liveTables = await this.getLiveTableNames(); + if (table.tableNameOld && table.tableNameOld !== table.tableName) { + if (liveTables.includes(table.tableNameOld)) { + console.log(`Renaming table: ${table.tableNameOld} -> ${table.tableName}`); + await this.run(`RENAME TABLE ${this.quoteIdentifier(table.tableNameOld)} TO ${this.quoteIdentifier(table.tableName)}`); + await this.insertDbManagerTable(table.tableName); + await this.removeDbManagerTable(table.tableNameOld); + tableExists = true; + } + } + if (!tableExists) { + await this.createTable(table); + await this.insertDbManagerTable(table.tableName); + } + else { + await this.updateTable(table); + await this.insertDbManagerTable(table.tableName); + } + await this.syncIndexes(table); + } + resolveTable(table) { + if (!table.parentTableName) { + return _.cloneDeep(table); + } + const parentTable = this.db_schema.tables.find((schemaTable) => schemaTable.tableName === table.parentTableName); + if (!parentTable) { + throw new Error(`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``); + } + return _.merge({}, parentTable, { + tableName: table.tableName, + tableDescription: table.tableDescription, + collation: table.collation, + fields: [...(parentTable.fields || []), ...(table.fields || [])], + indexes: [...(parentTable.indexes || []), ...(table.indexes || [])], + uniqueConstraints: [ + ...(parentTable.uniqueConstraints || []), + ...(table.uniqueConstraints || []), + ], + }); + } + async createTable(table) { + if (!table.tableName.match(/_temp_\d+$/)) { + console.log(`Creating table: ${table.tableName}`); + } + const newTable = this.resolveTable(table); + const columnDefinitions = []; + const foreignKeys = []; + for (const field of newTable.fields || []) { + columnDefinitions.push(this.buildColumnDefinition(field)); + if (field.foreignKey && !newTable.isVector) { + foreignKeys.push(this.buildForeignKeyConstraint(field)); + } + } + if (newTable.uniqueConstraints) { + for (const constraint of newTable.uniqueConstraints) { + if (constraint.constraintTableFields && + constraint.constraintTableFields.length > 0) { + const fields = constraint.constraintTableFields + .map((field) => this.quoteIdentifier(field.value)) + .join(", "); + const constraintName = constraint.constraintName || + `unique_${fields.replace(/`/g, "")}`; + columnDefinitions.push(`CONSTRAINT ${this.quoteIdentifier(constraintName)} UNIQUE (${fields})`); + } + } + } + const sql = `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(newTable.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${this.buildTableOptions(newTable)}`; + await this.run(sql); + } + buildTableOptions(table) { + const options = ["ENGINE=InnoDB"]; + if (table.collation) { + options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`); + } + return ` ${options.join(" ")}`; + } + async updateTable(table) { + console.log(`Updating table: ${table.tableName}`); + await this.recreateTable(table); + } + async getTableColumns(tableName) { + const tableSchemaWhere = this.tableSchemaWhere(tableName); + const rows = await this.query(`SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type FROM information_schema.COLUMNS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`, tableSchemaWhere.values); + return rows.map((row) => ({ + name: row.COLUMN_NAME, + type: row.COLUMN_TYPE, + })); + } + async addColumn(tableName, field) { + console.log(`Adding column: ${tableName}.${field.fieldName}`); + const columnDef = this.buildColumnDefinition(field) + .replace(/PRIMARY KEY/gi, "") + .replace(/AUTO_INCREMENT/gi, "") + .replace(/UNIQUE/gi, "") + .trim(); + await this.run(`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${columnDef}`); + } + async checkIfTableExists(table) { + const tableSchemaWhere = this.tableSchemaWhere(table); + const row = await this.query(`SELECT 1 AS exists FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? LIMIT 1`, tableSchemaWhere.values); + return Boolean(row[0]?.exists); + } + async recreateTable(table) { + if (table.isVector && !this.recreate_vector_table) { + return; + } + const doesTableExist = await this.checkIfTableExists(table.tableName); + if (table.isVector) { + let existingRows = []; + if (doesTableExist) { + existingRows = await this.query(`SELECT * FROM ${this.quoteIdentifier(table.tableName)}`); + await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`); + } + await this.createTable(table); + if (existingRows.length > 0) { + await this.insertRows(table.tableName, existingRows); + } + return; + } + const tempTableName = `${table.tableName}_temp_${Date.now()}`; + const existingColumns = await this.getTableColumns(table.tableName); + const columnsToKeep = table.fields + .filter((field) => existingColumns.some((column) => column.name === field.fieldName)) + .map((field) => field.fieldName) + .filter((fieldName) => Boolean(fieldName)); + await this.createTable({ ...table, tableName: tempTableName }); + if (columnsToKeep.length > 0) { + const columnList = columnsToKeep + .map((column) => this.quoteIdentifier(column)) + .join(", "); + await this.run(`INSERT INTO ${this.quoteIdentifier(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${this.quoteIdentifier(table.tableName)}`); + } + await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`); + await this.run(`RENAME TABLE ${this.quoteIdentifier(tempTableName)} TO ${this.quoteIdentifier(table.tableName)}`); + } + async insertRows(tableName, rows) { + for (const row of rows) { + const columns = Object.keys(row); + if (columns.length === 0) { + continue; + } + const values = columns.map((column) => row[column]); + const columnList = columns + .map((column) => this.quoteIdentifier(column)) + .join(", "); + const placeholders = columns.map(() => "?").join(", "); + await this.run(`INSERT INTO ${this.quoteIdentifier(tableName)} (${columnList}) VALUES (${placeholders})`, values); + } + } + buildColumnDefinition(field) { + if (!field.fieldName) { + throw new Error("Field name is required"); + } + const parts = [this.quoteIdentifier(field.fieldName)]; + parts.push(this.mapDataType(field)); + if (field.primaryKey) { + parts.push("PRIMARY KEY"); + if (field.autoIncrement) { + parts.push("AUTO_INCREMENT"); + } + } + if (field.notNullValue || field.primaryKey) { + if (!field.primaryKey) { + parts.push("NOT NULL"); + } + } + if (field.unique && !field.primaryKey) { + parts.push("UNIQUE"); + } + if (field.defaultValue !== undefined) { + if (typeof field.defaultValue === "string") { + parts.push(`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`); + } + else { + parts.push(`DEFAULT ${field.defaultValue}`); + } + } + else if (field.defaultValueLiteral) { + parts.push(`DEFAULT ${field.defaultValueLiteral}`); + } + if (field.onUpdate) { + parts.push(`ON UPDATE ${field.onUpdate}`); + } + else if (field.onUpdateLiteral) { + parts.push(`ON UPDATE ${field.onUpdateLiteral}`); + } + return parts.join(" "); + } + mapDataType(field) { + const dataType = field.dataType?.toLowerCase() || "text"; + const vectorSize = field.vectorSize || 1536; + if (field.isVector) { + return `LONGTEXT COMMENT 'vector_size=${vectorSize}'`; + } + if (dataType.includes("int") || + dataType === "bigint" || + dataType === "smallint" || + dataType === "tinyint") { + if (field.integerLength) { + return `INT(${field.integerLength})`; + } + return "INT"; + } + if (dataType === "bigint") { + if (field.integerLength) { + return `BIGINT(${field.integerLength})`; + } + return "BIGINT"; + } + if (dataType === "smallint") { + if (field.integerLength) { + return `SMALLINT(${field.integerLength})`; + } + return "SMALLINT"; + } + if (dataType === "tinyint") { + if (field.integerLength) { + return `TINYINT(${field.integerLength})`; + } + return "TINYINT"; + } + if (dataType.includes("double")) { + return "DOUBLE"; + } + if (dataType.includes("float")) { + return "FLOAT"; + } + if (dataType.includes("decimal") || dataType.includes("numeric")) { + if (field.integerLength && field.decimals) { + return `DECIMAL(${field.integerLength}, ${field.decimals})`; + } + return "DECIMAL"; + } + if (dataType.includes("blob") || dataType.includes("binary")) { + return "BLOB"; + } + if (dataType === "boolean" || dataType === "bool") { + return "TINYINT(1)"; + } + if (dataType.includes("timestamp")) { + return "TIMESTAMP"; + } + if (dataType.includes("datetime")) { + return "DATETIME"; + } + if (dataType.includes("date") || dataType.includes("time")) { + return "DATETIME"; + } + if (dataType.includes("varchar")) { + if (field.integerLength) { + return `VARCHAR(${field.integerLength})`; + } + return "VARCHAR(255)"; + } + return "TEXT"; + } + buildForeignKeyConstraint(field) { + const fk = field.foreignKey; + const constraintName = fk.foreignKeyName + ? `CONSTRAINT ${this.quoteIdentifier(fk.foreignKeyName)} ` + : ""; + let constraint = `${constraintName}FOREIGN KEY (${this.quoteIdentifier(field.fieldName)}) REFERENCES ${this.quoteIdentifier(fk.destinationTableName)}(${this.quoteIdentifier(fk.destinationTableColumnName)})`; + if (fk.cascadeDelete) { + constraint += " ON DELETE CASCADE"; + } + if (fk.cascadeUpdate) { + constraint += " ON UPDATE CASCADE"; + } + return constraint; + } + async syncIndexes(table) { + if (!table.indexes || table.indexes.length === 0) { + return; + } + const tableSchemaWhere = this.tableSchemaWhere(table.tableName); + const rows = await this.query(`SELECT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' GROUP BY INDEX_NAME ORDER BY INDEX_NAME`, tableSchemaWhere.values); + const existingIndexes = rows.map((row) => row.name); + for (const indexName of existingIndexes) { + const stillExists = table.indexes.some((index) => index.indexName === indexName); + if (!stillExists) { + console.log(`Dropping index: ${indexName}`); + await this.run(`DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(table.tableName)}`); + } + } + 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((field) => this.quoteIdentifier(field)) + .join(", "); + await this.run(`CREATE INDEX ${this.quoteIdentifier(index.indexName)} ON ${this.quoteIdentifier(table.tableName)} (${fields})`); + } + } + } + close() { } +} +export { MariaDBSchemaManager }; diff --git a/dist/lib/mariadb/db-schema-to-typedef.d.ts b/dist/lib/mariadb/db-schema-to-typedef.d.ts new file mode 100644 index 0000000..6b70359 --- /dev/null +++ b/dist/lib/mariadb/db-schema-to-typedef.d.ts @@ -0,0 +1,7 @@ +import type { BUN_MARIADB_DatabaseSchemaType, BunSQLiteConfig } from "../../types"; +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; + config: BunSQLiteConfig; +}; +export default function dbSchemaToType({ config, dbSchema, }: Params): string[] | undefined; +export {}; diff --git a/dist/lib/mariadb/db-schema-to-typedef.js b/dist/lib/mariadb/db-schema-to-typedef.js new file mode 100644 index 0000000..6d78c14 --- /dev/null +++ b/dist/lib/mariadb/db-schema-to-typedef.js @@ -0,0 +1,44 @@ +import _ from "lodash"; +import generateTypeDefinition from "./db-generate-type-defs"; +export default function dbSchemaToType({ config, dbSchema, }) { + let datasquirelSchema = 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 = config.db_name + ?.toUpperCase() + .replace(/[^a-zA-Z0-9]/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_MARIADB_${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_MARIADB_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}` + : ``; + return [tableNames, ...schemas, allTd]; +} diff --git a/dist/lib/mariadb/db-select.d.ts b/dist/lib/mariadb/db-select.d.ts new file mode 100644 index 0000000..ed27786 --- /dev/null +++ b/dist/lib/mariadb/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/mariadb/db-select.js b/dist/lib/mariadb/db-select.js new file mode 100644 index 0000000..8b1674f --- /dev/null +++ b/dist/lib/mariadb/db-select.js @@ -0,0 +1,58 @@ +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, }) { + let sqlObj = null; + try { + let finalQuery = query || {}; + if (targetId) { + finalQuery = _.merge(finalQuery, { + query: { + id: { + value: String(targetId), + }, + }, + }); + } + sqlObj = sqlGenerator({ + tableName: table, + genObject: finalQuery, + }); + let 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) { + let count_sql_object = sqlGenerator({ + tableName: table, + genObject: finalQuery, + count, + }); + let count_sql = mysql.format(count_sql_object.string, count_sql_object.values); + count_sql = `SELECT COUNT(*) FROM (${count_sql}) as c`; + const count_res = DbClient.query(count_sql).all(); + const count_val = count_res[0]?.["COUNT(*)"]; + resp["count"] = Number(count_val); + resp["debug"]["count_sql"] = count_sql; + } + return resp; + } + catch (error) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-sql.d.ts b/dist/lib/mariadb/db-sql.d.ts new file mode 100644 index 0000000..e528035 --- /dev/null +++ b/dist/lib/mariadb/db-sql.d.ts @@ -0,0 +1,11 @@ +import type { APIResponseObject, SQLInsertGenValueType } from "../../types"; +type Params = { + sql: string; + values?: SQLInsertGenValueType[]; +}; +export default function DbSQL({ sql, values }: Params): Promise>; +export {}; diff --git a/dist/lib/mariadb/db-sql.js b/dist/lib/mariadb/db-sql.js new file mode 100644 index 0000000..33fd427 --- /dev/null +++ b/dist/lib/mariadb/db-sql.js @@ -0,0 +1,34 @@ +import DbClient from "."; +import _ from "lodash"; +export default async function DbSQL({ sql, values }) { + try { + const trimmed_sql = sql.trim(); + const res = trimmed_sql.match(/^select/i) + ? DbClient.query(trimmed_sql).all(...(values || [])) + : DbClient.run(trimmed_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: trimmed_sql, + values, + }, + sql, + }, + }; + } + catch (error) { + return { + success: false, + error: error.message, + }; + } +} diff --git a/dist/lib/mariadb/db-update.d.ts b/dist/lib/mariadb/db-update.d.ts new file mode 100644 index 0000000..68bff0c --- /dev/null +++ b/dist/lib/mariadb/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/mariadb/db-update.js b/dist/lib/mariadb/db-update.js new file mode 100644 index 0000000..38a5daa --- /dev/null +++ b/dist/lib/mariadb/db-update.js @@ -0,0 +1,74 @@ +import DbClient from "."; +import _ from "lodash"; +import sqlGenerator from "../../utils/sql-generator"; +export default async function DbUpdate({ table, data, query, targetId, }) { + let sqlObj = { string: "", values: [] }; + 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 = { + updated_at: Date.now(), + ...data, + }; + 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}=?`; + const value = finalData[key]; + values.push(value || null); + if (!isLast) { + sql += `,`; + } + } + sql += ` ${whereClause}`; + values = [...values, ...sqlQueryObj.values]; + sqlObj.string = sql; + sqlObj.values = values; + const res = DbClient.run(sql, values); + return { + success: Boolean(res.changes), + postInsertReturn: { + affectedRows: res.changes, + insertId: Number(res.lastInsertRowid), + }, + debug: { + sqlObj, + }, + }; + } + else { + return { + success: false, + msg: `No WHERE clause`, + }; + } + } + catch (error) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/dist/lib/mariadb/schema-to-typedef.d.ts b/dist/lib/mariadb/schema-to-typedef.d.ts new file mode 100644 index 0000000..0ca673f --- /dev/null +++ b/dist/lib/mariadb/schema-to-typedef.d.ts @@ -0,0 +1,8 @@ +import type { BUN_MARIADB_DatabaseSchemaType, BunSQLiteConfig } from "../../types"; +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; + dst_file: string; + config: BunSQLiteConfig; +}; +export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }: Params): void; +export {}; diff --git a/dist/lib/mariadb/schema-to-typedef.js b/dist/lib/mariadb/schema-to-typedef.js new file mode 100644 index 0000000..f0c5ac3 --- /dev/null +++ b/dist/lib/mariadb/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, config, }) { + try { + if (!dbSchema) + throw new Error("No schema found"); + const definitions = dbSchemaToType({ dbSchema, config }); + 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/mariadb/schema.d.ts b/dist/lib/mariadb/schema.d.ts new file mode 100644 index 0000000..8ce6d90 --- /dev/null +++ b/dist/lib/mariadb/schema.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_DatabaseSchemaType } from "../../types"; +export declare const DbSchema: BUN_MARIADB_DatabaseSchemaType; diff --git a/dist/lib/mariadb/schema.js b/dist/lib/mariadb/schema.js new file mode 100644 index 0000000..5778e50 --- /dev/null +++ b/dist/lib/mariadb/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..c5bee3c --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,1417 @@ +import type { RequestOptions } from "https"; +import type { ConnectionConfig } from "mariadb"; +/** + * Fully-qualified database name used when a database needs to be referenced + * across local and remote operations. + */ +export type BUN_MARIADB_DatabaseFullName = string; +/** + * User fields that should be omitted from general-purpose payloads and public + * responses. + */ +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"]; +/** + * Describes an entire database schema, including its tables and clone/child + * database metadata. + */ +export interface BUN_MARIADB_DatabaseSchemaType { + id?: string | number; + dbName?: string; + dbSlug?: string; + dbFullName?: string; + dbDescription?: string; + dbImage?: string; + tables: BUN_MARIADB_TableSchemaType[]; + childrenDatabases?: BUN_MARIADB_ChildrenDatabaseObject[]; + childDatabase?: boolean; + childDatabaseDbId?: string | number; + updateData?: boolean; + collation?: (typeof MariaDBCollations)[number]; +} +/** + * Minimal reference to a child database linked to a parent database schema. + */ +export interface BUN_MARIADB_ChildrenDatabaseObject { + dbId?: string | number; +} +/** + * Supported MariaDB collations that can be applied at the database or table + * level. + */ +export declare const MariaDBCollations: readonly ["utf8mb4_bin", "utf8mb4_unicode_520_ci"]; +/** + * Describes a single table within a database schema, including its fields, + * indexes, unique constraints, and parent/child table relationships. + */ +export interface BUN_MARIADB_TableSchemaType { + id?: string | number; + tableName: string; + tableDescription?: string; + fields: BUN_MARIADB_FieldSchemaType[]; + indexes?: BUN_MARIADB_IndexSchemaType[]; + uniqueConstraints?: BUN_MARIADB_UniqueConstraintSchemaType[]; + childrenTables?: BUN_MARIADB_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]; + /** + * If this is a vector table + */ + isVector?: boolean; + /** + * Type of vector. Defaults to `vec0` + */ + vectorType?: string; +} +/** + * Reference object used to link a table to one of its child tables. + */ +export interface BUN_MARIADB_ChildrenTablesType { + tableId?: string | number; + dbId?: string | number; +} +/** + * Supported editor/content modes for text fields. + */ +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"; +}]; +/** + * Core SQLite column types supported by the schema builder. + */ +export declare const BUN_MARIADB_DATATYPES: readonly [{ + readonly value: "TEXT"; +}, { + readonly value: "INTEGER"; +}, { + readonly value: "BLOB"; +}, { + readonly value: "REAL"; +}]; +/** + * Describes a table column, including SQL constraints, text editor options, + * vector metadata, and foreign key configuration. + */ +export type BUN_MARIADB_FieldSchemaType = { + id?: number | string; + fieldName?: string; + fieldDescription?: string; + originName?: string; + dataType: (typeof BUN_MARIADB_DATATYPES)[number]["value"]; + nullValue?: boolean; + notNullValue?: boolean; + primaryKey?: boolean; + encrypted?: boolean; + autoIncrement?: boolean; + defaultValue?: string | number; + defaultValueLiteral?: string; + foreignKey?: BUN_MARIADB_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; + /** + * Eg. `cosine` + */ + vectorDistanceMetric?: string; +} & { + [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; +}; +/** + * Defines a foreign key relationship from one field to another table column. + */ +export interface BUN_MARIADB_ForeignKeyType { + foreignKeyName?: string; + destinationTableName?: string; + destinationTableColumnName?: string; + destinationTableColumnType?: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} +/** + * Describes a table index and the fields it covers. + */ +export interface BUN_MARIADB_IndexSchemaType { + /** + * Name of the index as it would appear on schema. Eg. + * `idx_user_id_index` + */ + indexName?: string; + indexTableFields?: string[]; +} +/** + * Describes a multi-field uniqueness rule for a table. + */ +export interface BUN_MARIADB_UniqueConstraintSchemaType { + id?: string | number; + constraintName?: string; + alias?: string; + constraintTableFields?: BUN_MARIADB_UniqueConstraintFieldType[]; +} +/** + * Single field reference inside a unique constraint definition. + */ +export interface BUN_MARIADB_UniqueConstraintFieldType { + value: string; +} +/** + * Field metadata used when generating SQL for indexes. + */ +export interface BUN_MARIADB_IndexTableFieldType { + value: string; + dataType: string; +} +/** + * Result shape returned by MySQL `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_INDEXES_Type { + Key_name: string; + Table: string; + Column_name: string; + Collation: string; + Index_type: string; + Cardinality: string; + Index_comment: string; + Comment: string; +} +/** + * Result shape returned by MySQL `SHOW COLUMNS` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_COLUMNS_Type { + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; +} +/** + * Result shape returned by MariaDB `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_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"; +} +/** + * Minimal metadata returned when listing MySQL foreign keys. + */ +export interface BUN_MARIADB_MYSQL_FOREIGN_KEYS_Type { + CONSTRAINT_NAME: string; + CONSTRAINT_SCHEMA: string; + TABLE_NAME: string; +} +/** + * Database record describing a user-owned database and optional remote sync + * metadata. + */ +export interface BUN_MARIADB_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; +} +/** + * Return value used when converting an uploaded image file to base64. + */ +export type ImageInputFileToBase64FunctionReturn = { + imageBase64?: string; + imageBase64Full?: string; + imageName?: string; + imageSize?: number; +}; +/** + * Querystring parameters accepted by generic GET data endpoints. + */ +export interface GetReqQueryObject { + db: string; + query: string; + queryValues?: string; + tableName?: string; + debug?: boolean; +} +/** + * Canonical logged-in user shape shared across auth, session, and API flows. + */ +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; +}; +/** + * Standard authenticated-user response wrapper. + */ +export interface AuthenticatedUser { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + cookieNames?: any; +} +/** + * Minimal successful user payload returned by user creation flows. + */ +export interface SuccessUserObject { + id: number; + first_name: string; + last_name: string; + email: string; +} +/** + * Return type for helpers that create a user record. + */ +export interface AddUserFunctionReturn { + success: boolean; + payload?: SuccessUserObject | null; + msg?: string; + sqlResult?: any; +} +/** + * Shape exposed by the Google Identity prompt lifecycle callback. + */ +export interface GoogleIdentityPromptNotification { + getMomentType: () => string; + getDismissedReason: () => string; + getNotDisplayedReason: () => string; + getSkippedReason: () => string; + isDismissedMoment: () => boolean; + isDisplayMoment: () => boolean; + isDisplayed: () => boolean; + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; +} +/** + * User data accepted by registration and profile-creation flows. + */ +export type UserDataPayload = { + first_name: string; + last_name: string; + email: string; + password?: string; + username?: string; +} & { + [key: string]: any; +}; +/** + * Return shape for helpers that fetch a single user. + */ +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; +} +/** + * Return type for reauthentication flows that refresh auth state. + */ +export interface ReauthUserFunctionReturn { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + token?: string; +} +/** + * Return type for user update helpers. + */ +export interface UpdateUserFunctionReturn { + success: boolean; + payload?: Object[] | string; +} +/** + * Generic success/error wrapper for read operations. + */ +export interface GetReturn { + success: boolean; + payload?: R; + msg?: string; + error?: string; + schema?: BUN_MARIADB_TableSchemaType; + finalQuery?: string; +} +/** + * Query parameters used when requesting schema metadata. + */ +export interface GetSchemaRequestQuery { + database?: string; + table?: string; + field?: string; + user_id?: string | number; + env?: { + [k: string]: string; + }; +} +/** + * API credential payload used when a schema request must be authenticated. + */ +export interface GetSchemaAPICredentialsParam { + key: string; +} +/** + * Complete request object for authenticated schema fetches. + */ +export type GetSchemaAPIParam = GetSchemaRequestQuery & GetSchemaAPICredentialsParam; +/** + * Generic response wrapper for write operations. + */ +export interface PostReturn { + success: boolean; + payload?: Object[] | string | PostInsertReturn; + msg?: string; + error?: any; + schema?: BUN_MARIADB_TableSchemaType; +} +/** + * High-level CRUD payload accepted by server-side data mutation handlers. + */ +export interface PostDataPayload { + action: "insert" | "update" | "delete"; + table: string; + data?: object; + identifierColumnName?: string; + identifierValue?: string; + duplicateColumnName?: string; + duplicateColumnValue?: string; + update?: boolean; +} +/** + * Local-only variant of `PostReturn` used by in-process operations. + */ +export interface LocalPostReturn { + success: boolean; + payload?: any; + msg?: string; + error?: string; +} +/** + * Query object for local write operations that may accept raw SQL or a CRUD + * payload. + */ +export interface LocalPostQueryObject { + query: string | PostDataPayload; + tableName?: string; + queryValues?: string[]; +} +/** + * Insert/update metadata returned by SQL drivers after a write completes. + */ +export interface PostInsertReturn { + fieldCount?: number; + affectedRows?: number; + insertId?: number; + serverStatus?: number; + warningCount?: number; + message?: string; + protocol41?: boolean; + changedRows?: number; + error?: string; +} +/** + * Extended user object used within the application runtime. + */ +export type UserType = DATASQUIREL_LoggedInUser & { + isSuperUser?: boolean; + staticHost?: string; + appHost?: string; + appName?: string; +}; +/** + * Stored API key definition and its associated metadata. + */ +export interface ApiKeyDef { + name: string; + scope: string; + date_created: string; + apiKeyPayload: string; +} +/** + * Aggregate dashboard metrics shown in admin and overview screens. + */ +export interface MetricsType { + dbCount: number; + tablesCount: number; + mediaCount: number; + apiKeysCount: number; +} +/** + * Shape of the internal MariaDB users table. + */ +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; +} +/** + * Credentials used to connect to a MariaDB instance as a specific user. + */ +export interface MariaDBUserCredType { + mariadb_user?: string; + mariadb_host?: string; + mariadb_pass?: string; +} +/** + * Supported logical operators for server-side query generation. + */ +export declare const ServerQueryOperators: readonly ["AND", "OR"]; +/** + * Supported comparison operators for server-side query generation. + */ +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", "IS NOT", "EXISTS", "NOT EXISTS", "GREATER THAN", "GREATER THAN OR EQUAL", "LESS THAN", "LESS THAN OR EQUAL", "MATCH", "MATCH_BOOLEAN"]; +/** + * Top-level query-builder input used to generate SELECT statements, joins, + * grouping, pagination, and full-text search clauses. + */ +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; + /** + * Raw SQL to use as select + */ + [key: string]: any; +}; +/** + * Represents a single `GROUP BY` field, optionally qualified by table name. + */ +export type ServerQueryParamGroupBy = { + field: keyof T; + table?: string; +}; +/** + * Represents a single `ORDER BY` clause. + */ +export type ServerQueryParamOrder = { + field: keyof T; + strategy: "ASC" | "DESC"; +}; +/** + * Configuration for a full-text search query and the alias used for the score + * column. + */ +export type ServerQueryParamFullTextSearch = { + fields: (keyof T)[]; + searchTerm: string; + /** Field Name to user to Rank the Score of Search Results */ + scoreAlias: string; +}; +/** + * Describes a count subquery that is projected into the main SELECT result. + */ +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; +}; +/** + * Source/target mapping used inside count subqueries. + */ +export type ServerQueryParamsCountSrcTrgMap = { + table: string; + field: string; +}; +/** + * Declares a selected field and an optional alias for the result set. + */ +export type TableSelectFieldsObject = { + fieldName: keyof T; + alias?: string; + count?: { + alias?: string; + }; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Omit; + distinct?: boolean; +}; +export type TableSelectFieldsBasicDirective = { + alias: string; +}; +/** + * Value wrapper used when a query condition needs per-value metadata such as a + * custom equality or explicit table/field names. + */ +export type ServerQueryValuesObject = { + value?: string | number; + /** + * Defaults to EQUAL + */ + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: string; + fieldName?: string; +}; +/** + * All value shapes accepted by a query condition, including arrays used by + * operators such as `IN` and `BETWEEN`. + */ +export type ServerQueryObjectValue = string | number | ServerQueryValuesObject | undefined | null | (string | number | ServerQueryValuesObject | undefined | null)[]; +/** + * Describes a single query condition and any nested subconditions for a field. + */ +export type ServerQueryObject = SQLComparisonsParams & { + 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: string]: 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; +}; +/** + * Field-to-condition map for server-side query generation. + */ +export type ServerQueryQueryObject = { + [key in keyof T]: ServerQueryObject; +}; +/** + * Parameters for authenticated fetch helpers used by the frontend and internal + * SDK layers. + */ +export type FetchDataParams = { + path: string; + method?: (typeof DataCrudRequestMethods)[number]; + body?: object | string; + query?: AuthFetchQuery; + tableName?: string; +}; +/** + * Query object accepted by authenticated data-fetch helpers. + */ +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: any; +}; +/** + * Join clause definition used by the server query builder. + */ +export type ServerQueryParamsJoin = { + joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; + alias?: string; + tableName: Table; + match?: ServerQueryParamsJoinMatchObject | ServerQueryParamsJoinMatchObject[]; + selectFields?: (keyof Field | SelectFieldObject)[]; + omitFields?: (keyof Field | { + field: keyof Field; + alias?: string; + count?: boolean; + })[]; + operator?: (typeof ServerQueryOperators)[number]; + /** + * Raw SQL to use as join select + */ + /** + * Concatenate multiple matches from another table + */ + group_concat?: GroupConcatObject; +}; +export type GroupConcatObject = { + field: string; + alias: string; + /** + * Separator. Default `,` + */ + separator?: string; + distinct?: boolean; +}; +export type SelectFieldObject = { + field: keyof Field; + alias?: string; + count?: boolean; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Pick; + distinct?: boolean; +}; +export declare const SQlComparisons: readonly [">", "<>", "<", "=", ">=", "<=", "!=", "IS NOT", "IS", "IS NULL", "IS NOT NULL", "IN", "NOT IN", "LIKE", "NOT LIKE", "GLOB", "NOT GLOB"]; +export type SQLBetween = { + min: SQLInsertGenValueType; + max: SQLInsertGenValueType; +}; +export type SQLComparisonsParams = { + raw_equality?: (typeof SQlComparisons)[number]; + between?: SQLBetween; + not_between?: SQLBetween; +}; +/** + * Defines how a root-table field maps to a join-table field in an `ON` clause. + */ +export type ServerQueryParamsJoinMatchObject = SQLComparisonsParams & { + /** 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"; + }; +}; +/** + * Explicit table/field reference for join match definitions. + */ +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; +/** + * Payload used when pushing or pulling a schema to or from a remote API. + */ +export type ApiConnectBody = { + url: string; + key: string; + database: BUN_MARIADB_MYSQL_user_databases_Type; + dbSchema: BUN_MARIADB_DatabaseSchemaType; + type: "pull" | "push"; + user_id?: string | number; +}; +/** + * Superuser credentials and auth state. + */ +export type SuUserType = { + email: string; + password: string; + authKey: string; + logged_in_status: boolean; + date: number; +}; +/** + * Configuration for a remote MariaDB host in replicated or load-balanced + * setups. + */ +export type MariadbRemoteServerObject = { + host: string; + port: number; + primary?: boolean; + loadBalanced?: boolean; + users?: MariadbRemoteServerUserObject[]; +}; +/** + * Credentials for a user provisioned on a remote MariaDB server. + */ +export type MariadbRemoteServerUserObject = { + name: string; + password: string; + host: string; +}; +/** + * Standard login response returned by API authentication helpers. + */ +export type APILoginFunctionReturn = { + success: boolean; + msg?: string; + payload?: DATASQUIREL_LoggedInUser | null; + userId?: number | string; + key?: string; + token?: string; + csrf?: string; + cookieNames?: any; +}; +/** + * Parameters required to create a user through the public API layer. + */ +export type APICreateUserFunctionParams = { + encryptionKey?: string; + payload: any; + database: string; + dsqlUserID?: string | number; + verify?: boolean; +}; +/** + * Function signature for API user-creation handlers. + */ +export type APICreateUserFunction = (params: APICreateUserFunctionParams) => Promise; +/** + * Result returned when reconciling a social login with the local user store. + */ +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; +/** + * Cookie definition used when setting auth/session cookies. + */ +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"; +}; +/** + * Request options accepted by the generic HTTP client helper. + */ +export type HttpRequestParams = RequestOptions & { + scheme?: "http" | "https"; + body?: ReqObj; + query?: ReqObj; + urlEncodedFormBody?: boolean; +}; +/** + * Function signature for the generic HTTP request helper. + */ +export type HttpRequestFunction = (param: HttpRequestParams) => Promise>; +/** + * Normalized response returned by the generic HTTP request helper. + */ +export type HttpFunctionResponse = { + status: number; + data?: ResObj; + error?: string; + str?: string; + requestedPath?: string; +}; +/** + * GET request payload for API data reads. + */ +export type ApiGetQueryObject = { + query: ServerQueryParam; + table: string; + dbFullName?: string; +}; +/** + * Uppercase HTTP methods supported by the CRUD helpers. + */ +export declare const DataCrudRequestMethods: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]; +/** + * Lowercase variant of the supported HTTP methods, used where string casing + * must match external APIs. + */ +export declare const DataCrudRequestMethodsLowerCase: readonly ["get", "post", "put", "patch", "delete", "options"]; +/** + * Runtime parameters passed to generic CRUD handlers. + */ +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; +}; +/** + * Hook used to mutate input data before a CRUD action is executed. + */ +export type DsqlCrudTransformDataFunction = (params: { + data: T; + user?: DATASQUIREL_LoggedInUser; + existingData?: T; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise; +/** + * Hook used to mutate query input before a CRUD action is executed. + */ +export type DsqlCrudTransformQueryFunction = (params: { + query: DsqlCrudQueryObject; + user?: DATASQUIREL_LoggedInUser; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise>; +/** + * High-level CRUD actions supported by the DSQL helpers. + */ +export declare const DsqlCrudActions: readonly ["insert", "update", "delete", "get"]; +/** + * Query object used by CRUD helpers, built on top of the server query builder. + */ +export type DsqlCrudQueryObject = ServerQueryParam & { + query?: ServerQueryQueryObject; +}; +/** + * Parameters used to generate a SQL `DELETE` statement. + */ +export type SQLDeleteGeneratorParams = { + tableName: string; + deleteKeyValues?: SQLDeleteData[]; + deleteKeyValuesOperator?: "AND" | "OR"; + dbFullName?: string; + data?: any; +}; +/** + * Single key/value predicate used by the delete SQL generator. + */ +export type SQLDeleteData = { + key: keyof T; + value: string | number | null | undefined; + operator?: (typeof ServerQueryEqualities)[number]; +}; +/** + * Prebuilt SQL where-clause fragment and its bound parameters. + */ +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; +/** + * Callback signature used when surfacing internal errors to callers. + */ +export type ErrorCallback = (title: string, error: Error, data?: any) => void; +/** + * Reserved query parameter names used throughout the API layer. + */ +export declare const QueryFields: readonly ["duplicate", "user_id", "delegated_user_id", "db_id", "table_id", "db_slug"]; +/** + * Minimal representation of a local folder entry. + */ +export type LocalFolderType = { + name: string; + isPrivate: boolean; +}; +/** + * SQL string and bound parameters produced during query generation. + */ +export type ResponseQueryObject = { + sql?: string; + params?: (string | number)[]; +}; +/** + * Common API response wrapper used by data, auth, media, and utility + * endpoints. + */ +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; +}; +/** + * Supported Docker Compose service names used by the deployment helpers. + */ +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"]; +/** + * Map of compose service names to service definitions. + */ +export type DockerComposeServicesType = { + [key in (typeof DockerComposeServices)[number]]: DockerComposeServiceWithBuildObject; +}; +/** + * Docker Compose network definitions. + */ +export type DockerComposeNetworks = { + [k: string]: { + driver?: "bridge"; + ipam?: { + config: DockerComposeNetworkConfigObject[]; + }; + external?: boolean; + }; +}; +/** + * Static network config for a Docker Compose network. + */ +export type DockerComposeNetworkConfigObject = { + subnet: string; + gateway: string; +}; +/** + * Service definition for compose services that are built from source. + */ +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; +}; +/** + * Service definition for compose services that use a prebuilt image. + */ +export type DockerComposeServiceWithImage = Omit & { + image: string; +}; +/** + * Build instructions for a Docker Compose service. + */ +export type DockerComposeServicesBuildObject = { + context: string; + dockerfile: string; +}; +/** + * Per-network addressing information for a Docker Compose service. + */ +export type DockerComposeServiceNetworkObject = { + [k: string]: { + ipv4_address: string; + }; +}; +/** + * Metadata recorded for a table cloned from another database. + */ +export type ClonedTableInfo = { + dbId?: string | number; + tableId?: string | number; + keepUpdated?: boolean; + keepDataUpdated?: boolean; +}; +/** + * Common columns present on default/generated table entries. + */ +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; +}; +/** + * Supported index strategies for generated schemas. + */ +export declare const IndexTypes: readonly ["regular", "full_text", "vector"]; +/** + * Structured error payload captured alongside generated SQL. + */ +export type BUNSQLITEErrorObject = { + sql?: string; + sqlValues?: any[]; + error?: string; +}; +/** + * Output of the SQL insert generator. + */ +export interface SQLInsertGenReturn { + query: string; + values: SQLInsertGenValueType[]; +} +/** + * Values accepted by the SQL insert generator, including vector buffers. + */ +export type SQLInsertGenValueType = string | number | Float32Array | Buffer | null; +/** + * Callback variant used when a generated insert value needs a custom + * placeholder. + */ +export type SQLInsertGenDataFn = () => { + placeholder: string; + value: SQLInsertGenValueType; +}; +/** + * Record shape accepted by the SQL insert generator. + */ +export type SQLInsertGenDataType = { + [k: string]: SQLInsertGenValueType | SQLInsertGenDataFn | undefined | null; +}; +/** + * Parameters used to build a multi-row SQL insert statement. + */ +export type SQLInsertGenParams = { + data: SQLInsertGenDataType[]; + tableName: string; + dbFullName?: string; +}; +/** + * Library configuration used to locate the SQLite database, schema file, + * generated types, and backup settings. + */ +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; + /** + * Whether to enable `WAL` mode + */ + wal_mode?: boolean; +}; +/** + * Resolved Bun SQLite config paired with the loaded database schema. + */ +export type BunSQLiteConfigReturn = { + config: BunSQLiteConfig; + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; +/** + * Default fields automatically suggested for new tables. + */ +export declare const DefaultFields: BUN_MARIADB_FieldSchemaType[]; +export type BunSQLiteQueryFieldValues = { + field: F; + table?: T; +}; +export type QueryRawValueType = string | number | null | undefined; +export type DsqlConnectionParam = { + /** + * No Database Connection + */ + noDb?: boolean; + /** + * Database Name + */ + database?: string; + /** + * Debug + */ + config?: ConnectionConfig; +}; +export type DBResponseObject = { + success: boolean; + payload?: T[]; + single_res?: T; +}; diff --git a/dist/types/index.js b/dist/types/index.js new file mode 100644 index 0000000..dba0d06 --- /dev/null +++ b/dist/types/index.js @@ -0,0 +1,187 @@ +/** + * User fields that should be omitted from general-purpose payloads and public + * responses. + */ +export const UsersOmitedFields = [ + "password", + "social_id", + "verification_status", + "date_created", + "date_created_code", + "date_created_timestamp", + "date_updated", + "date_updated_code", + "date_updated_timestamp", +]; +/** + * Supported MariaDB collations that can be applied at the database or table + * level. + */ +export const MariaDBCollations = [ + "utf8mb4_bin", + "utf8mb4_unicode_520_ci", +]; +/** + * Supported editor/content modes for text fields. + */ +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" }, +]; +/** + * Core SQLite column types supported by the schema builder. + */ +export const BUN_MARIADB_DATATYPES = [ + { value: "TEXT" }, + { value: "INTEGER" }, + { value: "BLOB" }, + { value: "REAL" }, +]; +/** + * Supported logical operators for server-side query generation. + */ +export const ServerQueryOperators = ["AND", "OR"]; +/** + * Supported comparison operators for server-side query generation. + */ +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", + "IS NOT", + "EXISTS", + "NOT EXISTS", + "GREATER THAN", + "GREATER THAN OR EQUAL", + "LESS THAN", + "LESS THAN OR EQUAL", + "MATCH", + "MATCH_BOOLEAN", +]; +export const SQlComparisons = [ + ">", + "<>", + "<", + "=", + ">=", + "<=", + "!=", + "IS NOT", + "IS", + "IS NULL", + "IS NOT NULL", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "GLOB", + "NOT GLOB", +]; +/** + * Uppercase HTTP methods supported by the CRUD helpers. + */ +export const DataCrudRequestMethods = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", +]; +/** + * Lowercase variant of the supported HTTP methods, used where string casing + * must match external APIs. + */ +export const DataCrudRequestMethodsLowerCase = [ + "get", + "post", + "put", + "patch", + "delete", + "options", +]; +/** + * High-level CRUD actions supported by the DSQL helpers. + */ +export const DsqlCrudActions = ["insert", "update", "delete", "get"]; +/** + * Reserved query parameter names used throughout the API layer. + */ +export const QueryFields = [ + "duplicate", + "user_id", + "delegated_user_id", + "db_id", + "table_id", + "db_slug", +]; +/** + * Supported Docker Compose service names used by the deployment helpers. + */ +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", +]; +/** + * Supported index strategies for generated schemas. + */ +export const IndexTypes = ["regular", "full_text", "vector"]; +/** + * Default fields automatically suggested for new tables. + */ +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..9d031fc --- /dev/null +++ b/dist/utils/append-default-fields-to-db-schema.d.ts @@ -0,0 +1,6 @@ +import { type BUN_MARIADB_DatabaseSchemaType } from "../types"; +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; +export default function ({ dbSchema }: Params): BUN_MARIADB_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-db-schema.d.ts b/dist/utils/grab-db-schema.d.ts new file mode 100644 index 0000000..c80395a --- /dev/null +++ b/dist/utils/grab-db-schema.d.ts @@ -0,0 +1 @@ +export default function grabDbSchema(): Promise; diff --git a/dist/utils/grab-db-schema.js b/dist/utils/grab-db-schema.js new file mode 100644 index 0000000..4780edb --- /dev/null +++ b/dist/utils/grab-db-schema.js @@ -0,0 +1,5 @@ +import init from "../functions/init"; +export default async function grabDbSchema() { + const { dbSchema } = await init(); + return dbSchema; +} diff --git a/dist/utils/grab-join-fields-from-query-object.d.ts b/dist/utils/grab-join-fields-from-query-object.d.ts new file mode 100644 index 0000000..dd16a58 --- /dev/null +++ b/dist/utils/grab-join-fields-from-query-object.d.ts @@ -0,0 +1,7 @@ +import type { BunSQLiteQueryFieldValues, ServerQueryParam } from "../types"; +type Params = Record> = { + query: ServerQueryParam; + ignore_select_fields?: boolean; +}; +export default function grabJoinFieldsFromQueryObject = Record, F extends string = string, T extends string = string>({ query, ignore_select_fields, }: Params): BunSQLiteQueryFieldValues[]; +export {}; diff --git a/dist/utils/grab-join-fields-from-query-object.js b/dist/utils/grab-join-fields-from-query-object.js new file mode 100644 index 0000000..2672c70 --- /dev/null +++ b/dist/utils/grab-join-fields-from-query-object.js @@ -0,0 +1,55 @@ +import _ from "lodash"; +export default function grabJoinFieldsFromQueryObject({ query, ignore_select_fields, }) { + const fields_values = []; + const new_query = _.cloneDeep(query); + if (new_query.join) { + for (let i = 0; i < new_query.join.length; i++) { + const join = new_query.join[i]; + if (!join) + continue; + if (Array.isArray(join)) { + for (let i = 0; i < join.length; i++) { + const single_join = join[i]; + fields_values.push(...grabSingleJoinData({ + join: single_join, + ignore_select_fields, + })); + } + } + else { + fields_values.push(...grabSingleJoinData({ + join: join, + ignore_select_fields, + })); + } + } + } + return fields_values; +} +function grabSingleJoinData({ join, ignore_select_fields, }) { + let values = []; + const join_select_fields = join?.selectFields; + if (!join_select_fields?.[0] && !ignore_select_fields) { + throw new Error(`\`selectFields\` required in joins. To ignore this error, pass the \`ignore_select_fields\` parameter`); + } + if (join_select_fields?.[0]) { + for (let i = 0; i < join_select_fields.length; i++) { + const select_field = join_select_fields[i]; + if (select_field) { + values.push({ + table: join.tableName, + field: typeof select_field == "object" + ? String(select_field.field) + : String(select_field), + }); + } + } + } + if (join.group_concat) { + values.push({ + table: join.tableName, + field: join.group_concat.field, + }); + } + return values; +} 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/query-value-parser.d.ts b/dist/utils/query-value-parser.d.ts new file mode 100644 index 0000000..bf5460c --- /dev/null +++ b/dist/utils/query-value-parser.d.ts @@ -0,0 +1,6 @@ +import type { QueryRawValueType, ServerQueryObjectValue } from "../types"; +type Params = { + query_value: ServerQueryObjectValue; +}; +export default function queryValueParser({ query_value, }: Params): QueryRawValueType | QueryRawValueType[]; +export {}; diff --git a/dist/utils/query-value-parser.js b/dist/utils/query-value-parser.js new file mode 100644 index 0000000..dfa68ea --- /dev/null +++ b/dist/utils/query-value-parser.js @@ -0,0 +1,21 @@ +export default function queryValueParser({ query_value, }) { + if (typeof query_value == "string" || typeof query_value == "number") { + return query_value; + } + if (Array.isArray(query_value)) { + let values = []; + for (let i = 0; i < query_value.length; i++) { + const single_value = query_value[i]; + if (single_value) { + const single_parsed_value = queryValueParser({ + query_value: single_value, + }); + if (!Array.isArray(single_parsed_value)) { + values.push(single_parsed_value); + } + } + } + return values; + } + return query_value?.value; +} 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..77dc7a9 --- /dev/null +++ b/dist/utils/sql-equality-parser.js @@ -0,0 +1,41 @@ +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 "IS NOT": + return "IS NOT"; + 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..d6adb66 --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.d.ts @@ -0,0 +1,20 @@ +import type { ServerQueryEqualities, ServerQueryObject, SQLInsertGenValueType } from "../types"; +type Params = { + fieldName: string; + value?: SQLInsertGenValueType; + equality?: (typeof ServerQueryEqualities)[number]; + queryObj: ServerQueryObject<{ + [key: string]: any; + }, string>; + isValueFieldValue?: boolean; +}; +type Return = { + str?: string; + param?: SQLInsertGenValueType; +}; +/** + * # 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..9e23fc4 --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.js @@ -0,0 +1,133 @@ +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 == "IS NOT") { + return { + str: `${fieldName} IS NOT ${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-gen-join-str.d.ts b/dist/utils/sql-generator-gen-join-str.d.ts new file mode 100644 index 0000000..5dbf8cb --- /dev/null +++ b/dist/utils/sql-generator-gen-join-str.d.ts @@ -0,0 +1,11 @@ +import type { ServerQueryParamsJoin, ServerQueryParamsJoinMatchObject, SQLInsertGenValueType } from "../types"; +type Param = { + mtch: ServerQueryParamsJoinMatchObject; + join: ServerQueryParamsJoin; + table_name: string; +}; +export default function sqlGenGenJoinStr({ join, mtch, table_name }: Param): { + str: string; + values: SQLInsertGenValueType[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-join-str.js b/dist/utils/sql-generator-gen-join-str.js new file mode 100644 index 0000000..0120afa --- /dev/null +++ b/dist/utils/sql-generator-gen-join-str.js @@ -0,0 +1,65 @@ +export default function sqlGenGenJoinStr({ join, mtch, table_name }) { + let values = []; + if (mtch.__batch) { + let btch_mtch = ``; + btch_mtch += `(`; + for (let i = 0; i < mtch.__batch.matches.length; i++) { + const __mtch = mtch.__batch.matches[i]; + const { str, values: batch_values } = sqlGenGenJoinStr({ + join, + mtch: __mtch, + table_name, + }); + btch_mtch += str; + values.push(...batch_values); + if (i < mtch.__batch.matches.length - 1) { + btch_mtch += ` ${mtch.__batch.operator || "OR"} `; + } + } + btch_mtch += `)`; + return { + str: btch_mtch, + values, + }; + } + const equality = mtch.raw_equality || "="; + const lhs = `${typeof mtch.source == "object" ? mtch.source.tableName : table_name}.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}`; + const rhs = `${(() => { + if (mtch.targetLiteral) { + values.push(mtch.targetLiteral); + // if (typeof mtch.targetLiteral == "number") { + // return `${mtch.targetLiteral}`; + // } + // return `'${mtch.targetLiteral}'`; + return `?`; + } + if (join.alias) { + return `${typeof mtch.target == "object" + ? mtch.target.tableName + : join.alias}.${typeof mtch.target == "object" + ? mtch.target.fieldName + : mtch.target}`; + } + return `${typeof mtch.target == "object" + ? mtch.target.tableName + : join.tableName}.${typeof mtch.target == "object" ? mtch.target.fieldName : mtch.target}`; + })()}`; + if (mtch.between) { + values.push(mtch.between.min, mtch.between.max); + return { + str: `${lhs} BETWEEN ? AND ?`, + values, + }; + } + if (mtch.not_between) { + values.push(mtch.not_between.min, mtch.not_between.max); + return { + str: `${lhs} NOT BETWEEN ? AND ?`, + values, + }; + } + return { + str: `${lhs} ${equality} ${rhs}`, + values, + }; +} diff --git a/dist/utils/sql-generator-gen-query-str.d.ts b/dist/utils/sql-generator-gen-query-str.d.ts new file mode 100644 index 0000000..886d00a --- /dev/null +++ b/dist/utils/sql-generator-gen-query-str.d.ts @@ -0,0 +1,22 @@ +import type { ServerQueryParam, TableSelectFieldsObject } from "../types"; +type Param = { + genObject?: ServerQueryParam; + selectFields?: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; + full_text_match_str?: string; + full_text_search_str?: string; +}; +export default function sqlGenGenQueryStr(params: Param): { + str: string; + values: any[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-query-str.js b/dist/utils/sql-generator-gen-query-str.js new file mode 100644 index 0000000..16fa389 --- /dev/null +++ b/dist/utils/sql-generator-gen-query-str.js @@ -0,0 +1,193 @@ +import { isUndefined } from "lodash"; +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; +import sqlGenGenJoinStr from "./sql-generator-gen-join-str"; +import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql"; +export default function sqlGenGenQueryStr(params) { + let str = "SELECT"; + const genObject = params.genObject; + const table_name = params.table_name; + const full_text_match_str = params.full_text_match_str; + const full_text_search_str = params.full_text_search_str; + let sqlSearhValues = []; + if (genObject?.select_sql) { + str += ` ${genObject.select_sql}`; + } + else if (genObject?.selectFields?.[0]) { + if (genObject.join) { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + append_table_names: true, + table_name, + }); + } + else { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + table_name, + }); + } + } + else { + if (genObject?.join) { + str += ` ${table_name}.*`; + } + 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) { + const existingJoinTableNames = [table_name]; + 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.group_concat) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${joinObj.group_concat.field}`, + alias: joinObj.group_concat.alias, + separator: joinObj.group_concat.separator, + }); + } + else if (joinObj.selectFields) { + return joinObj.selectFields + .map((selectField) => { + if (typeof selectField == "string") { + return `${joinTableName}.${selectField}`; + } + else if (typeof selectField == "object") { + let aliasSelectField = `${joinTableName}.${selectField.field}`; + if (selectField.count) { + aliasSelectField = `COUNT(${joinTableName}.${selectField.field})`; + } + else if (selectField.sum) { + aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${joinTableName}.${selectField.field})`; + } + else if (selectField.average) { + aliasSelectField = `AVERAGE(${joinTableName}.${selectField.field})`; + } + else if (selectField.max) { + aliasSelectField = `MAX(${joinTableName}.${selectField.field})`; + } + else if (selectField.min) { + aliasSelectField = `MIN(${joinTableName}.${selectField.field})`; + } + else if (selectField.group_concat && + selectField.alias) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${selectField.field}`, + alias: selectField.alias, + separator: selectField.group_concat + .separator, + distinct: selectField.group_concat + .distinct, + }); + } + else if (selectField.distinct) { + aliasSelectField = `DISTINCT ${joinTableName}.${selectField.field}`; + } + if (selectField.alias) + aliasSelectField += ` AS ${selectField.alias}`; + return aliasSelectField; + } + }) + .join(","); + } + else { + return `${joinTableName}.*`; + } + }) + .filter((_) => Boolean(_)) + .join(","); + } + if (genObject?.fullTextSearch && + full_text_match_str && + full_text_search_str) { + str += `, ${full_text_match_str} AS ${genObject.fullTextSearch.scoreAlias}`; + sqlSearhValues.push(full_text_search_str); + } + str += ` FROM ${table_name}`; + if (genObject?.join) { + str += + " " + + genObject.join + .flat() + .filter((j) => !isUndefined(j)) + .map((join) => { + return (join.joinType + + " " + + (join.alias + ? `${join.tableName}` + " " + join.alias + : `${join.tableName}`) + + " ON " + + (() => { + if (Array.isArray(join.match)) { + return ("(" + + join.match + .map((mtch) => { + const { str, values } = sqlGenGenJoinStr({ + mtch, + join, + table_name, + }); + sqlSearhValues.push(...values); + return str; + }) + .join(join.operator + ? ` ${join.operator} ` + : " AND ") + + ")"); + } + else if (typeof join.match == "object") { + const { str, values } = sqlGenGenJoinStr({ + mtch: join.match, + join, + table_name, + }); + sqlSearhValues.push(...values); + return str; + } + })()); + }) + .join(" "); + } + return { str, values: sqlSearhValues }; +} diff --git a/dist/utils/sql-generator-gen-search-str.d.ts b/dist/utils/sql-generator-gen-search-str.d.ts new file mode 100644 index 0000000..563daf8 --- /dev/null +++ b/dist/utils/sql-generator-gen-search-str.d.ts @@ -0,0 +1,12 @@ +import type { ServerQueryParamsJoin, ServerQueryQueryObject, SQLInsertGenValueType } from "../types"; +type Param = { + queryObj: ServerQueryQueryObject[string]; + join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[]; + field?: string; + table_name: string; +}; +export default function sqlGenGenSearchStr({ queryObj, join, field, table_name, }: Param): { + str: string; + values: SQLInsertGenValueType[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-search-str.js b/dist/utils/sql-generator-gen-search-str.js new file mode 100644 index 0000000..e7cf269 --- /dev/null +++ b/dist/utils/sql-generator-gen-search-str.js @@ -0,0 +1,92 @@ +import sqlGenOperatorGen from "./sql-gen-operator-gen"; +export default function sqlGenGenSearchStr({ queryObj, join, field, table_name, }) { + let sqlSearhValues = []; + const finalFieldName = (() => { + if (queryObj?.tableName) { + return `${queryObj.tableName}.${field}`; + } + if (join) { + return `${table_name}.${field}`; + } + return field; + })(); + let str = `${finalFieldName}=?`; + function grabValue(val) { + const valueParsed = val; + if (!valueParsed) + return; + const valueString = typeof valueParsed == "string" || typeof valueParsed == "number" + ? valueParsed + : valueParsed + ? valueParsed.fieldName && valueParsed.tableName + ? `${valueParsed.tableName}.${valueParsed.fieldName}` + : valueParsed.value + : undefined; + const valueEquality = typeof valueParsed == "object" + ? valueParsed.equality || queryObj.equality + : queryObj.equality; + const operatorStrParam = sqlGenOperatorGen({ + queryObj, + equality: valueEquality, + fieldName: finalFieldName || "", + value: valueString || "", + 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 if (queryObj.raw_equality && queryObj.value) { + str = `${finalFieldName} ${queryObj.raw_equality} ?`; + sqlSearhValues.push(queryObj.value); + } + else if (queryObj.between) { + str = `${finalFieldName} BETWEEN ? AND ?`; + sqlSearhValues.push(queryObj.between.min, queryObj.between.max); + } + else { + const valueParsed = queryObj.value ? 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 && !operatorStrParam.str.match(/\?/)) { + str = operatorStrParam.str; + } + else { + sqlSearhValues.push(valueParsed || ""); + } + } + return { str, values: sqlSearhValues }; +} diff --git a/dist/utils/sql-generator-grab-concat-str.d.ts b/dist/utils/sql-generator-grab-concat-str.d.ts new file mode 100644 index 0000000..3eb39c7 --- /dev/null +++ b/dist/utils/sql-generator-grab-concat-str.d.ts @@ -0,0 +1,8 @@ +type Param = { + field: string; + alias: string; + separator?: string; + distinct?: boolean; +}; +export default function sqlGenGrabConcatStr({ alias, field, separator, distinct, }: Param): string; +export {}; diff --git a/dist/utils/sql-generator-grab-concat-str.js b/dist/utils/sql-generator-grab-concat-str.js new file mode 100644 index 0000000..8993f00 --- /dev/null +++ b/dist/utils/sql-generator-grab-concat-str.js @@ -0,0 +1,13 @@ +export default function sqlGenGrabConcatStr({ alias, field, separator = ",", distinct, }) { + let gc = `GROUP_CONCAT(`; + if (distinct) { + gc += `DISTINCT `; + } + gc += `${field}`; + if (!distinct) { + gc += `, '${separator}'`; + } + gc += `)`; + gc += ` AS ${alias}`; + return gc; +} diff --git a/dist/utils/sql-generator-grab-select-field-sql.d.ts b/dist/utils/sql-generator-grab-select-field-sql.d.ts new file mode 100644 index 0000000..83d13cc --- /dev/null +++ b/dist/utils/sql-generator-grab-select-field-sql.d.ts @@ -0,0 +1,16 @@ +import type { TableSelectFieldsObject } from "../types"; +type Param = { + selectFields: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; +}; +export default function sqlGenGrabSelectFieldSQL({ selectFields, append_table_names, table_name }: Param): string; +export {}; diff --git a/dist/utils/sql-generator-grab-select-field-sql.js b/dist/utils/sql-generator-grab-select-field-sql.js new file mode 100644 index 0000000..4056788 --- /dev/null +++ b/dist/utils/sql-generator-grab-select-field-sql.js @@ -0,0 +1,55 @@ +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; +export default function sqlGenGrabSelectFieldSQL({ selectFields, append_table_names, table_name }) { + let str = ""; + str += ` ${selectFields + ?.map((fld) => { + let fld_str = ``; + const final_fld_name = typeof fld == "object" + ? append_table_names + ? `${table_name}.${String(fld)}` + : `${String(fld.fieldName)}` + : `${String(fld)}`; + if (typeof fld == "object") { + const fld_name = `${String(fld.fieldName)}`; + if (fld.count) { + fld_str += `COUNT(${fld_name})`; + } + else if (fld.sum) { + fld_str += `SUM(${fld_name})`; + } + else if (fld.average) { + fld_str += `AVERAGE(${fld_name})`; + } + else if (fld.max) { + fld_str += `MAX(${fld_name})`; + } + else if (fld.min) { + fld_str += `MIN(${fld_name})`; + } + else if (fld.distinct) { + fld_str += `DISTINCT ${fld_name}`; + } + else if (fld.group_concat) { + fld_str += sqlGenGrabConcatStr({ + field: fld_name, + alias: fld.group_concat.alias, + separator: fld.group_concat.separator, + distinct: fld.group_concat.distinct, + }); + } + else { + fld_str += + final_fld_name + (fld.alias ? ` as ${fld.alias}` : ``); + } + if (fld.alias) { + fld_str += ` AS ${fld.alias}`; + } + } + else { + fld_str += final_fld_name; + } + return fld_str; + }) + .join(",")}`; + return str; +} diff --git a/dist/utils/sql-generator.d.ts b/dist/utils/sql-generator.d.ts new file mode 100644 index 0000000..b6eea8f --- /dev/null +++ b/dist/utils/sql-generator.d.ts @@ -0,0 +1,25 @@ +import type { ServerQueryParam, SQLInsertGenValueType } from "../types"; +type Param = { + genObject?: ServerQueryParam; + tableName: string; + dbFullName?: string; + count?: boolean; +}; +type Return = { + string: string; + values: SQLInsertGenValueType[]; +}; +/** + * # 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..16aa373 --- /dev/null +++ b/dist/utils/sql-generator.js @@ -0,0 +1,303 @@ +import sqlGenGenSearchStr from "./sql-generator-gen-search-str"; +import sqlGenGenQueryStr from "./sql-generator-gen-query-str"; +/** + * # 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 = []; + 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 { str: queryString, values } = sqlGenGenQueryStr({ + table_name: tableName, + append_table_names: true, + full_text_match_str: fullTextMatchStr, + full_text_search_str: fullTextSearchStr, + genObject, + }); + sqlSearhValues.push(...values); + 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) { + const { str, values } = sqlGenGenSearchStr({ + queryObj: newSubQueryObj, + field: newSubQueryObj.fieldName || _field, + join: genObject?.join, + table_name: tableName, + }); + sqlSearhValues.push(...values); + return str; + } + }); + return ("(" + + subSearchString.join(` ${queryObj.operator || "AND"} `) + + ")"); + } + const { str, values } = sqlGenGenSearchStr({ + queryObj, + field: queryObj.fieldName || field, + join: genObject?.join, + table_name: tableName, + }); + sqlSearhValues.push(...values); + return str; + }); + 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(`${tableName}.${String(order.field)}`); + } + else { + orderFields.push(order.field); + } + orderSrt += ` ${orderFields.join(", ")} ${order.strategy}`; + return orderSrt; + } + if (genObject?.order) { + 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) { + queryString += ` OFFSET ${genObject.offset}`; + } + else if (genObject?.page && genObject.limit && !count) { + queryString += ` OFFSET ${(genObject.page - 1) * genObject.limit}`; + } + return { + string: queryString, + values: sqlSearhValues, + }; +} +// let queryString = (() => { +// let str = "SELECT"; +// if (genObject?.select_sql) { +// str += ` ${genObject.select_sql}`; +// } else if (genObject?.selectFields?.[0]) { +// if (genObject.join) { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// append_table_names: true, +// table_name: tableName, +// }); +// } else { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// table_name: tableName, +// }); +// } +// } else { +// if (genObject?.join) { +// str += ` ${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) { +// 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.group_concat) { +// return sqlGenGrabConcatStr({ +// field: `${joinTableName}.${joinObj.group_concat.field}`, +// alias: joinObj.group_concat.alias, +// separator: joinObj.group_concat.separator, +// }); +// } else if (joinObj.selectFields) { +// return joinObj.selectFields +// .map((selectField) => { +// if (typeof selectField == "string") { +// return `${joinTableName}.${selectField}`; +// } else if (typeof selectField == "object") { +// let aliasSelectField = selectField.count +// ? `COUNT(${joinTableName}.${selectField.field})` +// : `${joinTableName}.${selectField.field}`; +// if (selectField.alias) +// aliasSelectField += ` AS ${selectField.alias}`; +// return aliasSelectField; +// } +// }) +// .join(","); +// } else { +// return `${joinTableName}.*`; +// } +// }) +// .filter((_) => Boolean(_)) +// .join(","); +// } +// if ( +// genObject?.fullTextSearch && +// fullTextMatchStr && +// fullTextSearchStr +// ) { +// str += `, ${fullTextMatchStr} AS ${genObject.fullTextSearch.scoreAlias}`; +// sqlSearhValues.push(fullTextSearchStr); +// } +// str += ` FROM ${tableName}`; +// if (genObject?.join) { +// str += +// " " + +// genObject.join +// .flat() +// .filter((j) => !isUndefined(j)) +// .map((join) => { +// return ( +// join.joinType + +// " " + +// (join.alias +// ? `${join.tableName}` + " " + join.alias +// : `${join.tableName}`) + +// " ON " + +// (() => { +// if (Array.isArray(join.match)) { +// return ( +// "(" + +// join.match +// .map((mtch) => +// sqlGenGenJoinStr({ +// mtch, +// join, +// table_name: tableName, +// }), +// ) +// .join( +// join.operator +// ? ` ${join.operator} ` +// : " AND ", +// ) + +// ")" +// ); +// } else if (typeof join.match == "object") { +// return sqlGenGenJoinStr({ +// mtch: join.match, +// join, +// table_name: tableName, +// }); +// } +// })() +// ); +// }) +// .join(" "); +// } +// return str; +// })(); 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..2168122 --- /dev/null +++ b/dist/utils/sql-insert-generator.js @@ -0,0 +1,58 @@ +/** + * # 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 + : typeof value == "function" + ? value().value + : value + ? value + : null; + if (!finalValue) { + queryValues.push(null); + 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.json b/package.json new file mode 100644 index 0000000..2d14448 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "@moduletrace/bun-mariadb", + "version": "1.1.11", + "description": "Mariadb manager for Bun", + "author": "Benjamin Toby", + "main": "dist/index.js", + "bin": { + "bun-mariadb": "dist/commands/index.js" + }, + "scripts": { + "dev": "tsc --watch", + "compile": "rm -rf dist && tsc" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/inquirer": "^9.0.9", + "@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/bun-mariadb.git" + }, + "dependencies": { + "@inquirer/prompts": "^8.3.0", + "chalk": "^5.6.2", + "commander": "^14.0.3", + "inquirer": "^13.3.2", + "lodash": "^4.17.23", + "mariadb": "^3.5.3", + "mysql": "^2.18.1" + } +} diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..c52bbc5 --- /dev/null +++ b/publish.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +if [ -z "$1" ]; then + msg="Updates" +else + msg="$1" +fi + +tsc --noEmit +rm -rf dist +tsc +git add . +git commit -m "$msg" +git push +bun publish diff --git a/src/commands/admin/index.ts b/src/commands/admin/index.ts new file mode 100644 index 0000000..8690d10 --- /dev/null +++ b/src/commands/admin/index.ts @@ -0,0 +1,41 @@ +import { Command } from "commander"; +import grabDBDir from "../../utils/grab-db-dir"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +import listTables from "./list-tables"; +import runSQL from "./run-sql"; + +export default function () { + return new Command("admin") + .description("View Tables and Data, Run SQL Queries, Etc.") + .action(async () => { + const config = global.CONFIG; + const { db_file_path } = grabDBDir({ config }); + + console.log(chalk.bold(chalk.blue("\nBun MariaDB Admin\n"))); + + try { + while (true) { + const paradigm = await select({ + message: "Choose an action:", + choices: [ + { name: "Tables", value: "list_tables" }, + { name: "SQL", value: "run_sql" }, + { name: chalk.dim("✕ Exit"), value: "exit" }, + ], + }); + + if (paradigm === "exit") break; + if (paradigm === "list_tables") { + const result = await listTables(); + if (result === "__exit__") break; + } + if (paradigm === "run_sql") await runSQL(); + } + } catch (error: any) { + console.error(error.message); + } + + process.exit(); + }); +} diff --git a/src/commands/admin/list-tables.ts b/src/commands/admin/list-tables.ts new file mode 100644 index 0000000..0d8711e --- /dev/null +++ b/src/commands/admin/list-tables.ts @@ -0,0 +1,96 @@ +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +import { AppData } from "../../data/app-data"; +import showEntries from "./show-entries"; +import showFields from "./show-fields"; +import dbHandler from "../../lib/db-handler"; + +type Params = {}; + +type ColumnInfo = { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +}; + +export default async function listTables( + params?: Params, +): Promise<"__exit__" | void> { + const tables = ( + await dbHandler({ + query: `SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`, + }) + ).payload; + + if (!tables?.length) { + console.log(chalk.yellow("\nNo tables found.\n")); + return; + } + + // Level 1: table selection loop + while (true) { + const tableName = await select({ + message: "Select a table:", + choices: [ + ...tables.map((t) => ({ + name: t.table_name, + value: t.table_name, + })), + { name: chalk.dim("← Go Back"), value: "__back__" }, + { name: chalk.dim("✕ Exit"), value: "__exit__" }, + ], + }); + + if (tableName === "__back__") break; + if (tableName === "__exit__") return "__exit__"; + + // Level 2: action loop — stays here until "Go Back" + while (true) { + const action = await select({ + message: `"${tableName}" — choose an action:`, + choices: [ + { name: "Entries", value: "entries" }, + { name: "Fields", value: "fields" }, + { name: "Schema", value: "schema" }, + { name: chalk.dim("← Go Back"), value: "__back__" }, + { name: chalk.dim("✕ Exit"), value: "__exit__" }, + ], + }); + + if (action === "__back__") break; + if (action === "__exit__") return "__exit__"; + + if (action === "entries") { + const result = await showEntries({ tableName }); + if (result === "__exit__") return "__exit__"; + } + + if (action === "fields") { + const result = await showFields({ tableName }); + if (result === "__exit__") return "__exit__"; + } + + // if (action === "schema") { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + + // console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`); + // console.table( + // columns.map((c) => ({ + // "#": c.cid, + // Name: c.name, + // Type: c.type, + // "Not Null": c.notnull ? "YES" : "NO", + // Default: c.dflt_value ?? "(none)", + // "Primary Key": c.pk ? "YES" : "NO", + // })), + // ); + // console.log(); + // } + } + } +} diff --git a/src/commands/admin/run-sql.ts b/src/commands/admin/run-sql.ts new file mode 100644 index 0000000..b5511d4 --- /dev/null +++ b/src/commands/admin/run-sql.ts @@ -0,0 +1,32 @@ +import { input } from "@inquirer/prompts"; +import chalk from "chalk"; +import dbHandler from "../../lib/db-handler"; + +type Params = {}; + +export default async function runSQL(params?: Params) { + const sql = await input({ + message: "Enter SQL query:", + validate: (val) => val.trim().length > 0 || "Query cannot be empty", + }); + + try { + const res = await dbHandler({ query: sql }); + + if (res.payload) { + console.log( + `\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`, + ); + if (res.payload.length) console.table(res.payload); + else console.log(chalk.yellow("No res returned.\n")); + } else if (res.single_res) { + console.log( + chalk.green( + `\nSuccess! Affected rows: ${res.single_res.changes}, Last insert ID: ${res.single_res.lastInsertRowid}\n`, + ), + ); + } + } catch (error: any) { + console.error(chalk.red(`\nSQL Error: ${error.message}\n`)); + } +} diff --git a/src/commands/admin/show-entries.ts b/src/commands/admin/show-entries.ts new file mode 100644 index 0000000..8dd1c6b --- /dev/null +++ b/src/commands/admin/show-entries.ts @@ -0,0 +1,103 @@ +import chalk from "chalk"; +import { select, input } from "@inquirer/prompts"; +import dbHandler from "../../lib/db-handler"; + +type Params = { + tableName: string; +}; +type ColumnInfo = { cid: number; name: string }; + +const LIMIT = 50; + +export default async function showEntries({ tableName }: Params) { + let page = 0; + let searchField: string | null = null; + let searchTerm: string | null = null; + + while (true) { + const offset = page * LIMIT; + + const rows = searchTerm + ? ( + await dbHandler({ + query: `SELECT * FROM "${tableName}" WHERE "${searchField}" LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`, + values: [`%${searchTerm}%`], + }) + ).payload + : ( + await dbHandler({ + query: `SELECT * FROM "${tableName}" LIMIT ${LIMIT} OFFSET ${offset}`, + }) + ).payload; + + const countRow = searchTerm + ? ( + await dbHandler({ + query: `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`, + values: [`%${searchTerm}%`], + }) + ).payload + : ( + await dbHandler({ + query: `SELECT COUNT(*) as count FROM "${tableName}"`, + }) + ).payload; + + const total = countRow?.[0]?.count; + const searchInfo = searchTerm + ? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`) + : ""; + + if (!rows) { + return; + } + + console.log( + `\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`, + ); + + if (rows.length) { + console.log(rows); + // if (rows.length) console.table(rows); + } else { + console.log(chalk.yellow("No rows found.")); + console.log(); + } + + const choices: { name: string; value: string }[] = []; + if (page > 0) choices.push({ name: "← Previous Page", value: "prev" }); + if (offset + rows.length < total) + choices.push({ name: "Next Page →", value: "next" }); + choices.push({ name: "Search by Field", value: "search" }); + if (searchTerm) + choices.push({ name: "Clear Search", value: "clear_search" }); + choices.push({ name: chalk.dim("← Go Back"), value: "__back__" }); + choices.push({ name: chalk.dim("✕ Exit"), value: "__exit__" }); + + const action = await select({ message: "Navigate:", choices }); + + if (action === "__back__") break; + if (action === "__exit__") return "__exit__"; + if (action === "next") page++; + if (action === "prev") page--; + if (action === "clear_search") { + searchField = null; + searchTerm = null; + page = 0; + } + // if (action === "search") { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + // searchField = await select({ + // message: "Search by field:", + // choices: columns.map((c) => ({ name: c.name, value: c.name })), + // }); + // searchTerm = await input({ + // message: `Search term for "${searchField}":`, + // validate: (v) => v.trim().length > 0 || "Cannot be empty", + // }); + // page = 0; + // } + } +} diff --git a/src/commands/admin/show-fields.ts b/src/commands/admin/show-fields.ts new file mode 100644 index 0000000..57f003e --- /dev/null +++ b/src/commands/admin/show-fields.ts @@ -0,0 +1,92 @@ +type Params = { + tableName: string; +}; + +type ColumnInfo = { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +}; + +type IndexInfo = { name: string; unique: number; origin: string }; +type IndexColumn = { name: string }; +type ForeignKey = { + id: number; + from: string; + table: string; + to: string; + on_update: string; + on_delete: string; +}; + +export default async function showFields({ + tableName, +}: Params): Promise<"__exit__" | void> { + // const columns = db + // .query(`PRAGMA table_info("${tableName}")`) + // .all(); + // const indexes = db + // .query(`PRAGMA index_list("${tableName}")`) + // .all(); + // const foreignKeys = db + // .query(`PRAGMA foreign_key_list("${tableName}")`) + // .all(); + // const indexedFields = new Map(); + // for (const idx of indexes) { + // const cols = db + // .query(`PRAGMA index_info("${idx.name}")`) + // .all(); + // for (const col of cols) { + // indexedFields.set(col.name, { unique: idx.unique === 1 }); + // } + // } + // while (true) { + // const fieldName = await select({ + // message: `"${tableName}" — select a field:`, + // choices: [ + // ...columns.map((c) => ({ name: c.name, value: c.name })), + // { name: chalk.dim("← Go Back"), value: "__back__" }, + // { name: chalk.dim("✕ Exit"), value: "__exit__" }, + // ], + // }); + // if (fieldName === "__back__") break; + // if (fieldName === "__exit__") return "__exit__"; + // const col = columns.find((c) => c.name === fieldName)!; + // const idx = indexedFields.get(fieldName); + // const fk = foreignKeys.find((f) => f.from === fieldName); + // console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`); + // console.log(` ${chalk.dim("Table")} ${tableName}`); + // console.log(` ${chalk.dim("Column #")} ${col.cid}`); + // console.log( + // ` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`, + // ); + // console.log( + // ` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`, + // ); + // console.log( + // ` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`, + // ); + // console.log( + // ` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`, + // ); + // if (fk) { + // console.log( + // ` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`, + // ); + // console.log(` ${chalk.dim("On Update")} ${fk.on_update}`); + // console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`); + // } else { + // console.log(` ${chalk.dim("Foreign Key")} NO`); + // } + // console.log(); + // } +} diff --git a/src/commands/backup.ts b/src/commands/backup.ts new file mode 100644 index 0000000..5545f4f --- /dev/null +++ b/src/commands/backup.ts @@ -0,0 +1,28 @@ +import { Command } from "commander"; +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 = global.CONFIG; + + 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..699c757 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env bun + +import { program } from "commander"; +import schema from "./schema"; +import typedef from "./typedef"; +import backup from "./backup"; +import restore from "./restore"; +import admin from "./admin"; +import type { + BUN_MARIADB_DatabaseSchemaType, + BunMariaDBConfig, +} from "../types"; +import init from "../functions/init"; + +/** + * # Declare Global Variables + */ +declare global { + var CONFIG: BunMariaDBConfig; + var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType; +} + +await init(); + +if (!global.CONFIG) { + console.error(`Couldn't grab global Config.`); + process.exit(1); +} + +if (!global.DB_SCHEMA) { + console.error(`Couldn't grab Database Schema.`); + process.exit(1); +} + +/** + * # Describe Program + */ +program + .name(`bun-mariadb`) + .description(`MariaDB manager for Bun`) + .version(`1.0.0`); + +/** + * # Declare Commands + */ +program.addCommand(schema()); +program.addCommand(typedef()); +program.addCommand(backup()); +program.addCommand(restore()); +program.addCommand(admin()); + +/** + * # 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/src/commands/restore.ts b/src/commands/restore.ts new file mode 100644 index 0000000..7ff67b3 --- /dev/null +++ b/src/commands/restore.ts @@ -0,0 +1,55 @@ +import { Command } from "commander"; +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 = global.CONFIG; + + 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..59825d3 --- /dev/null +++ b/src/commands/schema.ts @@ -0,0 +1,73 @@ +import { Command } from "commander"; +import { MariaDBSchemaManager } from "../lib/mariadb/db-schema-manager"; +import grabDirNames from "../data/grab-dir-names"; +import path from "path"; +import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef"; +import _ from "lodash"; +import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema"; +import chalk from "chalk"; +import { writeLiveSchema } from "../functions/live-schema"; +import grabDBDir from "../utils/grab-db-dir"; +import { cpSync } from "fs"; + +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 = global.CONFIG; + const dbSchema = global.DB_SCHEMA; + + const { ROOT_DIR, BUN_MARIADB_TEMP_DB_FILE_PATH } = grabDirNames(); + const { db_file_path } = grabDBDir({ config }); + + cpSync(db_file_path, BUN_MARIADB_TEMP_DB_FILE_PATH); + + try { + const isVector = Boolean(opts.vector || opts.v); + const isTypeDef = Boolean(opts.typedef || opts.t); + + const finaldbSchema = appendDefaultFieldsToDbSchema({ + dbSchema, + }); + + const manager = new MariaDBSchemaManager({ + 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, + config, + }); + } + + writeLiveSchema({ schema: finaldbSchema }); + + console.log( + `${chalk.bold(chalk.green(`DB Schema setup success!`))}`, + ); + process.exit(); + } catch (error) { + console.log(error); + cpSync(BUN_MARIADB_TEMP_DB_FILE_PATH, db_file_path); + process.exit(1); + } + }); +} diff --git a/src/commands/typedef.ts b/src/commands/typedef.ts new file mode 100644 index 0000000..4edec32 --- /dev/null +++ b/src/commands/typedef.ts @@ -0,0 +1,40 @@ +import { Command } from "commander"; +import dbSchemaToTypeDef from "../lib/mariadb/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 = global.CONFIG; + const dbSchema = global.DB_SCHEMA; + + 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, + config, + }); + } 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..1a8462b --- /dev/null +++ b/src/data/app-data.ts @@ -0,0 +1,6 @@ +export const AppData = { + ConfigFileName: "bun-mariadb.config.ts", + MaxBackups: 10, + DefaultBackupDirName: ".backups", + DbSchemaManagerTableName: "__db_schema_manager__", +} as const; diff --git a/src/data/grab-dir-names.ts b/src/data/grab-dir-names.ts new file mode 100644 index 0000000..5b85663 --- /dev/null +++ b/src/data/grab-dir-names.ts @@ -0,0 +1,28 @@ +import path from "path"; +import type { BunMariaDBConfig } from "../types"; + +type Params = { + config?: BunMariaDBConfig; +}; + +export default function grabDirNames(params?: Params) { + const ROOT_DIR = process.cwd(); + const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-mariadb"); + const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp"); + const BUN_MARIADB_TEMP_DB_FILE_PATH = path.join( + BUN_MARIADB_TEMP_DIR, + "temp.db", + ); + const BUN_MARIADB_LIVE_SCHEMA = path.join( + BUN_MARIADB_DIR, + "live-schema.json", + ); + + return { + ROOT_DIR, + BUN_MARIADB_DIR, + BUN_MARIADB_TEMP_DIR, + BUN_MARIADB_LIVE_SCHEMA, + BUN_MARIADB_TEMP_DB_FILE_PATH, + }; +} diff --git a/src/functions/init.ts b/src/functions/init.ts new file mode 100644 index 0000000..7678d80 --- /dev/null +++ b/src/functions/init.ts @@ -0,0 +1,73 @@ +import path from "path"; +import fs from "fs"; +import { AppData } from "../data/app-data"; +import grabDirNames from "../data/grab-dir-names"; +import type { + BunMariaDBConfig, + BunMariaDBConfigReturn, + BUN_MARIADB_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.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 BunMariaDBConfig; + + 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_MARIADB_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 }); + } + + global.CONFIG = Config; + global.DB_SCHEMA = DbSchema; + + return { + config: Config, + dbSchema: DbSchema, + }; + } catch (error: any) { + console.error(`Initialization ERROR => ` + error.message); + process.exit(1); + } +} diff --git a/src/functions/live-schema.ts b/src/functions/live-schema.ts new file mode 100644 index 0000000..0feaf05 --- /dev/null +++ b/src/functions/live-schema.ts @@ -0,0 +1,24 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import grabDirNames from "../data/grab-dir-names"; +import type { BUN_MARIADB_DatabaseSchemaType } from "../types"; +import path from "path"; + +const { BUN_MARIADB_LIVE_SCHEMA } = grabDirNames(); + +type Params = { + schema: BUN_MARIADB_DatabaseSchemaType; +}; + +export function writeLiveSchema({ schema }: Params) { + mkdirSync(path.dirname(BUN_MARIADB_LIVE_SCHEMA), { recursive: true }); + writeFileSync(BUN_MARIADB_LIVE_SCHEMA, JSON.stringify(schema)); +} + +export function readLiveSchema() { + if (!existsSync(BUN_MARIADB_LIVE_SCHEMA)) { + return undefined; + } + + const live_schema = readFileSync(BUN_MARIADB_LIVE_SCHEMA, "utf-8"); + return JSON.parse(live_schema) as BUN_MARIADB_DatabaseSchemaType; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3794aa5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,40 @@ +import init from "./functions/init"; +import DbDelete from "./lib/mariadb/db-delete"; +import DbInsert from "./lib/mariadb/db-insert"; +import DbSelect from "./lib/mariadb/db-select"; +import DbSQL from "./lib/mariadb/db-sql"; +import DbUpdate from "./lib/mariadb/db-update"; +import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "./types"; +import grabDbSchema from "./utils/grab-db-schema"; +import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object"; + +declare global { + var CONFIG: BunMariaDBConfig; + var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType; +} + +await init(); + +if (!global.CONFIG) { + console.error(`Couldn't grab global Config.`); + process.exit(1); +} + +if (!global.DB_SCHEMA) { + console.error(`Couldn't grab Database Schema.`); + process.exit(1); +} + +const BunMariaDB = { + select: DbSelect, + insert: DbInsert, + update: DbUpdate, + delete: DbDelete, + sql: DbSQL, + utils: { + grab_db_schema: grabDbSchema, + grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject, + }, +} as const; + +export default BunMariaDB; diff --git a/src/lib/db-handler.ts b/src/lib/db-handler.ts new file mode 100644 index 0000000..56076d3 --- /dev/null +++ b/src/lib/db-handler.ts @@ -0,0 +1,69 @@ +import type { Connection, ConnectionConfig } from "mariadb"; +import type { BUN_MARIADB_TableSchemaType, DBResponseObject } from "../types"; +import grabDBConnection from "./grab-db-connection"; + +type Param = { + query: string; + values?: string[] | object; + noErrorLogs?: boolean; + database?: string; + tableSchema?: BUN_MARIADB_TableSchemaType; + config?: ConnectionConfig; +}; + +/** + * # Main DB Handler Function + * @requires DSQL_DB_CONN - Gobal Variable for Datasquirel Database + */ +export default async function dbHandler< + T extends { [k: string]: any } = { [k: string]: any }, +>({ + query, + values, + noErrorLogs, + database, + config, +}: Param): Promise { + let CONNECTION: Connection | undefined; + let results: T | null = null; + + try { + CONNECTION = await grabDBConnection({ database, config }); + + if (query && values) { + const queryResults = await CONNECTION.query(query, values); + results = queryResults[0]; + } else { + const queryResults = await CONNECTION.query(query); + results = queryResults[0]; + } + } catch (error: any) { + if ( + error.message && + typeof error.message == "string" && + error.message.match(/Access denied for user.*password/i) + ) { + throw new Error("Authentication Failed!"); + } + + if (!noErrorLogs) { + console.log(error); + } + + results = null; + } finally { + await CONNECTION?.end(); + } + + if (results) { + return { + success: true, + payload: Array.isArray(results) ? results : undefined, + single_res: Array.isArray(results) ? undefined : results, + }; + } else { + return { + success: false, + }; + } +} diff --git a/src/lib/grab-db-connection.ts b/src/lib/grab-db-connection.ts new file mode 100644 index 0000000..fb5296d --- /dev/null +++ b/src/lib/grab-db-connection.ts @@ -0,0 +1,20 @@ +import * as mariadb from "mariadb"; +import type { Connection } from "mariadb"; +import grabDSQLConnectionConfig from "./grab-dsql-connection-config"; +import type { DsqlConnectionParam } from "../types"; + +/** + * # Grab General CONNECTION for DSQL + */ +export default async function grabDBConnection( + param?: DsqlConnectionParam, +): Promise { + const config = grabDSQLConnectionConfig(param); + + try { + return await mariadb.createConnection(config); + } catch (error) { + console.log(`Error Grabbing DSQL Connection =>`, config); + throw error; + } +} diff --git a/src/lib/grab-db-ssl.ts b/src/lib/grab-db-ssl.ts new file mode 100644 index 0000000..59a0528 --- /dev/null +++ b/src/lib/grab-db-ssl.ts @@ -0,0 +1,28 @@ +import fs from "fs"; +import type { ConnectionConfig } from "mariadb"; +import path from "path"; + +type Return = ConnectionConfig["ssl"] | undefined; + +/** + * # Grab SSL + */ +export default function grabDbSSL(): Return { + const caProivdedPath = process.env.DSQL_SSL_CA_CERT; + + if (!caProivdedPath?.match(/./)) { + return undefined; + } + + const caFilePath = path.resolve(process.cwd(), caProivdedPath); + + if (!fs.existsSync(caFilePath)) { + console.log(`${caFilePath} does not exist`); + return undefined; + } + + return { + ca: fs.readFileSync(caFilePath), + rejectUnauthorized: false, + }; +} diff --git a/src/lib/grab-dsql-connection-config.ts b/src/lib/grab-dsql-connection-config.ts new file mode 100644 index 0000000..da5c4a9 --- /dev/null +++ b/src/lib/grab-dsql-connection-config.ts @@ -0,0 +1,35 @@ +import { type ConnectionConfig } from "mariadb"; +import type { DsqlConnectionParam } from "../types"; +import grabDbSSL from "./grab-db-ssl"; + +/** + * # Grab General CONNECTION for DSQL + */ +export default function grabDSQLConnectionConfig( + param?: DsqlConnectionParam, +): ConnectionConfig { + const CONN_TIMEOUT = 10000; + + const config: ConnectionConfig = { + host: process.env.DSQL_DB_HOST, + user: process.env.DSQL_DB_USERNAME, + password: process.env.DSQL_DB_PASSWORD, + // database: param?.dbFullName, + port: process.env.DSQL_DB_PORT + ? Number(process.env.DSQL_DB_PORT) + : undefined, + charset: "utf8mb4", + ssl: grabDbSSL(), + bigIntAsNumber: true, + supportBigNumbers: true, + bigNumberStrings: false, + dateStrings: true, + metaAsArray: true, + socketTimeout: CONN_TIMEOUT, + connectTimeout: CONN_TIMEOUT, + compress: true, + ...param?.config, + }; + + return config; +} diff --git a/src/lib/grab-duplicate-safe-insert-sql.ts b/src/lib/grab-duplicate-safe-insert-sql.ts new file mode 100644 index 0000000..f5065b5 --- /dev/null +++ b/src/lib/grab-duplicate-safe-insert-sql.ts @@ -0,0 +1,40 @@ +type Params = { + sql: string; + table: string; + data: any | any[]; +}; + +export default async function ({ sql: passed_sql, table, data }: Params) { + let sql = passed_sql; + const config = global.CONFIG; + const dbSchema = global.DB_SCHEMA; + + const table_schema = dbSchema.tables.find((t) => t.tableName == table); + const now = Date.now(); + + if (table_schema?.tableName) { + const set_sql_arr = Object.keys( + Array.isArray(data) ? data[0] : data, + ).map((field) => `${field} = excluded.${field}`); + + set_sql_arr.push(`updated_at = ${now}`); + + const set_sql = set_sql_arr.join(", "); + + const unique_fields = table_schema.fields.filter((f) => f.unique); + + for (let i = 0; i < unique_fields.length; i++) { + const field = unique_fields[i]; + sql += ` ON CONFLICT(${field?.fieldName}) DO UPDATE SET ${set_sql}`; + } + + if (table_schema.uniqueConstraints?.[0]) { + for (let i = 0; i < table_schema.uniqueConstraints.length; i++) { + const constraint = table_schema.uniqueConstraints[i]; + sql += ` ON CONFLICT(${constraint?.constraintTableFields?.map((c) => c.value)?.join(", ")}) DO UPDATE SET ${set_sql}`; + } + } + } + + return sql; +} diff --git a/src/lib/mariadb/db-delete.ts b/src/lib/mariadb/db-delete.ts new file mode 100644 index 0000000..e16f22b --- /dev/null +++ b/src/lib/mariadb/db-delete.ts @@ -0,0 +1,83 @@ +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 { + let sqlObj: ReturnType | null = null; + + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + finalQuery, + { + query: { + id: { + value: String(targetId), + }, + }, + }, + ); + } + + sqlObj = sqlGenerator({ + tableName: table, + genObject: finalQuery, + }); + + const whereClause = sqlObj.string.match(/WHERE .*/)?.[0]; + + if (whereClause) { + let sql = `DELETE FROM ${table} ${whereClause}`; + + sqlObj.string = sql; + + const res = DbClient.run(sql, sqlObj.values); + + return { + success: Boolean(res.changes), + postInsertReturn: { + affectedRows: res.changes, + insertId: Number(res.lastInsertRowid), + }, + debug: { + sqlObj, + }, + }; + } else { + return { + success: false, + msg: `No WHERE clause`, + debug: { + sqlObj, + }, + }; + } + } catch (error: any) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/src/lib/mariadb/db-generate-type-defs.ts b/src/lib/mariadb/db-generate-type-defs.ts new file mode 100644 index 0000000..e5a126a --- /dev/null +++ b/src/lib/mariadb/db-generate-type-defs.ts @@ -0,0 +1,117 @@ +import type { + BUN_MARIADB_FieldSchemaType, + BUN_MARIADB_TableSchemaType, +} from "../../types"; + +type Param = { + paradigm: "JavaScript" | "TypeScript" | undefined; + table: BUN_MARIADB_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_MARIADB_${dbName}_${table.tableName}`.toUpperCase() + : `BUN_MARIADB_${query.single}_${query.single_table}`.toUpperCase(); + + const fields = table.fields; + + function typeMap(schemaType: BUN_MARIADB_FieldSchemaType) { + if (schemaType.options && schemaType.options.length > 0) { + let opts = schemaType.options.map((opt) => + schemaType.dataType?.match(/int/i) || typeof opt == "number" + ? `${opt}` + : `"${opt}"`, + ); + + opts.push(`""`); + + return opts.join(" | "); + } + + if (schemaType.dataType?.match(/blob/i)) { + return `Float32Array | Buffer | null`; + } + + if (schemaType.dataType?.match(/int|double|decimal|real/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/mariadb/db-insert.ts b/src/lib/mariadb/db-insert.ts new file mode 100644 index 0000000..5dce989 --- /dev/null +++ b/src/lib/mariadb/db-insert.ts @@ -0,0 +1,67 @@ +import DbClient from "."; +import type { APIResponseObject, SQLInsertGenReturn } from "../../types"; +import sqlInsertGenerator from "../../utils/sql-insert-generator"; +import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql"; + +type Params< + Schema extends { [k: string]: any } = { [k: string]: any }, + Table extends string = string, +> = { + table: Table; + data: Schema[]; + update_on_duplicate?: boolean; +}; + +export default async function DbInsert< + Schema extends { [k: string]: any } = { [k: string]: any }, + Table extends string = string, +>({ + table, + data, + update_on_duplicate, +}: Params): Promise { + let sqlObj: SQLInsertGenReturn | null = null; + + try { + const finalData: { [k: string]: any }[] = data.map((d) => ({ + created_at: Date.now(), + updated_at: Date.now(), + ...d, + })); + + sqlObj = + sqlInsertGenerator({ + tableName: table, + data: finalData as any[], + }) || null; + + let sql = sqlObj?.query || ""; + + if (update_on_duplicate && data[0]) { + sql = await grabDuplicateSafeInsertSql({ data, table, sql }); + } + + (sqlObj || ({} as any)).query = sql; + + const res = DbClient.run(sql, 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, + debug: { + sqlObj, + }, + }; + } +} diff --git a/src/lib/mariadb/db-schema-manager.ts b/src/lib/mariadb/db-schema-manager.ts new file mode 100644 index 0000000..442205c --- /dev/null +++ b/src/lib/mariadb/db-schema-manager.ts @@ -0,0 +1,656 @@ +#!/usr/bin/env bun + +import _ from "lodash"; +import dbHandler from "../db-handler"; +import type { + BUN_MARIADB_DatabaseSchemaType, + BUN_MARIADB_FieldSchemaType, + BUN_MARIADB_TableSchemaType, +} from "../../types"; +import { AppData } from "../../data/app-data"; +import { readLiveSchema } from "../../functions/live-schema"; + +type QueryValues = any[]; + +type TableInfoRow = { + table_name: string; +}; + +type ColumnInfoRow = { + name: string; + type: string; +}; + +type IndexInfoRow = { + name: string; +}; + +class MariaDBSchemaManager { + private db_manager_table_name: string; + private recreate_vector_table: boolean; + private db_schema: BUN_MARIADB_DatabaseSchemaType; + + constructor({ + schema, + recreate_vector_table = false, + }: { + schema: BUN_MARIADB_DatabaseSchemaType; + recreate_vector_table?: boolean; + }) { + this.db_manager_table_name = AppData["DbSchemaManagerTableName"]; + this.recreate_vector_table = recreate_vector_table; + this.db_schema = schema; + } + + async syncSchema(): Promise { + console.log("Starting schema synchronization..."); + + await this.createDbManagerTable(); + + const existingTables = await this.getExistingTables(); + const schemaTables = this.db_schema.tables.map((t) => t.tableName); + + for (const table of this.db_schema.tables) { + await this.syncTable(table, existingTables); + } + + await this.dropRemovedTables(existingTables, schemaTables); + + console.log("Schema synchronization complete!"); + } + + private getDatabaseName(): string | undefined { + return this.db_schema.dbName || this.db_schema.dbSlug; + } + + private quoteIdentifier(identifier: string): string { + return `\`${identifier.replace(/`/g, "``")}\``; + } + + private tableSchemaWhere(tableName: string): { + where: string; + values: QueryValues; + } { + const databaseName = this.getDatabaseName(); + + if (databaseName) { + return { + where: "TABLE_SCHEMA = ?", + values: [databaseName, tableName], + }; + } + + return { + where: "TABLE_SCHEMA = DATABASE()", + values: [tableName], + }; + } + + private async run(query: string, values?: QueryValues): Promise { + const res = await dbHandler({ + query, + values: values as any, + config: this.getDatabaseName() + ? { database: this.getDatabaseName() } + : undefined, + }); + + if (!res.success) { + throw new Error(`Database query failed: ${query}`); + } + } + + private async query>( + query: string, + values?: QueryValues, + ): Promise { + const res = await dbHandler({ + query, + values: values as any, + config: this.getDatabaseName() + ? { database: this.getDatabaseName() } + : undefined, + }); + + if (!res.success) { + throw new Error(`Database query failed: ${query}`); + } + + return (res.payload || []) as T[]; + } + + private async createDbManagerTable(): Promise { + await this.run(` + CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(this.db_manager_table_name)} ( + table_name VARCHAR(255) NOT NULL PRIMARY KEY, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci + `); + } + + private async insertDbManagerTable(tableName: string): Promise { + const now = Date.now(); + + await this.run( + `INSERT INTO ${this.quoteIdentifier(this.db_manager_table_name)} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`, + [tableName, now, now], + ); + } + + private async removeDbManagerTable(tableName: string): Promise { + await this.run( + `DELETE FROM ${this.quoteIdentifier(this.db_manager_table_name)} WHERE table_name = ?`, + [tableName], + ); + } + + private async getExistingTables(): Promise { + const rows = await this.query( + `SELECT table_name FROM ${this.quoteIdentifier(this.db_manager_table_name)}`, + ); + + return rows.map((row) => row.table_name); + } + + private async getLiveTableNames(): Promise { + const tableSchemaWhere = this.tableSchemaWhere(""); + const rows = await this.query<{ TABLE_NAME: string }>( + `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_TYPE = 'BASE TABLE'`, + tableSchemaWhere.values, + ); + + return rows.map((row) => row.TABLE_NAME); + } + + private async dropRemovedTables( + existingTables: string[], + schemaTables: string[], + ): Promise { + console.log(`Cleaning up tables ...`); + + const tablesToDrop = existingTables.filter( + (tableName) => + !schemaTables.includes(tableName) && + !schemaTables.some((schemaTable) => + tableName.startsWith(`${schemaTable}_`), + ), + ); + + const currentSchema = readLiveSchema(); + + if (currentSchema?.tables?.[0]) { + for (const table of currentSchema.tables) { + if (!table?.tableName) continue; + + const doesTableExist = schemaTables.find( + (tableName) => tableName === table.tableName, + ); + + if (!doesTableExist) { + tablesToDrop.push(table.tableName); + } + } + } + + for (const tableName of tablesToDrop) { + console.log(`Dropping table: ${tableName}`); + await this.run(`DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`); + await this.removeDbManagerTable(tableName); + } + } + + private async syncTable( + table: BUN_MARIADB_TableSchemaType, + existingTables: string[], + ): Promise { + let tableExists = existingTables.includes(table.tableName); + const liveTables = await this.getLiveTableNames(); + + if (table.tableNameOld && table.tableNameOld !== table.tableName) { + if (liveTables.includes(table.tableNameOld)) { + console.log( + `Renaming table: ${table.tableNameOld} -> ${table.tableName}`, + ); + await this.run( + `RENAME TABLE ${this.quoteIdentifier(table.tableNameOld)} TO ${this.quoteIdentifier(table.tableName)}`, + ); + await this.insertDbManagerTable(table.tableName); + await this.removeDbManagerTable(table.tableNameOld); + tableExists = true; + } + } + + if (!tableExists) { + await this.createTable(table); + await this.insertDbManagerTable(table.tableName); + } else { + await this.updateTable(table); + await this.insertDbManagerTable(table.tableName); + } + + await this.syncIndexes(table); + } + + private resolveTable(table: BUN_MARIADB_TableSchemaType) { + if (!table.parentTableName) { + return _.cloneDeep(table); + } + + const parentTable = this.db_schema.tables.find( + (schemaTable) => schemaTable.tableName === table.parentTableName, + ); + + if (!parentTable) { + throw new Error( + `Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``, + ); + } + + return _.merge({}, parentTable, { + tableName: table.tableName, + tableDescription: table.tableDescription, + collation: table.collation, + fields: [...(parentTable.fields || []), ...(table.fields || [])], + indexes: [...(parentTable.indexes || []), ...(table.indexes || [])], + uniqueConstraints: [ + ...(parentTable.uniqueConstraints || []), + ...(table.uniqueConstraints || []), + ], + }); + } + + private async createTable( + table: BUN_MARIADB_TableSchemaType, + ): Promise { + if (!table.tableName.match(/_temp_\d+$/)) { + console.log(`Creating table: ${table.tableName}`); + } + + const newTable = this.resolveTable(table); + const columnDefinitions: string[] = []; + const foreignKeys: string[] = []; + + for (const field of newTable.fields || []) { + columnDefinitions.push(this.buildColumnDefinition(field)); + + if (field.foreignKey && !newTable.isVector) { + foreignKeys.push(this.buildForeignKeyConstraint(field)); + } + } + + if (newTable.uniqueConstraints) { + for (const constraint of newTable.uniqueConstraints) { + if ( + constraint.constraintTableFields && + constraint.constraintTableFields.length > 0 + ) { + const fields = constraint.constraintTableFields + .map((field) => this.quoteIdentifier(field.value)) + .join(", "); + const constraintName = + constraint.constraintName || + `unique_${fields.replace(/`/g, "")}`; + + columnDefinitions.push( + `CONSTRAINT ${this.quoteIdentifier(constraintName)} UNIQUE (${fields})`, + ); + } + } + } + + const sql = `CREATE TABLE IF NOT EXISTS ${this.quoteIdentifier(newTable.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${this.buildTableOptions(newTable)}`; + + await this.run(sql); + } + + private buildTableOptions(table: BUN_MARIADB_TableSchemaType): string { + const options = ["ENGINE=InnoDB"]; + + if (table.collation) { + options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`); + } + + return ` ${options.join(" ")}`; + } + + private async updateTable( + table: BUN_MARIADB_TableSchemaType, + ): Promise { + console.log(`Updating table: ${table.tableName}`); + await this.recreateTable(table); + } + + private async getTableColumns( + tableName: string, + ): Promise { + const tableSchemaWhere = this.tableSchemaWhere(tableName); + const rows = await this.query<{ COLUMN_NAME: string; COLUMN_TYPE: string }>( + `SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type FROM information_schema.COLUMNS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`, + tableSchemaWhere.values, + ); + + return rows.map((row) => ({ + name: row.COLUMN_NAME, + type: row.COLUMN_TYPE, + })); + } + + private async addColumn( + tableName: string, + field: BUN_MARIADB_FieldSchemaType, + ): Promise { + console.log(`Adding column: ${tableName}.${field.fieldName}`); + + const columnDef = this.buildColumnDefinition(field) + .replace(/PRIMARY KEY/gi, "") + .replace(/AUTO_INCREMENT/gi, "") + .replace(/UNIQUE/gi, "") + .trim(); + + await this.run( + `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${columnDef}`, + ); + } + + private async checkIfTableExists(table: string): Promise { + const tableSchemaWhere = this.tableSchemaWhere(table); + const row = await this.query<{ exists: number }>( + `SELECT 1 AS exists FROM information_schema.TABLES WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? LIMIT 1`, + tableSchemaWhere.values, + ); + + return Boolean(row[0]?.exists); + } + + private async recreateTable( + table: BUN_MARIADB_TableSchemaType, + ): Promise { + if (table.isVector && !this.recreate_vector_table) { + return; + } + + const doesTableExist = await this.checkIfTableExists(table.tableName); + + if (table.isVector) { + let existingRows: Record[] = []; + + if (doesTableExist) { + existingRows = await this.query>( + `SELECT * FROM ${this.quoteIdentifier(table.tableName)}`, + ); + await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`); + } + + await this.createTable(table); + + if (existingRows.length > 0) { + await this.insertRows(table.tableName, existingRows); + } + + return; + } + + const tempTableName = `${table.tableName}_temp_${Date.now()}`; + const existingColumns = await this.getTableColumns(table.tableName); + const columnsToKeep = table.fields + .filter((field) => + existingColumns.some((column) => column.name === field.fieldName), + ) + .map((field) => field.fieldName) + .filter((fieldName): fieldName is string => Boolean(fieldName)); + + await this.createTable({ ...table, tableName: tempTableName }); + + if (columnsToKeep.length > 0) { + const columnList = columnsToKeep + .map((column) => this.quoteIdentifier(column)) + .join(", "); + + await this.run( + `INSERT INTO ${this.quoteIdentifier(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${this.quoteIdentifier(table.tableName)}`, + ); + } + + await this.run(`DROP TABLE ${this.quoteIdentifier(table.tableName)}`); + await this.run( + `RENAME TABLE ${this.quoteIdentifier(tempTableName)} TO ${this.quoteIdentifier(table.tableName)}`, + ); + } + + private async insertRows( + tableName: string, + rows: Record[], + ): Promise { + for (const row of rows) { + const columns = Object.keys(row); + + if (columns.length === 0) { + continue; + } + + const values = columns.map((column) => row[column]); + const columnList = columns + .map((column) => this.quoteIdentifier(column)) + .join(", "); + const placeholders = columns.map(() => "?").join(", "); + + await this.run( + `INSERT INTO ${this.quoteIdentifier(tableName)} (${columnList}) VALUES (${placeholders})`, + values, + ); + } + } + + private buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string { + if (!field.fieldName) { + throw new Error("Field name is required"); + } + + const parts: string[] = [this.quoteIdentifier(field.fieldName)]; + + parts.push(this.mapDataType(field)); + + if (field.primaryKey) { + parts.push("PRIMARY KEY"); + + if (field.autoIncrement) { + parts.push("AUTO_INCREMENT"); + } + } + + if (field.notNullValue || field.primaryKey) { + if (!field.primaryKey) { + parts.push("NOT NULL"); + } + } + + if (field.unique && !field.primaryKey) { + parts.push("UNIQUE"); + } + + if (field.defaultValue !== undefined) { + if (typeof field.defaultValue === "string") { + parts.push( + `DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`, + ); + } else { + parts.push(`DEFAULT ${field.defaultValue}`); + } + } else if (field.defaultValueLiteral) { + parts.push(`DEFAULT ${field.defaultValueLiteral}`); + } + + if (field.onUpdate) { + parts.push(`ON UPDATE ${field.onUpdate}`); + } else if (field.onUpdateLiteral) { + parts.push(`ON UPDATE ${field.onUpdateLiteral}`); + } + + return parts.join(" "); + } + + private mapDataType(field: BUN_MARIADB_FieldSchemaType): string { + const dataType = field.dataType?.toLowerCase() || "text"; + const vectorSize = field.vectorSize || 1536; + + if (field.isVector) { + return `LONGTEXT COMMENT 'vector_size=${vectorSize}'`; + } + + if (dataType === "bigint") { + if (field.integerLength) { + return `BIGINT(${field.integerLength})`; + } + + return "BIGINT"; + } + + if (dataType === "smallint") { + if (field.integerLength) { + return `SMALLINT(${field.integerLength})`; + } + + return "SMALLINT"; + } + + if (dataType === "tinyint") { + if (field.integerLength) { + return `TINYINT(${field.integerLength})`; + } + + return "TINYINT"; + } + + if ( + dataType.includes("int") || + dataType === "bigint" || + dataType === "smallint" || + dataType === "tinyint" + ) { + if (field.integerLength) { + return `INT(${field.integerLength})`; + } + + return "INT"; + } + + if (dataType.includes("double")) { + return "DOUBLE"; + } + + if (dataType.includes("float")) { + return "FLOAT"; + } + + if (dataType.includes("decimal") || dataType.includes("numeric")) { + if (field.integerLength && field.decimals) { + return `DECIMAL(${field.integerLength}, ${field.decimals})`; + } + + return "DECIMAL"; + } + + if (dataType.includes("blob") || dataType.includes("binary")) { + return "BLOB"; + } + + if (dataType === "boolean" || dataType === "bool") { + return "TINYINT(1)"; + } + + if (dataType.includes("timestamp")) { + return "TIMESTAMP"; + } + + if (dataType.includes("datetime")) { + return "DATETIME"; + } + + if (dataType.includes("date") || dataType.includes("time")) { + return "DATETIME"; + } + + if (dataType.includes("varchar")) { + if (field.integerLength) { + return `VARCHAR(${field.integerLength})`; + } + + return "VARCHAR(255)"; + } + + return "TEXT"; + } + + private buildForeignKeyConstraint( + field: BUN_MARIADB_FieldSchemaType, + ): string { + const fk = field.foreignKey!; + const constraintName = fk.foreignKeyName + ? `CONSTRAINT ${this.quoteIdentifier(fk.foreignKeyName)} ` + : ""; + + let constraint = `${constraintName}FOREIGN KEY (${this.quoteIdentifier(field.fieldName!)}) REFERENCES ${this.quoteIdentifier(fk.destinationTableName!)}(${this.quoteIdentifier(fk.destinationTableColumnName!)})`; + + if (fk.cascadeDelete) { + constraint += " ON DELETE CASCADE"; + } + + if (fk.cascadeUpdate) { + constraint += " ON UPDATE CASCADE"; + } + + return constraint; + } + + private async syncIndexes( + table: BUN_MARIADB_TableSchemaType, + ): Promise { + if (!table.indexes || table.indexes.length === 0) { + return; + } + + const tableSchemaWhere = this.tableSchemaWhere(table.tableName); + const rows = await this.query( + `SELECT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE ${tableSchemaWhere.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' GROUP BY INDEX_NAME ORDER BY INDEX_NAME`, + tableSchemaWhere.values, + ); + const existingIndexes = rows.map((row) => row.name); + + for (const indexName of existingIndexes) { + const stillExists = table.indexes.some( + (index) => index.indexName === indexName, + ); + + if (!stillExists) { + console.log(`Dropping index: ${indexName}`); + await this.run(`DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(table.tableName)}`); + } + } + + 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((field) => this.quoteIdentifier(field)) + .join(", "); + + await this.run( + `CREATE INDEX ${this.quoteIdentifier(index.indexName)} ON ${this.quoteIdentifier(table.tableName)} (${fields})`, + ); + } + } + } + + close(): void {} +} + +export { MariaDBSchemaManager }; diff --git a/src/lib/mariadb/db-schema-to-typedef.ts b/src/lib/mariadb/db-schema-to-typedef.ts new file mode 100644 index 0000000..547beef --- /dev/null +++ b/src/lib/mariadb/db-schema-to-typedef.ts @@ -0,0 +1,71 @@ +import _ from "lodash"; +import type { + BUN_MARIADB_DatabaseSchemaType, + BunMariaDBConfig, +} from "../../types"; +import generateTypeDefinition from "./db-generate-type-defs"; + +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; + config: BunMariaDBConfig; +}; + +export default function dbSchemaToType({ + config, + dbSchema, +}: Params): string[] | undefined { + let datasquirelSchema = dbSchema; + + if (!datasquirelSchema) return; + + let tableNames = `export const BunMariaDBTables = [\n${datasquirelSchema.tables + .map((tbl) => ` "${tbl.tableName}",`) + .join("\n")}\n] as const`; + + const dbTablesSchemas = datasquirelSchema.tables; + + const defDbName = config.db_name + ?.toUpperCase() + .replace(/[^a-zA-Z0-9]/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_MARIADB_${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_MARIADB_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}` + : ``; + + return [tableNames, ...schemas, allTd]; +} diff --git a/src/lib/mariadb/db-select.ts b/src/lib/mariadb/db-select.ts new file mode 100644 index 0000000..86c3ca4 --- /dev/null +++ b/src/lib/mariadb/db-select.ts @@ -0,0 +1,95 @@ +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> { + let sqlObj: ReturnType | null = null; + + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + finalQuery, + { + query: { + id: { + value: String(targetId), + }, + }, + }, + ); + } + + sqlObj = sqlGenerator({ + tableName: table, + genObject: finalQuery, + }); + + let 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) { + let count_sql_object = sqlGenerator({ + tableName: table, + genObject: finalQuery, + count, + }); + + let count_sql = mysql.format( + count_sql_object.string, + count_sql_object.values, + ); + + count_sql = `SELECT COUNT(*) FROM (${count_sql}) as c`; + + const count_res = DbClient.query(count_sql).all(); + + const count_val = count_res[0]?.["COUNT(*)"]; + resp["count"] = Number(count_val); + resp["debug"]["count_sql"] = count_sql; + } + + return resp; + } catch (error: any) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/src/lib/mariadb/db-sql.ts b/src/lib/mariadb/db-sql.ts new file mode 100644 index 0000000..eb89ad2 --- /dev/null +++ b/src/lib/mariadb/db-sql.ts @@ -0,0 +1,44 @@ +import DbClient from "."; +import _ from "lodash"; +import type { APIResponseObject, SQLInsertGenValueType } from "../../types"; + +type Params = { + sql: string; + values?: SQLInsertGenValueType[]; +}; + +export default async function DbSQL< + T extends { [k: string]: any } = { [k: string]: any }, +>({ sql, values }: Params): Promise> { + try { + const trimmed_sql = sql.trim(); + + const res = trimmed_sql.match(/^select/i) + ? DbClient.query(trimmed_sql).all(...(values || [])) + : DbClient.run(trimmed_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: trimmed_sql, + values, + }, + sql, + }, + }; + } catch (error: any) { + return { + success: false, + error: error.message, + }; + } +} diff --git a/src/lib/mariadb/db-update.ts b/src/lib/mariadb/db-update.ts new file mode 100644 index 0000000..51d5f7e --- /dev/null +++ b/src/lib/mariadb/db-update.ts @@ -0,0 +1,114 @@ +import DbClient from "."; +import _ from "lodash"; +import type { + APIResponseObject, + SQLInsertGenValueType, + 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 { + let sqlObj: ReturnType = { string: "", values: [] }; + + try { + let finalQuery = query || {}; + + if (targetId) { + finalQuery = _.merge, ServerQueryParam>( + finalQuery, + { + query: { + id: { + value: String(targetId), + }, + }, + }, + ); + } + + const sqlQueryObj = sqlGenerator({ + tableName: table, + genObject: finalQuery, + }); + + let values: SQLInsertGenValueType[] = []; + + const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0]; + + if (whereClause) { + let sql = `UPDATE ${table} SET`; + + const finalData: { [k: string]: SQLInsertGenValueType } = { + updated_at: Date.now(), + ...data, + }; + + 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}=?`; + const value = finalData[key]; + values.push(value || null); + + if (!isLast) { + sql += `,`; + } + } + + sql += ` ${whereClause}`; + values = [...values, ...sqlQueryObj.values]; + + sqlObj.string = sql; + sqlObj.values = values as any[]; + + const res = DbClient.run(sql, values); + + return { + success: Boolean(res.changes), + postInsertReturn: { + affectedRows: res.changes, + insertId: Number(res.lastInsertRowid), + }, + debug: { + sqlObj, + }, + }; + } else { + return { + success: false, + msg: `No WHERE clause`, + }; + } + } catch (error: any) { + return { + success: false, + error: error.message, + debug: { + sqlObj, + }, + }; + } +} diff --git a/src/lib/mariadb/schema-to-typedef.ts b/src/lib/mariadb/schema-to-typedef.ts new file mode 100644 index 0000000..bacedf1 --- /dev/null +++ b/src/lib/mariadb/schema-to-typedef.ts @@ -0,0 +1,35 @@ +import path from "node:path"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import type { + BUN_MARIADB_DatabaseSchemaType, + BunMariaDBConfig, +} from "../../types"; +import dbSchemaToType from "./db-schema-to-typedef"; + +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; + dst_file: string; + config: BunMariaDBConfig; +}; + +export default function dbSchemaToTypeDef({ + dbSchema, + dst_file, + config, +}: Params) { + try { + if (!dbSchema) throw new Error("No schema found"); + + const definitions = dbSchemaToType({ dbSchema, config }); + + 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/mariadb/schema.ts b/src/lib/mariadb/schema.ts new file mode 100644 index 0000000..8926845 --- /dev/null +++ b/src/lib/mariadb/schema.ts @@ -0,0 +1,7 @@ +import _ from "lodash"; +import type { BUN_MARIADB_DatabaseSchemaType } from "../../types"; + +export const DbSchema: BUN_MARIADB_DatabaseSchemaType = { + dbName: "travis-ai", + tables: [], +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..095d938 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,1591 @@ +import type { RequestOptions } from "https"; +import type { ConnectionConfig } from "mariadb"; + +/** + * Fully-qualified database name used when a database needs to be referenced + * across local and remote operations. + */ +export type BUN_MARIADB_DatabaseFullName = string; + +/** + * User fields that should be omitted from general-purpose payloads and public + * responses. + */ +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; + +/** + * Describes an entire database schema, including its tables and clone/child + * database metadata. + */ +export interface BUN_MARIADB_DatabaseSchemaType { + id?: string | number; + dbName?: string; + dbSlug?: string; + dbFullName?: string; + dbDescription?: string; + dbImage?: string; + tables: BUN_MARIADB_TableSchemaType[]; + childrenDatabases?: BUN_MARIADB_ChildrenDatabaseObject[]; + childDatabase?: boolean; + childDatabaseDbId?: string | number; + updateData?: boolean; + collation?: (typeof MariaDBCollations)[number]; +} + +/** + * Minimal reference to a child database linked to a parent database schema. + */ +export interface BUN_MARIADB_ChildrenDatabaseObject { + dbId?: string | number; +} + +/** + * Supported MariaDB collations that can be applied at the database or table + * level. + */ +export const MariaDBCollations = [ + "utf8mb4_bin", + "utf8mb4_unicode_520_ci", +] as const; + +/** + * Describes a single table within a database schema, including its fields, + * indexes, unique constraints, and parent/child table relationships. + */ +export interface BUN_MARIADB_TableSchemaType { + id?: string | number; + tableName: string; + tableDescription?: string; + fields: BUN_MARIADB_FieldSchemaType[]; + indexes?: BUN_MARIADB_IndexSchemaType[]; + uniqueConstraints?: BUN_MARIADB_UniqueConstraintSchemaType[]; + childrenTables?: BUN_MARIADB_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]; + /** + * If this is a vector table + */ + isVector?: boolean; + /** + * Type of vector. Defaults to `vec0` + */ + vectorType?: string; +} + +/** + * Reference object used to link a table to one of its child tables. + */ +export interface BUN_MARIADB_ChildrenTablesType { + tableId?: string | number; + dbId?: string | number; +} + +/** + * Supported editor/content modes for text fields. + */ +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; + +/** + * Core MariaDB column types supported by the schema builder. + */ +export const BUN_MARIADB_DATATYPES = [ + { value: "TEXT" }, + { value: "INTEGER" }, + { value: "BLOB" }, + { value: "REAL" }, +] as const; + +/** + * Describes a table column, including SQL constraints, text editor options, + * vector metadata, and foreign key configuration. + */ +export type BUN_MARIADB_FieldSchemaType = { + id?: number | string; + fieldName?: string; + fieldDescription?: string; + originName?: string; + dataType: (typeof BUN_MARIADB_DATATYPES)[number]["value"]; + nullValue?: boolean; + notNullValue?: boolean; + primaryKey?: boolean; + encrypted?: boolean; + autoIncrement?: boolean; + defaultValue?: string | number; + defaultValueLiteral?: string; + foreignKey?: BUN_MARIADB_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; +} & { + [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; +}; + +/** + * Defines a foreign key relationship from one field to another table column. + */ +export interface BUN_MARIADB_ForeignKeyType { + foreignKeyName?: string; + destinationTableName?: string; + destinationTableColumnName?: string; + destinationTableColumnType?: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} + +/** + * Describes a table index and the fields it covers. + */ +export interface BUN_MARIADB_IndexSchemaType { + /** + * Name of the index as it would appear on schema. Eg. + * `idx_user_id_index` + */ + indexName?: string; + indexTableFields?: string[]; +} + +/** + * Describes a multi-field uniqueness rule for a table. + */ +export interface BUN_MARIADB_UniqueConstraintSchemaType { + id?: string | number; + constraintName?: string; + alias?: string; + constraintTableFields?: BUN_MARIADB_UniqueConstraintFieldType[]; +} + +/** + * Single field reference inside a unique constraint definition. + */ +export interface BUN_MARIADB_UniqueConstraintFieldType { + value: string; +} + +/** + * Field metadata used when generating SQL for indexes. + */ +export interface BUN_MARIADB_IndexTableFieldType { + value: string; + dataType: string; +} + +/** + * Result shape returned by MySQL `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_INDEXES_Type { + Key_name: string; + Table: string; + Column_name: string; + Collation: string; + Index_type: string; + Cardinality: string; + Index_comment: string; + Comment: string; +} + +/** + * Result shape returned by MySQL `SHOW COLUMNS` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_COLUMNS_Type { + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; +} + +/** + * Result shape returned by MariaDB `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_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"; +} + +/** + * Minimal metadata returned when listing MySQL foreign keys. + */ +export interface BUN_MARIADB_MYSQL_FOREIGN_KEYS_Type { + CONSTRAINT_NAME: string; + CONSTRAINT_SCHEMA: string; + TABLE_NAME: string; +} + +/** + * Database record describing a user-owned database and optional remote sync + * metadata. + */ +export interface BUN_MARIADB_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; +} + +/** + * Return value used when converting an uploaded image file to base64. + */ +export type ImageInputFileToBase64FunctionReturn = { + imageBase64?: string; + imageBase64Full?: string; + imageName?: string; + imageSize?: number; +}; + +/** + * Querystring parameters accepted by generic GET data endpoints. + */ +export interface GetReqQueryObject { + db: string; + query: string; + queryValues?: string; + tableName?: string; + debug?: boolean; +} + +/** + * Canonical logged-in user shape shared across auth, session, and API flows. + */ +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; +}; + +/** + * Standard authenticated-user response wrapper. + */ +export interface AuthenticatedUser { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + cookieNames?: any; +} + +/** + * Minimal successful user payload returned by user creation flows. + */ +export interface SuccessUserObject { + id: number; + first_name: string; + last_name: string; + email: string; +} + +/** + * Return type for helpers that create a user record. + */ +export interface AddUserFunctionReturn { + success: boolean; + payload?: SuccessUserObject | null; + msg?: string; + sqlResult?: any; +} + +/** + * Shape exposed by the Google Identity prompt lifecycle callback. + */ +export interface GoogleIdentityPromptNotification { + getMomentType: () => string; + getDismissedReason: () => string; + getNotDisplayedReason: () => string; + getSkippedReason: () => string; + isDismissedMoment: () => boolean; + isDisplayMoment: () => boolean; + isDisplayed: () => boolean; + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; +} + +/** + * User data accepted by registration and profile-creation flows. + */ +export type UserDataPayload = { + first_name: string; + last_name: string; + email: string; + password?: string; + username?: string; +} & { + [key: string]: any; +}; + +/** + * Return shape for helpers that fetch a single user. + */ +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; +} + +/** + * Return type for reauthentication flows that refresh auth state. + */ +export interface ReauthUserFunctionReturn { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + token?: string; +} + +/** + * Return type for user update helpers. + */ +export interface UpdateUserFunctionReturn { + success: boolean; + payload?: Object[] | string; +} + +/** + * Generic success/error wrapper for read operations. + */ +export interface GetReturn { + success: boolean; + payload?: R; + msg?: string; + error?: string; + schema?: BUN_MARIADB_TableSchemaType; + finalQuery?: string; +} + +/** + * Query parameters used when requesting schema metadata. + */ +export interface GetSchemaRequestQuery { + database?: string; + table?: string; + field?: string; + user_id?: string | number; + env?: { [k: string]: string }; +} + +/** + * API credential payload used when a schema request must be authenticated. + */ +export interface GetSchemaAPICredentialsParam { + key: string; +} + +/** + * Complete request object for authenticated schema fetches. + */ +export type GetSchemaAPIParam = GetSchemaRequestQuery & + GetSchemaAPICredentialsParam; + +/** + * Generic response wrapper for write operations. + */ +export interface PostReturn { + success: boolean; + payload?: Object[] | string | PostInsertReturn; + msg?: string; + error?: any; + schema?: BUN_MARIADB_TableSchemaType; +} + +/** + * High-level CRUD payload accepted by server-side data mutation handlers. + */ +export interface PostDataPayload { + action: "insert" | "update" | "delete"; + table: string; + data?: object; + identifierColumnName?: string; + identifierValue?: string; + duplicateColumnName?: string; + duplicateColumnValue?: string; + update?: boolean; +} + +/** + * Local-only variant of `PostReturn` used by in-process operations. + */ +export interface LocalPostReturn { + success: boolean; + payload?: any; + msg?: string; + error?: string; +} + +/** + * Query object for local write operations that may accept raw SQL or a CRUD + * payload. + */ +export interface LocalPostQueryObject { + query: string | PostDataPayload; + tableName?: string; + queryValues?: string[]; +} + +/** + * Insert/update metadata returned by SQL drivers after a write completes. + */ +export interface PostInsertReturn { + fieldCount?: number; + affectedRows?: number; + insertId?: number; + serverStatus?: number; + warningCount?: number; + message?: string; + protocol41?: boolean; + changedRows?: number; + error?: string; +} + +/** + * Extended user object used within the application runtime. + */ +export type UserType = DATASQUIREL_LoggedInUser & { + isSuperUser?: boolean; + staticHost?: string; + appHost?: string; + appName?: string; +}; + +/** + * Stored API key definition and its associated metadata. + */ +export interface ApiKeyDef { + name: string; + scope: string; + date_created: string; + apiKeyPayload: string; +} + +/** + * Aggregate dashboard metrics shown in admin and overview screens. + */ +export interface MetricsType { + dbCount: number; + tablesCount: number; + mediaCount: number; + apiKeysCount: number; +} + +/** + * Shape of the internal MariaDB users table. + */ +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; +} + +/** + * Credentials used to connect to a MariaDB instance as a specific user. + */ +export interface MariaDBUserCredType { + mariadb_user?: string; + mariadb_host?: string; + mariadb_pass?: string; +} + +/** + * Supported logical operators for server-side query generation. + */ +export const ServerQueryOperators = ["AND", "OR"] as const; +/** + * Supported comparison operators for server-side query generation. + */ +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", + "IS NOT", + "EXISTS", + "NOT EXISTS", + "GREATER THAN", + "GREATER THAN OR EQUAL", + "LESS THAN", + "LESS THAN OR EQUAL", + "MATCH", + "MATCH_BOOLEAN", +] as const; + +/** + * Top-level query-builder input used to generate SELECT statements, joins, + * grouping, pagination, and full-text search clauses. + */ +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; + /** + * Raw SQL to use as select + */ + // select_sql?: string; + [key: string]: any; +}; + +/** + * Represents a single `GROUP BY` field, optionally qualified by table name. + */ +export type ServerQueryParamGroupBy< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + field: keyof T; + table?: string; +}; + +/** + * Represents a single `ORDER BY` clause. + */ +export type ServerQueryParamOrder< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + field: keyof T; + strategy: "ASC" | "DESC"; +}; + +/** + * Configuration for a full-text search query and the alias used for the score + * column. + */ +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; +}; + +/** + * Describes a count subquery that is projected into the main SELECT result. + */ +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; +}; + +/** + * Source/target mapping used inside count subqueries. + */ +export type ServerQueryParamsCountSrcTrgMap = { + table: string; + field: string; +}; + +/** + * Declares a selected field and an optional alias for the result set. + */ +export type TableSelectFieldsObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + fieldName: keyof T; + alias?: string; + count?: { + alias?: string; + }; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Omit; + distinct?: boolean; +}; + +export type TableSelectFieldsBasicDirective = { + alias: string; +}; + +/** + * Value wrapper used when a query condition needs per-value metadata such as a + * custom equality or explicit table/field names. + */ +export type ServerQueryValuesObject = { + value?: string | number; + /** + * Defaults to EQUAL + */ + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: string; + fieldName?: string; +}; + +/** + * All value shapes accepted by a query condition, including arrays used by + * operators such as `IN` and `BETWEEN`. + */ +export type ServerQueryObjectValue = + | string + | number + | ServerQueryValuesObject + | undefined + | null + | (string | number | ServerQueryValuesObject | undefined | null)[]; + +/** + * Describes a single query condition and any nested subconditions for a field. + */ +export type ServerQueryObject< + T extends object = { [key: string]: any }, + K extends string = string, +> = SQLComparisonsParams & { + 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: string]: 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; +}; + +/** + * Field-to-condition map for server-side query generation. + */ +export type ServerQueryQueryObject< + T extends object = { [key: string]: any }, + K extends string = string, +> = { + [key in keyof T]: ServerQueryObject; +}; + +/** + * Parameters for authenticated fetch helpers used by the frontend and internal + * SDK layers. + */ +export type FetchDataParams = { + path: string; + method?: (typeof DataCrudRequestMethods)[number]; + body?: object | string; + query?: AuthFetchQuery; + tableName?: string; +}; + +/** + * Query object accepted by authenticated data-fetch helpers. + */ +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: any; +}; + +/** + * Join clause definition used by the server query builder. + */ +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 | SelectFieldObject)[]; + omitFields?: ( + | keyof Field + | { + field: keyof Field; + alias?: string; + count?: boolean; + } + )[]; + operator?: (typeof ServerQueryOperators)[number]; + /** + * Raw SQL to use as join select + */ + // select_sql?: string; + /** + * Concatenate multiple matches from another table + */ + group_concat?: GroupConcatObject; +}; + +export type GroupConcatObject = { + field: string; + alias: string; + /** + * Separator. Default `,` + */ + separator?: string; + distinct?: boolean; +}; + +export type SelectFieldObject = { + field: keyof Field; + alias?: string; + count?: boolean; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Pick; + distinct?: boolean; +}; + +export const SQlComparisons = [ + ">", + "<>", + "<", + "=", + ">=", + "<=", + "!=", + "IS NOT", + "IS", + "IS NULL", + "IS NOT NULL", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "GLOB", + "NOT GLOB", +] as const; + +export type SQLBetween = { + min: SQLInsertGenValueType; + max: SQLInsertGenValueType; +}; + +export type SQLComparisonsParams = { + raw_equality?: (typeof SQlComparisons)[number]; + between?: SQLBetween; + not_between?: SQLBetween; +}; + +/** + * Defines how a root-table field maps to a join-table field in an `ON` clause. + */ +export type ServerQueryParamsJoinMatchObject< + Field extends object = { [key: string]: any }, +> = SQLComparisonsParams & { + /** 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"; + }; +}; + +/** + * Explicit table/field reference for join match definitions. + */ +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; + +/** + * Payload used when pushing or pulling a schema to or from a remote API. + */ +export type ApiConnectBody = { + url: string; + key: string; + database: BUN_MARIADB_MYSQL_user_databases_Type; + dbSchema: BUN_MARIADB_DatabaseSchemaType; + type: "pull" | "push"; + user_id?: string | number; +}; + +/** + * Superuser credentials and auth state. + */ +export type SuUserType = { + email: string; + password: string; + authKey: string; + logged_in_status: boolean; + date: number; +}; + +/** + * Configuration for a remote MariaDB host in replicated or load-balanced + * setups. + */ +export type MariadbRemoteServerObject = { + host: string; + port: number; + primary?: boolean; + loadBalanced?: boolean; + users?: MariadbRemoteServerUserObject[]; +}; + +/** + * Credentials for a user provisioned on a remote MariaDB server. + */ +export type MariadbRemoteServerUserObject = { + name: string; + password: string; + host: string; +}; + +/** + * Standard login response returned by API authentication helpers. + */ +export type APILoginFunctionReturn = { + success: boolean; + msg?: string; + payload?: DATASQUIREL_LoggedInUser | null; + userId?: number | string; + key?: string; + token?: string; + csrf?: string; + cookieNames?: any; +}; + +/** + * Parameters required to create a user through the public API layer. + */ +export type APICreateUserFunctionParams = { + encryptionKey?: string; + payload: any; + database: string; + dsqlUserID?: string | number; + verify?: boolean; +}; + +/** + * Function signature for API user-creation handlers. + */ +export type APICreateUserFunction = ( + params: APICreateUserFunctionParams, +) => Promise; + +/** + * Result returned when reconciling a social login with the local user store. + */ +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; + +/** + * Cookie definition used when setting auth/session cookies. + */ +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"; +}; + +/** + * Request options accepted by the generic HTTP client helper. + */ +export type HttpRequestParams< + ReqObj extends { [k: string]: any } = { [k: string]: any }, +> = RequestOptions & { + scheme?: "http" | "https"; + body?: ReqObj; + query?: ReqObj; + urlEncodedFormBody?: boolean; +}; + +/** + * Function signature for the generic HTTP request helper. + */ +export type HttpRequestFunction< + ReqObj extends { [k: string]: any } = { [k: string]: any }, + ResObj extends { [k: string]: any } = { [k: string]: any }, +> = (param: HttpRequestParams) => Promise>; + +/** + * Normalized response returned by the generic HTTP request helper. + */ +export type HttpFunctionResponse< + ResObj extends { [k: string]: any } = { [k: string]: any }, +> = { + status: number; + data?: ResObj; + error?: string; + str?: string; + requestedPath?: string; +}; + +/** + * GET request payload for API data reads. + */ +export type ApiGetQueryObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + query: ServerQueryParam; + table: string; + dbFullName?: string; +}; + +/** + * Uppercase HTTP methods supported by the CRUD helpers. + */ +export const DataCrudRequestMethods = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", +] as const; + +/** + * Lowercase variant of the supported HTTP methods, used where string casing + * must match external APIs. + */ +export const DataCrudRequestMethodsLowerCase = [ + "get", + "post", + "put", + "patch", + "delete", + "options", +] as const; + +/** + * Runtime parameters passed to generic CRUD handlers. + */ +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; +}; + +/** + * Hook used to mutate input data before a CRUD action is executed. + */ +export type DsqlCrudTransformDataFunction< + T extends { [key: string]: any } = { [key: string]: any }, +> = (params: { + data: T; + user?: DATASQUIREL_LoggedInUser; + existingData?: T; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise; + +/** + * Hook used to mutate query input before a CRUD action is executed. + */ +export type DsqlCrudTransformQueryFunction< + T extends { [key: string]: any } = { [key: string]: any }, +> = (params: { + query: DsqlCrudQueryObject; + user?: DATASQUIREL_LoggedInUser; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise>; + +/** + * High-level CRUD actions supported by the DSQL helpers. + */ +export const DsqlCrudActions = ["insert", "update", "delete", "get"] as const; + +/** + * Query object used by CRUD helpers, built on top of the server query builder. + */ +export type DsqlCrudQueryObject< + T extends { [key: string]: any } = { [key: string]: any }, + K extends string = string, +> = ServerQueryParam & { + query?: ServerQueryQueryObject; +}; + +/** + * Parameters used to generate a SQL `DELETE` statement. + */ +export type SQLDeleteGeneratorParams< + T extends { [key: string]: any } = { [key: string]: any }, +> = { + tableName: string; + deleteKeyValues?: SQLDeleteData[]; + deleteKeyValuesOperator?: "AND" | "OR"; + dbFullName?: string; + data?: any; +}; + +/** + * Single key/value predicate used by the delete SQL generator. + */ +export type SQLDeleteData< + T extends { [key: string]: any } = { [key: string]: any }, +> = { + key: keyof T; + value: string | number | null | undefined; + operator?: (typeof ServerQueryEqualities)[number]; +}; + +/** + * Prebuilt SQL where-clause fragment and its bound parameters. + */ +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; + +/** + * Callback signature used when surfacing internal errors to callers. + */ +export type ErrorCallback = (title: string, error: Error, data?: any) => void; + +/** + * Reserved query parameter names used throughout the API layer. + */ +export const QueryFields = [ + "duplicate", + "user_id", + "delegated_user_id", + "db_id", + "table_id", + "db_slug", +] as const; + +/** + * Minimal representation of a local folder entry. + */ +export type LocalFolderType = { + name: string; + isPrivate: boolean; +}; + +/** + * SQL string and bound parameters produced during query generation. + */ +export type ResponseQueryObject = { + sql?: string; + params?: (string | number)[]; +}; + +/** + * Common API response wrapper used by data, auth, media, and utility + * endpoints. + */ +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?: BunMariaDBErrorObject[]; + 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; +}; + +/** + * Supported Docker Compose service names used by the deployment helpers. + */ +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; + +/** + * Map of compose service names to service definitions. + */ +export type DockerComposeServicesType = { + [key in (typeof DockerComposeServices)[number]]: DockerComposeServiceWithBuildObject; +}; + +/** + * Docker Compose network definitions. + */ +export type DockerComposeNetworks = { + [k: string]: { + driver?: "bridge"; + ipam?: { + config: DockerComposeNetworkConfigObject[]; + }; + external?: boolean; + }; +}; + +/** + * Static network config for a Docker Compose network. + */ +export type DockerComposeNetworkConfigObject = { + subnet: string; + gateway: string; +}; + +/** + * Service definition for compose services that are built from source. + */ +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; +}; + +/** + * Service definition for compose services that use a prebuilt image. + */ +export type DockerComposeServiceWithImage = Omit< + DockerComposeServiceWithBuildObject, + "build" +> & { + image: string; +}; + +/** + * Build instructions for a Docker Compose service. + */ +export type DockerComposeServicesBuildObject = { + context: string; + dockerfile: string; +}; + +/** + * Per-network addressing information for a Docker Compose service. + */ +export type DockerComposeServiceNetworkObject = { + [k: string]: { + ipv4_address: string; + }; +}; + +/** + * Metadata recorded for a table cloned from another database. + */ +export type ClonedTableInfo = { + dbId?: string | number; + tableId?: string | number; + keepUpdated?: boolean; + keepDataUpdated?: boolean; +}; + +/** + * Common columns present on default/generated table entries. + */ +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; +}; + +/** + * Supported index strategies for generated schemas. + */ +export const IndexTypes = ["regular", "full_text", "vector"] as const; + +/** + * Structured error payload captured alongside generated SQL. + */ +export type BunMariaDBErrorObject = { + sql?: string; + sqlValues?: any[]; + error?: string; +}; + +/** + * Output of the SQL insert generator. + */ +export interface SQLInsertGenReturn { + query: string; + values: SQLInsertGenValueType[]; +} + +/** + * Values accepted by the SQL insert generator, including vector buffers. + */ +export type SQLInsertGenValueType = + | string + | number + | Float32Array + | Buffer + | null; + +/** + * Callback variant used when a generated insert value needs a custom + * placeholder. + */ +export type SQLInsertGenDataFn = () => { + placeholder: string; + value: SQLInsertGenValueType; +}; + +/** + * Record shape accepted by the SQL insert generator. + */ +export type SQLInsertGenDataType = { + [k: string]: SQLInsertGenValueType | SQLInsertGenDataFn | undefined | null; +}; + +/** + * Parameters used to build a multi-row SQL insert statement. + */ +export type SQLInsertGenParams = { + data: SQLInsertGenDataType[]; + tableName: string; + dbFullName?: string; +}; + +/** + * Library configuration used to locate the MariaDB database, schema file, + * generated types, and backup settings. + */ +export type BunMariaDBConfig = { + 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; + /** + * Whether to enable `WAL` mode + */ + wal_mode?: boolean; +}; + +/** + * Resolved Bun MariaDB config paired with the loaded database schema. + */ +export type BunMariaDBConfigReturn = { + config: BunMariaDBConfig; + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; + +/** + * Default fields automatically suggested for new tables. + */ +export const DefaultFields: BUN_MARIADB_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)", + }, +]; + +export type BunMariaDBQueryFieldValues< + F extends string = string, + T extends string = string, +> = { + field: F; + table?: T; +}; + +export type QueryRawValueType = string | number | null | undefined; + +export type DsqlConnectionParam = { + /** + * No Database Connection + */ + noDb?: boolean; + /** + * Database Name + */ + database?: string; + /** + * Debug + */ + config?: ConnectionConfig; +}; + +export type DBResponseObject< + T extends { [k: string]: any } = { [k: string]: any }, +> = { + success: boolean; + payload?: T[]; + single_res?: T; +}; 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..87a02c9 --- /dev/null +++ b/src/utils/append-default-fields-to-db-schema.ts @@ -0,0 +1,20 @@ +import _ from "lodash"; +import { DefaultFields, type BUN_MARIADB_DatabaseSchemaType } from "../types"; + +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; + +export default function ({ dbSchema }: Params): BUN_MARIADB_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..f632173 --- /dev/null +++ b/src/utils/grab-db-backup-file-name.ts @@ -0,0 +1,11 @@ +import type { BunMariaDBConfig } from "../types"; + +type Params = { + config: BunMariaDBConfig; +}; + +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..8e9dcd7 --- /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 { BunMariaDBConfig } from "../types"; +import { AppData } from "../data/app-data"; + +type Params = { + config: BunMariaDBConfig; +}; + +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-db-schema.ts b/src/utils/grab-db-schema.ts new file mode 100644 index 0000000..bcf4429 --- /dev/null +++ b/src/utils/grab-db-schema.ts @@ -0,0 +1,6 @@ +export default async function grabDbSchema() { + const config = global.CONFIG; + const dbSchema = global.DB_SCHEMA; + + return dbSchema; +} diff --git a/src/utils/grab-join-fields-from-query-object.ts b/src/utils/grab-join-fields-from-query-object.ts new file mode 100644 index 0000000..8547d1e --- /dev/null +++ b/src/utils/grab-join-fields-from-query-object.ts @@ -0,0 +1,94 @@ +import _ from "lodash"; +import type { + BunMariaDBQueryFieldValues, + ServerQueryParam, + ServerQueryParamsJoin, +} from "../types"; + +type Params = Record> = { + query: ServerQueryParam; + ignore_select_fields?: boolean; +}; + +export default function grabJoinFieldsFromQueryObject< + Q extends Record = Record, + F extends string = string, + T extends string = string, +>({ + query, + ignore_select_fields, +}: Params): BunMariaDBQueryFieldValues[] { + const fields_values: BunMariaDBQueryFieldValues[] = []; + const new_query = _.cloneDeep(query); + + if (new_query.join) { + for (let i = 0; i < new_query.join.length; i++) { + const join = new_query.join[i]; + + if (!join) continue; + + if (Array.isArray(join)) { + for (let i = 0; i < join.length; i++) { + const single_join = join[i]; + fields_values.push( + ...(grabSingleJoinData({ + join: single_join as ServerQueryParamsJoin, + ignore_select_fields, + }) as BunMariaDBQueryFieldValues[]), + ); + } + } else { + fields_values.push( + ...(grabSingleJoinData({ + join: join as ServerQueryParamsJoin, + ignore_select_fields, + }) as BunMariaDBQueryFieldValues[]), + ); + } + } + } + + return fields_values; +} + +function grabSingleJoinData({ + join, + ignore_select_fields, +}: { + join: ServerQueryParamsJoin; + ignore_select_fields?: boolean; +}): BunMariaDBQueryFieldValues[] { + let values: BunMariaDBQueryFieldValues[] = []; + + const join_select_fields = join?.selectFields; + + if (!join_select_fields?.[0] && !ignore_select_fields) { + throw new Error( + `\`selectFields\` required in joins. To ignore this error, pass the \`ignore_select_fields\` parameter`, + ); + } + + if (join_select_fields?.[0]) { + for (let i = 0; i < join_select_fields.length; i++) { + const select_field = join_select_fields[i]; + if (select_field) { + values.push({ + table: join.tableName, + field: + typeof select_field == "object" + ? String(select_field.field) + : String(select_field), + }); + } + } + } + + if (join.group_concat) { + values.push({ + table: join.tableName, + field: join.group_concat.field, + }); + } + + return values; +} diff --git a/src/utils/grab-sorted-backups.ts b/src/utils/grab-sorted-backups.ts new file mode 100644 index 0000000..96c4c19 --- /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 { BunMariaDBConfig } from "../types"; + +type Params = { + config: BunMariaDBConfig; +}; + +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/query-value-parser.ts b/src/utils/query-value-parser.ts new file mode 100644 index 0000000..896c3a7 --- /dev/null +++ b/src/utils/query-value-parser.ts @@ -0,0 +1,33 @@ +import type { QueryRawValueType, ServerQueryObjectValue } from "../types"; + +type Params = { + query_value: ServerQueryObjectValue; +}; + +export default function queryValueParser({ + query_value, +}: Params): QueryRawValueType | QueryRawValueType[] { + if (typeof query_value == "string" || typeof query_value == "number") { + return query_value; + } + + if (Array.isArray(query_value)) { + let values: QueryRawValueType[] = []; + + for (let i = 0; i < query_value.length; i++) { + const single_value = query_value[i]; + if (single_value) { + const single_parsed_value = queryValueParser({ + query_value: single_value, + }); + if (!Array.isArray(single_parsed_value)) { + values.push(single_parsed_value); + } + } + } + + return values; + } + + return query_value?.value; +} diff --git a/src/utils/sql-equality-parser.ts b/src/utils/sql-equality-parser.ts new file mode 100644 index 0000000..afb6972 --- /dev/null +++ b/src/utils/sql-equality-parser.ts @@ -0,0 +1,44 @@ +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 "IS NOT": + return "IS NOT"; + 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..3ffd445 --- /dev/null +++ b/src/utils/sql-gen-operator-gen.ts @@ -0,0 +1,149 @@ +import type { + ServerQueryEqualities, + ServerQueryObject, + SQLInsertGenValueType, +} from "../types"; +import sqlEqualityParser from "./sql-equality-parser"; + +type Params = { + fieldName: string; + value?: SQLInsertGenValueType; + equality?: (typeof ServerQueryEqualities)[number]; + queryObj: ServerQueryObject< + { + [key: string]: any; + }, + string + >; + isValueFieldValue?: boolean; +}; + +type Return = { + str?: string; + param?: SQLInsertGenValueType; +}; + +/** + * # 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 == "IS NOT") { + return { + str: `${fieldName} IS NOT ${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-gen-join-str.ts b/src/utils/sql-generator-gen-join-str.ts new file mode 100644 index 0000000..be646de --- /dev/null +++ b/src/utils/sql-generator-gen-join-str.ts @@ -0,0 +1,109 @@ +import type { + ServerQueryParamsJoin, + ServerQueryParamsJoinMatchObject, + SQLInsertGenValueType, +} from "../types"; + +type Param = { + mtch: ServerQueryParamsJoinMatchObject; + join: ServerQueryParamsJoin; + table_name: string; +}; + +export default function sqlGenGenJoinStr({ join, mtch, table_name }: Param) { + let values: SQLInsertGenValueType[] = []; + + 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; + + const { str, values: batch_values } = sqlGenGenJoinStr({ + join, + mtch: __mtch, + table_name, + }); + + btch_mtch += str; + + values.push(...batch_values); + + if (i < mtch.__batch.matches.length - 1) { + btch_mtch += ` ${mtch.__batch.operator || "OR"} `; + } + } + + btch_mtch += `)`; + + return { + str: btch_mtch, + values, + }; + } + + const equality = mtch.raw_equality || "="; + + const lhs = `${ + typeof mtch.source == "object" ? mtch.source.tableName : table_name + }.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}`; + + const rhs = `${(() => { + if (mtch.targetLiteral) { + values.push(mtch.targetLiteral); + + // if (typeof mtch.targetLiteral == "number") { + // return `${mtch.targetLiteral}`; + // } + // return `'${mtch.targetLiteral}'`; + + return `?`; + } + + if (join.alias) { + return `${ + typeof mtch.target == "object" + ? mtch.target.tableName + : join.alias + }.${ + typeof mtch.target == "object" + ? mtch.target.fieldName + : mtch.target + }`; + } + + return `${ + typeof mtch.target == "object" + ? mtch.target.tableName + : join.tableName + }.${ + typeof mtch.target == "object" ? mtch.target.fieldName : mtch.target + }`; + })()}`; + + if (mtch.between) { + values.push(mtch.between.min, mtch.between.max); + + return { + str: `${lhs} BETWEEN ? AND ?`, + values, + }; + } + + if (mtch.not_between) { + values.push(mtch.not_between.min, mtch.not_between.max); + + return { + str: `${lhs} NOT BETWEEN ? AND ?`, + values, + }; + } + + return { + str: `${lhs} ${equality} ${rhs}`, + values, + }; +} diff --git a/src/utils/sql-generator-gen-query-str.ts b/src/utils/sql-generator-gen-query-str.ts new file mode 100644 index 0000000..5492ac1 --- /dev/null +++ b/src/utils/sql-generator-gen-query-str.ts @@ -0,0 +1,235 @@ +import { isUndefined } from "lodash"; +import type { ServerQueryParam, TableSelectFieldsObject } from "../types"; +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; +import sqlGenGenJoinStr from "./sql-generator-gen-join-str"; +import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql"; + +type Param = { + genObject?: ServerQueryParam; + selectFields?: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; + full_text_match_str?: string; + full_text_search_str?: string; +}; + +export default function sqlGenGenQueryStr< + T extends { [key: string]: any } = { [key: string]: any }, +>(params: Param) { + let str = "SELECT"; + + const genObject = params.genObject; + const table_name = params.table_name; + const full_text_match_str = params.full_text_match_str; + const full_text_search_str = params.full_text_search_str; + + let sqlSearhValues: any[] = []; + + if (genObject?.select_sql) { + str += ` ${genObject.select_sql}`; + } else if (genObject?.selectFields?.[0]) { + if (genObject.join) { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + append_table_names: true, + table_name, + }); + } else { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + table_name, + }); + } + } else { + if (genObject?.join) { + str += ` ${table_name}.*`; + } 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) { + const existingJoinTableNames: string[] = [table_name]; + + 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.group_concat) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${joinObj.group_concat.field}`, + alias: joinObj.group_concat.alias, + separator: joinObj.group_concat.separator, + }); + } else if (joinObj.selectFields) { + return joinObj.selectFields + .map((selectField) => { + if (typeof selectField == "string") { + return `${joinTableName}.${selectField}`; + } else if (typeof selectField == "object") { + let aliasSelectField = `${joinTableName}.${selectField.field}`; + + if (selectField.count) { + aliasSelectField = `COUNT(${joinTableName}.${selectField.field})`; + } else if (selectField.sum) { + aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${joinTableName}.${selectField.field})`; + } else if (selectField.average) { + aliasSelectField = `AVERAGE(${joinTableName}.${selectField.field})`; + } else if (selectField.max) { + aliasSelectField = `MAX(${joinTableName}.${selectField.field})`; + } else if (selectField.min) { + aliasSelectField = `MIN(${joinTableName}.${selectField.field})`; + } else if ( + selectField.group_concat && + selectField.alias + ) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${selectField.field}`, + alias: selectField.alias, + separator: + selectField.group_concat + .separator, + distinct: + selectField.group_concat + .distinct, + }); + } else if (selectField.distinct) { + aliasSelectField = `DISTINCT ${joinTableName}.${selectField.field}`; + } + + if (selectField.alias) + aliasSelectField += ` AS ${selectField.alias}`; + return aliasSelectField; + } + }) + .join(","); + } else { + return `${joinTableName}.*`; + } + }) + .filter((_) => Boolean(_)) + .join(","); + } + + if ( + genObject?.fullTextSearch && + full_text_match_str && + full_text_search_str + ) { + str += `, ${full_text_match_str} AS ${genObject.fullTextSearch.scoreAlias}`; + sqlSearhValues.push(full_text_search_str); + } + + str += ` FROM ${table_name}`; + + if (genObject?.join) { + str += + " " + + genObject.join + .flat() + .filter((j) => !isUndefined(j)) + .map((join) => { + return ( + join.joinType + + " " + + (join.alias + ? `${join.tableName}` + " " + join.alias + : `${join.tableName}`) + + " ON " + + (() => { + if (Array.isArray(join.match)) { + return ( + "(" + + join.match + .map((mtch) => { + const { str, values } = + sqlGenGenJoinStr({ + mtch, + join, + table_name, + }); + + sqlSearhValues.push(...values); + + return str; + }) + .join( + join.operator + ? ` ${join.operator} ` + : " AND ", + ) + + ")" + ); + } else if (typeof join.match == "object") { + const { str, values } = sqlGenGenJoinStr({ + mtch: join.match, + join, + table_name, + }); + + sqlSearhValues.push(...values); + + return str; + } + })() + ); + }) + .join(" "); + } + + return { str, values: sqlSearhValues }; +} diff --git a/src/utils/sql-generator-gen-search-str.ts b/src/utils/sql-generator-gen-search-str.ts new file mode 100644 index 0000000..07cc0e9 --- /dev/null +++ b/src/utils/sql-generator-gen-search-str.ts @@ -0,0 +1,123 @@ +import type { + QueryRawValueType, + ServerQueryParamsJoin, + ServerQueryQueryObject, + ServerQueryValuesObject, + SQLInsertGenValueType, +} from "../types"; +import sqlGenOperatorGen from "./sql-gen-operator-gen"; + +type Param = { + queryObj: ServerQueryQueryObject[string]; + join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[]; + field?: string; + table_name: string; +}; + +export default function sqlGenGenSearchStr({ + queryObj, + join, + field, + table_name, +}: Param) { + let sqlSearhValues: SQLInsertGenValueType[] = []; + + const finalFieldName = (() => { + if (queryObj?.tableName) { + return `${queryObj.tableName}.${field}`; + } + if (join) { + return `${table_name}.${field}`; + } + return field; + })(); + + let str = `${finalFieldName}=?`; + + function grabValue(val?: string | number | ServerQueryValuesObject | null) { + const valueParsed = val; + + if (!valueParsed) return; + + const valueString = + typeof valueParsed == "string" || typeof valueParsed == "number" + ? valueParsed + : valueParsed + ? valueParsed.fieldName && valueParsed.tableName + ? `${valueParsed.tableName}.${valueParsed.fieldName}` + : valueParsed.value + : undefined; + + const valueEquality = + typeof valueParsed == "object" + ? valueParsed.equality || queryObj.equality + : queryObj.equality; + + const operatorStrParam = sqlGenOperatorGen({ + queryObj, + equality: valueEquality, + fieldName: finalFieldName || "", + value: valueString || "", + 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 if (queryObj.raw_equality && queryObj.value) { + str = `${finalFieldName} ${queryObj.raw_equality} ?`; + sqlSearhValues.push(queryObj.value); + } else if (queryObj.between) { + str = `${finalFieldName} BETWEEN ? AND ?`; + sqlSearhValues.push(queryObj.between.min, queryObj.between.max); + } else { + const valueParsed = queryObj.value ? 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 && !operatorStrParam.str.match(/\?/)) { + str = operatorStrParam.str; + } else { + sqlSearhValues.push(valueParsed || ""); + } + } + + return { str, values: sqlSearhValues }; +} diff --git a/src/utils/sql-generator-grab-concat-str.ts b/src/utils/sql-generator-grab-concat-str.ts new file mode 100644 index 0000000..30ff581 --- /dev/null +++ b/src/utils/sql-generator-grab-concat-str.ts @@ -0,0 +1,30 @@ +type Param = { + field: string; + alias: string; + separator?: string; + distinct?: boolean; +}; + +export default function sqlGenGrabConcatStr({ + alias, + field, + separator = ",", + distinct, +}: Param) { + let gc = `GROUP_CONCAT(`; + + if (distinct) { + gc += `DISTINCT `; + } + + gc += `${field}`; + + if (!distinct) { + gc += `, '${separator}'`; + } + + gc += `)`; + gc += ` AS ${alias}`; + + return gc; +} diff --git a/src/utils/sql-generator-grab-select-field-sql.ts b/src/utils/sql-generator-grab-select-field-sql.ts new file mode 100644 index 0000000..668570b --- /dev/null +++ b/src/utils/sql-generator-grab-select-field-sql.ts @@ -0,0 +1,65 @@ +import type { TableSelectFieldsObject } from "../types"; +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; + +type Param = { + selectFields: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; +}; + +export default function sqlGenGrabSelectFieldSQL< + T extends { [key: string]: any } = { [key: string]: any }, +>({ selectFields, append_table_names, table_name }: Param) { + let str = ""; + + str += ` ${selectFields + ?.map((fld) => { + let fld_str = ``; + + const final_fld_name = + typeof fld == "object" + ? append_table_names + ? `${table_name}.${String(fld)}` + : `${String(fld.fieldName)}` + : `${String(fld)}`; + + if (typeof fld == "object") { + const fld_name = `${String(fld.fieldName)}`; + + if (fld.count) { + fld_str += `COUNT(${fld_name})`; + } else if (fld.sum) { + fld_str += `SUM(${fld_name})`; + } else if (fld.average) { + fld_str += `AVERAGE(${fld_name})`; + } else if (fld.max) { + fld_str += `MAX(${fld_name})`; + } else if (fld.min) { + fld_str += `MIN(${fld_name})`; + } else if (fld.distinct) { + fld_str += `DISTINCT ${fld_name}`; + } else if (fld.group_concat) { + fld_str += sqlGenGrabConcatStr({ + field: fld_name, + alias: fld.group_concat.alias, + separator: fld.group_concat.separator, + distinct: fld.group_concat.distinct, + }); + } else { + fld_str += + final_fld_name + (fld.alias ? ` as ${fld.alias}` : ``); + } + + if (fld.alias) { + fld_str += ` AS ${fld.alias}`; + } + } else { + fld_str += final_fld_name; + } + + return fld_str; + }) + .join(",")}`; + + return str; +} diff --git a/src/utils/sql-generator.ts b/src/utils/sql-generator.ts new file mode 100644 index 0000000..9b6511f --- /dev/null +++ b/src/utils/sql-generator.ts @@ -0,0 +1,378 @@ +import type { + ServerQueryParam, + ServerQueryParamOrder, + SQLInsertGenValueType, +} from "../types"; +import sqlGenGenSearchStr from "./sql-generator-gen-search-str"; +import sqlGenGenQueryStr from "./sql-generator-gen-query-str"; + +type Param = { + genObject?: ServerQueryParam; + tableName: string; + dbFullName?: string; + count?: boolean; +}; + +type Return = { + string: string; + values: SQLInsertGenValueType[]; +}; + +/** + * # 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: SQLInsertGenValueType[] = []; + + 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 { str: queryString, values } = sqlGenGenQueryStr({ + table_name: tableName, + append_table_names: true, + full_text_match_str: fullTextMatchStr, + full_text_search_str: fullTextSearchStr, + genObject, + }); + + sqlSearhValues.push(...values); + + 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) { + const { str, values } = sqlGenGenSearchStr({ + queryObj: newSubQueryObj, + field: newSubQueryObj.fieldName || _field, + join: genObject?.join, + table_name: tableName, + }); + + sqlSearhValues.push(...values); + + return str; + } + }); + + return ( + "(" + + subSearchString.join(` ${queryObj.operator || "AND"} `) + + ")" + ); + } + + const { str, values } = sqlGenGenSearchStr({ + queryObj, + field: queryObj.fieldName || field, + join: genObject?.join, + table_name: tableName, + }); + + sqlSearhValues.push(...values); + + return str; + }); + + 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(`${tableName}.${String(order.field)}`); + } else { + orderFields.push(order.field); + } + + orderSrt += ` ${orderFields.join(", ")} ${order.strategy}`; + + return orderSrt; + } + + if (genObject?.order) { + 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) { + queryString += ` OFFSET ${genObject.offset}`; + } else if (genObject?.page && genObject.limit && !count) { + queryString += ` OFFSET ${(genObject.page - 1) * genObject.limit}`; + } + + return { + string: queryString, + values: sqlSearhValues, + }; +} + +// let queryString = (() => { +// let str = "SELECT"; + +// if (genObject?.select_sql) { +// str += ` ${genObject.select_sql}`; +// } else if (genObject?.selectFields?.[0]) { +// if (genObject.join) { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// append_table_names: true, +// table_name: tableName, +// }); +// } else { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// table_name: tableName, +// }); +// } +// } else { +// if (genObject?.join) { +// str += ` ${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) { +// 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.group_concat) { +// return sqlGenGrabConcatStr({ +// field: `${joinTableName}.${joinObj.group_concat.field}`, +// alias: joinObj.group_concat.alias, +// separator: joinObj.group_concat.separator, +// }); +// } else if (joinObj.selectFields) { +// return joinObj.selectFields +// .map((selectField) => { +// if (typeof selectField == "string") { +// return `${joinTableName}.${selectField}`; +// } else if (typeof selectField == "object") { +// let aliasSelectField = selectField.count +// ? `COUNT(${joinTableName}.${selectField.field})` +// : `${joinTableName}.${selectField.field}`; +// if (selectField.alias) +// aliasSelectField += ` AS ${selectField.alias}`; +// return aliasSelectField; +// } +// }) +// .join(","); +// } else { +// return `${joinTableName}.*`; +// } +// }) +// .filter((_) => Boolean(_)) +// .join(","); +// } + +// if ( +// genObject?.fullTextSearch && +// fullTextMatchStr && +// fullTextSearchStr +// ) { +// str += `, ${fullTextMatchStr} AS ${genObject.fullTextSearch.scoreAlias}`; +// sqlSearhValues.push(fullTextSearchStr); +// } + +// str += ` FROM ${tableName}`; + +// if (genObject?.join) { +// str += +// " " + +// genObject.join +// .flat() +// .filter((j) => !isUndefined(j)) +// .map((join) => { +// return ( +// join.joinType + +// " " + +// (join.alias +// ? `${join.tableName}` + " " + join.alias +// : `${join.tableName}`) + +// " ON " + +// (() => { +// if (Array.isArray(join.match)) { +// return ( +// "(" + +// join.match +// .map((mtch) => +// sqlGenGenJoinStr({ +// mtch, +// join, +// table_name: tableName, +// }), +// ) +// .join( +// join.operator +// ? ` ${join.operator} ` +// : " AND ", +// ) + +// ")" +// ); +// } else if (typeof join.match == "object") { +// return sqlGenGenJoinStr({ +// mtch: join.match, +// join, +// table_name: tableName, +// }); +// } +// })() +// ); +// }) +// .join(" "); +// } + +// return str; +// })(); diff --git a/src/utils/sql-insert-generator.ts b/src/utils/sql-insert-generator.ts new file mode 100644 index 0000000..86bd612 --- /dev/null +++ b/src/utils/sql-insert-generator.ts @@ -0,0 +1,82 @@ +import type { + SQLInsertGenParams, + SQLInsertGenReturn, + SQLInsertGenValueType, +} 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: SQLInsertGenValueType[] = []; + + data.forEach((item) => { + queryBatches.push( + `(${insertKeys + .map((ky) => { + const value = item[ky]; + + const finalValue = + typeof value == "string" || + typeof value == "number" + ? value + : typeof value == "function" + ? value().value + : value + ? value + : null; + + if (!finalValue) { + queryValues.push(null); + 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..46f170b --- /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 { BunMariaDBConfig } from "../types"; +import grabSortedBackups from "./grab-sorted-backups"; +import { AppData } from "../data/app-data"; +import path from "path"; + +type Params = { + config: BunMariaDBConfig; +}; + +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..2b0f895 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "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"] +}