Major version update. Major bugfixes. Security vulnerabilites fixed
This commit is contained in:
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
export declare const ExportArchiveMembers: {
|
||||
readonly DbFileName: "database.sqlite";
|
||||
readonly SchemaFileName: "schema";
|
||||
};
|
||||
export type ExportArchiveContents = {
|
||||
db: Uint8Array;
|
||||
schema: string;
|
||||
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
|
||||
schemaFileName: string;
|
||||
};
|
||||
export declare function isArchivePath(filePath: string): boolean;
|
||||
export declare function isSqlPath(filePath: string): boolean;
|
||||
export declare function isSQLiteFile(bytes: Uint8Array): boolean;
|
||||
/**
|
||||
* Create a portable export archive (tar / tar.gz / zip) with the database
|
||||
* file + schema file.
|
||||
*/
|
||||
export declare function writeExportArchive({ contents, outPath, }: {
|
||||
contents: ExportArchiveContents;
|
||||
outPath: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* Read a portable export archive (tar / tar.gz / zip).
|
||||
*/
|
||||
export declare function readExportArchive(archivePath: string): Promise<ExportArchiveContents>;
|
||||
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { AppData } from "../data/app-data";
|
||||
import grabDirNames from "../data/grab-dir-names";
|
||||
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||
export const ExportArchiveMembers = {
|
||||
DbFileName: "database.sqlite",
|
||||
SchemaFileName: AppData.DbSchemaFileName,
|
||||
};
|
||||
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"];
|
||||
/** Every SQLite database file starts with this header */
|
||||
const SQLITE_HEADER = "SQLite format 3\0";
|
||||
export function isArchivePath(filePath) {
|
||||
const lower = filePath.toLowerCase();
|
||||
return ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||
}
|
||||
export function isSqlPath(filePath) {
|
||||
return filePath.toLowerCase().endsWith(".sql");
|
||||
}
|
||||
export function isSQLiteFile(bytes) {
|
||||
return (new TextDecoder().decode(bytes.subarray(0, SQLITE_HEADER.length)) ===
|
||||
SQLITE_HEADER);
|
||||
}
|
||||
/**
|
||||
* Create a portable export archive (tar / tar.gz / zip) with the database
|
||||
* file + schema file.
|
||||
*/
|
||||
export async function writeExportArchive({ contents, outPath, }) {
|
||||
const lower = outPath.toLowerCase();
|
||||
if (lower.endsWith(".zip")) {
|
||||
await writeZipArchive({ contents, outPath });
|
||||
return;
|
||||
}
|
||||
const members = {
|
||||
[ExportArchiveMembers.DbFileName]: contents.db,
|
||||
[contents.schemaFileName]: contents.schema,
|
||||
};
|
||||
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||
if (gzip) {
|
||||
await Bun.Archive.write(outPath, members, { compress: "gzip" });
|
||||
}
|
||||
else {
|
||||
await Bun.Archive.write(outPath, members);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read a portable export archive (tar / tar.gz / zip).
|
||||
*/
|
||||
export async function readExportArchive(archivePath) {
|
||||
if (archivePath.toLowerCase().endsWith(".zip")) {
|
||||
return readZipArchive(archivePath);
|
||||
}
|
||||
const bytes = await Bun.file(archivePath).bytes();
|
||||
const archive = new Bun.Archive(bytes);
|
||||
const files = await archive.files();
|
||||
let db = null;
|
||||
let schemaEntry = null;
|
||||
for (const [entry, file] of files) {
|
||||
const base = path.basename(entry);
|
||||
if (!db && base === ExportArchiveMembers.DbFileName) {
|
||||
db = await file.bytes();
|
||||
}
|
||||
else if (!schemaEntry && isSchemaFileName(base)) {
|
||||
schemaEntry = {
|
||||
content: await file.text(),
|
||||
fileName: schemaMemberName(base),
|
||||
};
|
||||
}
|
||||
}
|
||||
return validateContents({ db, schemaEntry });
|
||||
}
|
||||
function schemaMemberName(base) {
|
||||
return base === AppData.DbSchemaFileName ? "schema.ts" : base;
|
||||
}
|
||||
function validateContents({ db, schemaEntry, }) {
|
||||
if (!db) {
|
||||
throw new Error(`Archive is missing the database file (expected \`${ExportArchiveMembers.DbFileName}\`)`);
|
||||
}
|
||||
if (!isSQLiteFile(db)) {
|
||||
throw new Error(`\`${ExportArchiveMembers.DbFileName}\` in the archive is not a SQLite database`);
|
||||
}
|
||||
if (!schemaEntry) {
|
||||
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|js|json|yaml|yml\`)`);
|
||||
}
|
||||
return {
|
||||
db,
|
||||
schema: schemaEntry.content,
|
||||
schemaFileName: schemaEntry.fileName,
|
||||
};
|
||||
}
|
||||
async function runCommand(cmd, cwd) {
|
||||
const proc = Bun.spawn(cmd, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const [stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`${cmd[0]} failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`${cmd[0]}\` is installed, or use .tar.gz.`);
|
||||
}
|
||||
}
|
||||
function makeTempDir(prefix) {
|
||||
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
|
||||
const tempDir = path.join(BUN_SQLITE_TEMP_DIR, `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
return tempDir;
|
||||
}
|
||||
async function writeZipArchive({ contents, outPath, }) {
|
||||
const tempDir = makeTempDir("export");
|
||||
try {
|
||||
fs.writeFileSync(path.join(tempDir, ExportArchiveMembers.DbFileName), contents.db);
|
||||
fs.writeFileSync(path.join(tempDir, contents.schemaFileName), contents.schema, "utf-8");
|
||||
await runCommand([
|
||||
"zip",
|
||||
"-q",
|
||||
"-j",
|
||||
path.resolve(outPath),
|
||||
ExportArchiveMembers.DbFileName,
|
||||
contents.schemaFileName,
|
||||
], tempDir);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
async function readZipArchive(archivePath) {
|
||||
const tempDir = makeTempDir("import");
|
||||
try {
|
||||
await runCommand([
|
||||
"unzip",
|
||||
"-q",
|
||||
"-o",
|
||||
path.resolve(archivePath),
|
||||
"-d",
|
||||
tempDir,
|
||||
]);
|
||||
let db = null;
|
||||
let schemaEntry = null;
|
||||
const stack = [tempDir];
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
for (const entry of fs.readdirSync(current, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
const full = path.join(current, entry.name);
|
||||
// Never follow symlinks from an extracted (untrusted) archive
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full);
|
||||
}
|
||||
else if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
else if (!db &&
|
||||
entry.name === ExportArchiveMembers.DbFileName) {
|
||||
db = new Uint8Array(fs.readFileSync(full));
|
||||
}
|
||||
else if (!schemaEntry && isSchemaFileName(entry.name)) {
|
||||
schemaEntry = {
|
||||
content: fs.readFileSync(full, "utf-8"),
|
||||
fileName: schemaMemberName(entry.name),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return validateContents({ db, schemaEntry });
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ type Params = {
|
||||
export default function grabDBDir({ config }: Params): {
|
||||
db_dir: string;
|
||||
backup_dir: string;
|
||||
export_dir: string;
|
||||
db_file_path: string;
|
||||
};
|
||||
export {};
|
||||
|
||||
Vendored
+3
-2
@@ -5,10 +5,11 @@ export default function grabDBDir({ config }) {
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
let db_dir = ROOT_DIR;
|
||||
if (config.db_dir) {
|
||||
db_dir = config.db_dir;
|
||||
db_dir = path.resolve(ROOT_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 export_dir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
|
||||
const db_file_path = path.resolve(db_dir, config.db_name);
|
||||
return { db_dir, backup_dir, db_file_path };
|
||||
return { db_dir, backup_dir, export_dir, db_file_path };
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
export default function grabDbSchema(): Promise<import("../types").BUN_SQLITE_DatabaseSchemaType>;
|
||||
export default function grabDbSchema(): Promise<import("..").BUN_SQLITE_DatabaseSchemaType>;
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
import init from "../functions/init";
|
||||
export default async function grabDbSchema() {
|
||||
const { dbSchema } = await init();
|
||||
const { dbSchema } = init();
|
||||
return dbSchema;
|
||||
}
|
||||
|
||||
Vendored
+12
-5
@@ -1,18 +1,25 @@
|
||||
import grabDBDir from "../utils/grab-db-dir";
|
||||
import fs from "fs";
|
||||
import _ from "lodash";
|
||||
export default function grabSortedBackups({ config }) {
|
||||
const { backup_dir } = grabDBDir({ config });
|
||||
const backups = fs.readdirSync(backup_dir);
|
||||
if (!fs.existsSync(backup_dir))
|
||||
return [];
|
||||
/**
|
||||
* Only files named by `grabDBBackupFileName` for this database. Anything
|
||||
* else in the directory is never listed (or trimmed).
|
||||
*/
|
||||
const backup_name_regex = new RegExp(`^${_.escapeRegExp(config.db_name)}-\\d+$`);
|
||||
const backups = fs
|
||||
.readdirSync(backup_dir)
|
||||
.filter((b) => backup_name_regex.test(b));
|
||||
/**
|
||||
* 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 b_date - a_date;
|
||||
});
|
||||
return ordered_backups;
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import type { BunSQLiteConfig } from "../types";
|
||||
type Params = {
|
||||
config: BunSQLiteConfig;
|
||||
};
|
||||
export default function grabSortedExports({ config }: Params): string[];
|
||||
export {};
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import fs from "fs";
|
||||
import grabDBDir from "./grab-db-dir";
|
||||
function stripExportExtension(name) {
|
||||
const lower = name.toLowerCase();
|
||||
if (lower.endsWith(".tar.gz"))
|
||||
return name.slice(0, -7);
|
||||
if (lower.endsWith(".tgz"))
|
||||
return name.slice(0, -4);
|
||||
if (lower.endsWith(".tar"))
|
||||
return name.slice(0, -4);
|
||||
if (lower.endsWith(".zip"))
|
||||
return name.slice(0, -4);
|
||||
return name;
|
||||
}
|
||||
function exportTimestamp(name) {
|
||||
const base = stripExportExtension(name);
|
||||
const ts = Number(base.split("-").pop());
|
||||
return Number.isFinite(ts) ? ts : 0;
|
||||
}
|
||||
export default function grabSortedExports({ config }) {
|
||||
const { export_dir } = grabDBDir({ config });
|
||||
if (!fs.existsSync(export_dir)) {
|
||||
return [];
|
||||
}
|
||||
const exports = fs.readdirSync(export_dir);
|
||||
return exports.sort((a, b) => exportTimestamp(b) - exportTimestamp(a));
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
type Params = {
|
||||
src: string;
|
||||
db_file_path: string;
|
||||
};
|
||||
/**
|
||||
* # Replace the database file with a copy of `src`
|
||||
* @description All connections to `db_file_path` must be closed first.
|
||||
* Leftover `-wal`/`-shm`/`-journal` files are removed so SQLite doesn't
|
||||
* replay stale pages onto the new file, and the copy is swapped in with an
|
||||
* atomic rename.
|
||||
*/
|
||||
export default function replaceDbFile({ src, db_file_path }: Params): void;
|
||||
export {};
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
import fs from "fs";
|
||||
/**
|
||||
* # Replace the database file with a copy of `src`
|
||||
* @description All connections to `db_file_path` must be closed first.
|
||||
* Leftover `-wal`/`-shm`/`-journal` files are removed so SQLite doesn't
|
||||
* replay stale pages onto the new file, and the copy is swapped in with an
|
||||
* atomic rename.
|
||||
*/
|
||||
export default function replaceDbFile({ src, db_file_path }) {
|
||||
const tmp_file_path = `${db_file_path}.replace-${Date.now()}`;
|
||||
fs.copyFileSync(src, tmp_file_path);
|
||||
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
||||
fs.rmSync(`${db_file_path}${suffix}`, { force: true });
|
||||
}
|
||||
fs.renameSync(tmp_file_path, db_file_path);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { AppData } from "../data/app-data";
|
||||
export type DataFileFormat = "ts" | "js" | "json" | "yaml";
|
||||
export type ResolvedDataFile = {
|
||||
path: string;
|
||||
basename: string;
|
||||
extension: (typeof AppData.SupportedDataFileExtensions)[number];
|
||||
format: DataFileFormat;
|
||||
};
|
||||
export type LoadedDataFile<T> = ResolvedDataFile & {
|
||||
data: T;
|
||||
};
|
||||
/**
|
||||
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||
* Priority: .ts > .js > .json > .yaml > .yml
|
||||
* Errors if multiple matches exist.
|
||||
*/
|
||||
export declare function resolveDataFile(dir: string, baseName: string): ResolvedDataFile | null;
|
||||
export declare function supportedDataFileNames(baseName: string): string;
|
||||
export declare function isSchemaFileName(name: string): boolean;
|
||||
/**
|
||||
* Load a data file as a plain object.
|
||||
* - .ts / .js: require() and use default export (or module itself)
|
||||
* - .json: JSON.parse
|
||||
* - .yaml / .yml: Bun.YAML.parse
|
||||
*/
|
||||
export declare function loadDataFile<T>(resolved: ResolvedDataFile): T;
|
||||
export declare function resolveAndLoadDataFile<T>(dir: string, baseName: string): LoadedDataFile<T> | null;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { AppData } from "../data/app-data";
|
||||
function extensionToFormat(extension) {
|
||||
switch (extension) {
|
||||
case ".ts":
|
||||
return "ts";
|
||||
case ".js":
|
||||
return "js";
|
||||
case ".json":
|
||||
return "json";
|
||||
case ".yaml":
|
||||
case ".yml":
|
||||
return "yaml";
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||
* Priority: .ts > .js > .json > .yaml > .yml
|
||||
* Errors if multiple matches exist.
|
||||
*/
|
||||
export function resolveDataFile(dir, baseName) {
|
||||
const matches = [];
|
||||
for (const extension of AppData.SupportedDataFileExtensions) {
|
||||
const filePath = path.join(dir, `${baseName}${extension}`);
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
matches.push({
|
||||
path: filePath,
|
||||
basename: `${baseName}${extension}`,
|
||||
extension,
|
||||
format: extensionToFormat(extension),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const found = matches.map((m) => m.basename).join(", ");
|
||||
throw new Error(`Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`);
|
||||
}
|
||||
return matches[0];
|
||||
}
|
||||
export function supportedDataFileNames(baseName) {
|
||||
return AppData.SupportedDataFileExtensions.map((ext) => `\`${baseName}${ext}\``).join(", ");
|
||||
}
|
||||
export function isSchemaFileName(name) {
|
||||
const base = path.basename(name);
|
||||
if (base === AppData.DbSchemaFileName) {
|
||||
return true;
|
||||
}
|
||||
return AppData.SupportedDataFileExtensions.some((ext) => base === `${AppData.DbSchemaFileName}${ext}`);
|
||||
}
|
||||
/**
|
||||
* Load a data file as a plain object.
|
||||
* - .ts / .js: require() and use default export (or module itself)
|
||||
* - .json: JSON.parse
|
||||
* - .yaml / .yml: Bun.YAML.parse
|
||||
*/
|
||||
export function loadDataFile(resolved) {
|
||||
if (resolved.format === "ts" || resolved.format === "js") {
|
||||
const imported = require(resolved.path);
|
||||
const data = imported && typeof imported === "object" && "default" in imported
|
||||
? imported.default
|
||||
: imported;
|
||||
if (data == null) {
|
||||
throw new Error(`No default export from \`${resolved.path}\`. Please export a default module.`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
const text = fs.readFileSync(resolved.path, "utf-8");
|
||||
if (resolved.format === "json") {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
return Bun.YAML.parse(text);
|
||||
}
|
||||
export function resolveAndLoadDataFile(dir, baseName) {
|
||||
const resolved = resolveDataFile(dir, baseName);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...resolved,
|
||||
data: loadDataFile(resolved),
|
||||
};
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import type { Database } from "bun:sqlite";
|
||||
type Params = {
|
||||
db: Database;
|
||||
dst: string;
|
||||
};
|
||||
/**
|
||||
* # Write a consistent copy of the database to `dst`
|
||||
* @description Uses `VACUUM INTO`, which includes changes still in the WAL
|
||||
* file (a plain file copy misses them).
|
||||
*/
|
||||
export default function snapshotDb({ db, dst }: Params): void;
|
||||
export {};
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
/**
|
||||
* # Write a consistent copy of the database to `dst`
|
||||
* @description Uses `VACUUM INTO`, which includes changes still in the WAL
|
||||
* file (a plain file copy misses them).
|
||||
*/
|
||||
export default function snapshotDb({ db, dst }) {
|
||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
||||
fs.rmSync(dst, { force: true });
|
||||
db.run("VACUUM INTO ?", [dst]);
|
||||
}
|
||||
Vendored
+8
-3
@@ -6,15 +6,20 @@ type Params = {
|
||||
queryObj: ServerQueryObject<{
|
||||
[key: string]: any;
|
||||
}, string>;
|
||||
/**
|
||||
* `value` is an already-validated `table.field` reference rather than a
|
||||
* literal, so it is inlined instead of bound
|
||||
*/
|
||||
isValueFieldValue?: boolean;
|
||||
};
|
||||
type Return = {
|
||||
str?: string;
|
||||
param?: SQLInsertGenValueType;
|
||||
str: string;
|
||||
params: SQLInsertGenValueType[];
|
||||
};
|
||||
/**
|
||||
* # SQL Gen Operator Gen
|
||||
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
|
||||
* @description Generates an SQL condition for a single field. Literal values
|
||||
* are always returned in `params` to be bound, never inlined.
|
||||
*/
|
||||
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }: Params): Return;
|
||||
export {};
|
||||
|
||||
Vendored
+63
-103
@@ -1,133 +1,93 @@
|
||||
import sqlEqualityParser from "./sql-equality-parser";
|
||||
import { sqlIdent } from "./sql-safe";
|
||||
/**
|
||||
* # SQL Gen Operator Gen
|
||||
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
|
||||
* @description Generates an SQL condition for a single field. Literal values
|
||||
* are always returned in `params` to be bound, never inlined.
|
||||
*/
|
||||
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }) {
|
||||
if (queryObj.nullValue) {
|
||||
return { str: `${fieldName} IS NULL` };
|
||||
if (queryObj.nullValue || equality == "IS NULL") {
|
||||
return { str: `${fieldName} IS NULL`, params: [] };
|
||||
}
|
||||
if (queryObj.notNullValue) {
|
||||
return { str: `${fieldName} IS NOT NULL` };
|
||||
if (queryObj.notNullValue || equality == "IS NOT NULL") {
|
||||
return { str: `${fieldName} IS NOT NULL`, params: [] };
|
||||
}
|
||||
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,
|
||||
};
|
||||
const finalValue = isValueFieldValue ? String(value) : "?";
|
||||
const params = isValueFieldValue
|
||||
? []
|
||||
: [value ?? ""];
|
||||
const wrappedValue = isValueFieldValue ? `'%' || ${value} || '%'` : "?";
|
||||
const wrappedParams = isValueFieldValue
|
||||
? []
|
||||
: [`%${value ?? ""}%`];
|
||||
switch (equality) {
|
||||
case "MATCH":
|
||||
case "MATCH_BOOLEAN":
|
||||
case "FULLTEXT": {
|
||||
const matchValue = queryObj.vectorFunction && !isValueFieldValue
|
||||
? `${sqlIdent(queryObj.vectorFunction, "vector function")}(?)`
|
||||
: finalValue;
|
||||
return { str: `${fieldName} MATCH ${matchValue}`, params };
|
||||
}
|
||||
else if (equality == "MATCH_BOOLEAN") {
|
||||
case "LIKE_LOWER":
|
||||
return {
|
||||
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
|
||||
param: finalParams,
|
||||
str: `LOWER(${fieldName}) LIKE LOWER(${wrappedValue})`,
|
||||
params: wrappedParams,
|
||||
};
|
||||
}
|
||||
else if (equality == "LIKE_LOWER") {
|
||||
case "LIKE_LOWER_RAW":
|
||||
return {
|
||||
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
|
||||
param: `%${finalParams}%`,
|
||||
params,
|
||||
};
|
||||
}
|
||||
else if (equality == "LIKE_LOWER_RAW") {
|
||||
case "LIKE":
|
||||
return {
|
||||
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
|
||||
param: finalParams,
|
||||
str: `${fieldName} LIKE ${wrappedValue}`,
|
||||
params: wrappedParams,
|
||||
};
|
||||
}
|
||||
else if (equality == "LIKE") {
|
||||
case "LIKE_RAW":
|
||||
return { str: `${fieldName} LIKE ${finalValue}`, params };
|
||||
case "NOT_LIKE_LOWER":
|
||||
return {
|
||||
str: `${fieldName} LIKE ${finalValue}`,
|
||||
param: `%${finalParams}%`,
|
||||
str: `LOWER(${fieldName}) NOT LIKE LOWER(${wrappedValue})`,
|
||||
params: wrappedParams,
|
||||
};
|
||||
}
|
||||
else if (equality == "LIKE_RAW") {
|
||||
return {
|
||||
str: `${fieldName} LIKE ${finalValue}`,
|
||||
param: finalParams,
|
||||
};
|
||||
}
|
||||
else if (equality == "NOT_LIKE_LOWER") {
|
||||
case "NOT_LIKE_LOWER_RAW":
|
||||
return {
|
||||
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
|
||||
param: `%${finalParams}%`,
|
||||
params,
|
||||
};
|
||||
}
|
||||
else if (equality == "NOT_LIKE_LOWER_RAW") {
|
||||
case "NOT LIKE":
|
||||
return {
|
||||
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
|
||||
param: finalParams,
|
||||
str: `${fieldName} NOT LIKE ${wrappedValue}`,
|
||||
params: wrappedParams,
|
||||
};
|
||||
}
|
||||
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") {
|
||||
case "NOT LIKE_RAW":
|
||||
return { str: `${fieldName} NOT LIKE ${finalValue}`, params };
|
||||
case "REGEXP":
|
||||
/**
|
||||
* SQLite has no built-in REGEXP function. This only works when
|
||||
* an extension providing `regexp()` is loaded.
|
||||
*/
|
||||
return {
|
||||
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
|
||||
param: finalParams,
|
||||
params,
|
||||
};
|
||||
}
|
||||
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) {
|
||||
case "NOT EQUAL":
|
||||
return { str: `${fieldName} != ${finalValue}`, params };
|
||||
case "IS NOT":
|
||||
return { str: `${fieldName} IS NOT ${finalValue}`, params };
|
||||
case "IN":
|
||||
case "NOT IN":
|
||||
return { str: `${fieldName} ${equality} (${finalValue})`, params };
|
||||
case "BETWEEN":
|
||||
case "NOT BETWEEN":
|
||||
throw new Error(`\`${equality}\` on \`${fieldName}\` requires an array of two values`);
|
||||
case undefined:
|
||||
return { str: `${fieldName} = ${finalValue}`, params };
|
||||
default:
|
||||
return {
|
||||
str: `${fieldName} ${sqlEqualityParser(equality)} ${finalValue}`,
|
||||
param: finalParams,
|
||||
params,
|
||||
};
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-23
@@ -1,3 +1,4 @@
|
||||
import { sqlIdent, sqlOperator, sqlRawEquality } from "./sql-safe";
|
||||
export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
||||
let values = [];
|
||||
if (mtch.__batch) {
|
||||
@@ -13,7 +14,7 @@ export default function sqlGenGenJoinStr({ join, 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 += ` ${sqlOperator(mtch.__batch.operator, "OR")} `;
|
||||
}
|
||||
}
|
||||
btch_mtch += `)`;
|
||||
@@ -22,28 +23,8 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
||||
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}`;
|
||||
})()}`;
|
||||
const equality = mtch.raw_equality ? sqlRawEquality(mtch.raw_equality) : "=";
|
||||
const lhs = `${sqlIdent(typeof mtch.source == "object" ? mtch.source.tableName : table_name, "table name")}.${sqlIdent(typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source, "field name")}`;
|
||||
if (mtch.between) {
|
||||
values.push(mtch.between.min, mtch.between.max);
|
||||
return {
|
||||
@@ -58,6 +39,19 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
||||
values,
|
||||
};
|
||||
}
|
||||
const rhs = (() => {
|
||||
if (mtch.targetLiteral !== undefined && mtch.targetLiteral !== null) {
|
||||
values.push(mtch.targetLiteral);
|
||||
return `?`;
|
||||
}
|
||||
const targetTable = typeof mtch.target == "object"
|
||||
? mtch.target.tableName
|
||||
: join.alias || join.tableName;
|
||||
const targetField = typeof mtch.target == "object"
|
||||
? mtch.target.fieldName
|
||||
: mtch.target;
|
||||
return `${sqlIdent(targetTable, "table name")}.${sqlIdent(targetField, "field name")}`;
|
||||
})();
|
||||
return {
|
||||
str: `${lhs} ${equality} ${rhs}`,
|
||||
values,
|
||||
|
||||
-2
@@ -8,8 +8,6 @@ type Param<T extends {
|
||||
selectFields?: (keyof T | TableSelectFieldsObject<T>)[];
|
||||
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;
|
||||
|
||||
+73
-81
@@ -2,12 +2,11 @@ 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";
|
||||
import { sqlIdent, sqlJoinType, sqlOperator } from "./sql-safe";
|
||||
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}`;
|
||||
@@ -41,108 +40,102 @@ export default function sqlGenGenQueryStr(params) {
|
||||
const countSubQuery = genObject.countSubQueries[i];
|
||||
if (!countSubQuery)
|
||||
continue;
|
||||
const tableAlias = countSubQuery.table_alias;
|
||||
const countTable = sqlIdent(countSubQuery.table, "table name");
|
||||
const tableAlias = countSubQuery.table_alias
|
||||
? sqlIdent(countSubQuery.table_alias, "alias")
|
||||
: undefined;
|
||||
let subQStr = `(SELECT COUNT(*)`;
|
||||
subQStr += ` FROM ${countSubQuery.table}${tableAlias ? ` ${tableAlias}` : ""}`;
|
||||
subQStr += ` FROM ${countTable}${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}`;
|
||||
subQStr += ` ${tableAlias || countTable}.${sqlIdent(csqSrc.src, "field name")}`;
|
||||
if (typeof csqSrc.trg == "string") {
|
||||
subQStr += ` = ?`;
|
||||
sqlSearhValues.push(csqSrc.trg);
|
||||
}
|
||||
else if (typeof csqSrc.trg == "object") {
|
||||
subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`;
|
||||
subQStr += ` = ${sqlIdent(csqSrc.trg.table, "table name")}.${sqlIdent(csqSrc.trg.field, "field name")}`;
|
||||
}
|
||||
if (j < countSubQuery.srcTrgMap.length - 1) {
|
||||
subQStr += ` AND `;
|
||||
}
|
||||
}
|
||||
subQStr += ` )) AS ${countSubQuery.alias}`;
|
||||
subQStr += ` )) AS ${sqlIdent(countSubQuery.alias, "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) {
|
||||
const joinSelectStr = genObject.join
|
||||
.flat()
|
||||
.filter((j) => !isUndefined(j))
|
||||
.map((joinObj) => {
|
||||
const joinTableName = sqlIdent(joinObj.alias ? joinObj.alias : joinObj.tableName, "join table name");
|
||||
if (existingJoinTableNames.includes(joinTableName))
|
||||
return null;
|
||||
existingJoinTableNames.push(joinTableName);
|
||||
if (joinObj.group_concat) {
|
||||
return sqlGenGrabConcatStr({
|
||||
field: `${joinTableName}.${sqlIdent(joinObj.group_concat.field, "field name")}`,
|
||||
alias: joinObj.group_concat.alias,
|
||||
separator: joinObj.group_concat.separator,
|
||||
distinct: joinObj.group_concat.distinct,
|
||||
});
|
||||
}
|
||||
else if (joinObj.selectFields) {
|
||||
return joinObj.selectFields
|
||||
.map((selectField) => {
|
||||
if (typeof selectField != "object") {
|
||||
return `${joinTableName}.${sqlIdent(selectField, "field name")}`;
|
||||
}
|
||||
const field = `${joinTableName}.${sqlIdent(selectField.field, "field name")}`;
|
||||
if (selectField.group_concat &&
|
||||
selectField.alias) {
|
||||
return sqlGenGrabConcatStr({
|
||||
field: `${joinTableName}.${joinObj.group_concat.field}`,
|
||||
alias: joinObj.group_concat.alias,
|
||||
separator: joinObj.group_concat.separator,
|
||||
field,
|
||||
alias: selectField.alias,
|
||||
separator: selectField.group_concat.separator,
|
||||
distinct: selectField.group_concat.distinct,
|
||||
});
|
||||
}
|
||||
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(",");
|
||||
let aliasSelectField = field;
|
||||
if (selectField.count) {
|
||||
aliasSelectField = `COUNT(${field})`;
|
||||
}
|
||||
else {
|
||||
return `${joinTableName}.*`;
|
||||
else if (selectField.sum) {
|
||||
aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${field})`;
|
||||
}
|
||||
else if (selectField.average) {
|
||||
aliasSelectField = `AVG(${field})`;
|
||||
}
|
||||
else if (selectField.max) {
|
||||
aliasSelectField = `MAX(${field})`;
|
||||
}
|
||||
else if (selectField.min) {
|
||||
aliasSelectField = `MIN(${field})`;
|
||||
}
|
||||
else if (selectField.distinct) {
|
||||
aliasSelectField = `DISTINCT ${field}`;
|
||||
}
|
||||
if (selectField.alias)
|
||||
aliasSelectField += ` AS ${sqlIdent(selectField.alias, "alias")}`;
|
||||
return aliasSelectField;
|
||||
})
|
||||
.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);
|
||||
}
|
||||
else {
|
||||
return `${joinTableName}.*`;
|
||||
}
|
||||
})
|
||||
.filter((_) => Boolean(_))
|
||||
.join(",");
|
||||
if (joinSelectStr) {
|
||||
str += "," + joinSelectStr;
|
||||
}
|
||||
}
|
||||
str += ` FROM ${table_name}`;
|
||||
if (genObject?.join) {
|
||||
@@ -152,11 +145,12 @@ export default function sqlGenGenQueryStr(params) {
|
||||
.flat()
|
||||
.filter((j) => !isUndefined(j))
|
||||
.map((join) => {
|
||||
return (join.joinType +
|
||||
const joinTable = sqlIdent(join.tableName, "table name");
|
||||
return (sqlJoinType(join.joinType) +
|
||||
" " +
|
||||
(join.alias
|
||||
? `${join.tableName}` + " " + join.alias
|
||||
: `${join.tableName}`) +
|
||||
? `${joinTable} ${sqlIdent(join.alias, "alias")}`
|
||||
: joinTable) +
|
||||
" ON " +
|
||||
(() => {
|
||||
if (Array.isArray(join.match)) {
|
||||
@@ -171,9 +165,7 @@ export default function sqlGenGenQueryStr(params) {
|
||||
sqlSearhValues.push(...values);
|
||||
return str;
|
||||
})
|
||||
.join(join.operator
|
||||
? ` ${join.operator} `
|
||||
: " AND ") +
|
||||
.join(` ${sqlOperator(join.operator)} `) +
|
||||
")");
|
||||
}
|
||||
else if (typeof join.match == "object") {
|
||||
|
||||
+84
-64
@@ -1,92 +1,112 @@
|
||||
import sqlGenOperatorGen from "./sql-gen-operator-gen";
|
||||
import { sqlField, sqlIdent, sqlOperator, sqlRawEquality } from "./sql-safe";
|
||||
function isPresent(val) {
|
||||
return val !== undefined && val !== null;
|
||||
}
|
||||
function isValuesObject(val) {
|
||||
return typeof val == "object" && val !== null && !ArrayBuffer.isView(val);
|
||||
}
|
||||
export default function sqlGenGenSearchStr({ queryObj, join, field, table_name, }) {
|
||||
let sqlSearhValues = [];
|
||||
const safeField = sqlField(field);
|
||||
const finalFieldName = (() => {
|
||||
if (queryObj?.tableName) {
|
||||
return `${queryObj.tableName}.${field}`;
|
||||
return `${sqlIdent(queryObj.tableName, "table name")}.${safeField}`;
|
||||
}
|
||||
if (join) {
|
||||
return `${table_name}.${field}`;
|
||||
return `${table_name}.${safeField}`;
|
||||
}
|
||||
return field;
|
||||
return safeField;
|
||||
})();
|
||||
let str = `${finalFieldName}=?`;
|
||||
function grabValue(val) {
|
||||
const valueParsed = val;
|
||||
if (!valueParsed)
|
||||
if (!isPresent(val))
|
||||
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({
|
||||
if (isValuesObject(val)) {
|
||||
const isFieldValue = Boolean(val.fieldName && val.tableName);
|
||||
return sqlGenOperatorGen({
|
||||
queryObj,
|
||||
equality: val.equality || queryObj.equality,
|
||||
fieldName: finalFieldName,
|
||||
value: isFieldValue
|
||||
? `${sqlIdent(val.tableName, "table name")}.${sqlIdent(val.fieldName, "field name")}`
|
||||
: val.value,
|
||||
isValueFieldValue: isFieldValue,
|
||||
});
|
||||
}
|
||||
return sqlGenOperatorGen({
|
||||
queryObj,
|
||||
equality: valueEquality,
|
||||
fieldName: finalFieldName || "",
|
||||
value: valueString || "",
|
||||
isValueFieldValue: Boolean(typeof valueParsed == "object" &&
|
||||
valueParsed.fieldName &&
|
||||
valueParsed.tableName),
|
||||
equality: queryObj.equality,
|
||||
fieldName: finalFieldName,
|
||||
value: val,
|
||||
});
|
||||
return operatorStrParam;
|
||||
}
|
||||
let str;
|
||||
if (Array.isArray(queryObj.value)) {
|
||||
const strArray = [];
|
||||
queryObj.value.forEach((val) => {
|
||||
const operatorStrParam = grabValue(val);
|
||||
if (!operatorStrParam)
|
||||
return;
|
||||
if (operatorStrParam.str && operatorStrParam.param) {
|
||||
const items = queryObj.value.filter(isPresent);
|
||||
const rawValues = items
|
||||
.map((v) => (isValuesObject(v) ? v.value : v))
|
||||
.filter(isPresent);
|
||||
const equality = queryObj.equality;
|
||||
if ((equality == "IN" || equality == "NOT IN") && !rawValues.length) {
|
||||
// Empty IN () is invalid SQL: IN matches nothing, NOT IN matches everything
|
||||
str = equality == "IN" ? "1 = 0" : "1 = 1";
|
||||
}
|
||||
else if (equality == "IN" || equality == "NOT IN") {
|
||||
str = `${finalFieldName} ${equality} (${rawValues
|
||||
.map(() => "?")
|
||||
.join(", ")})`;
|
||||
sqlSearhValues.push(...rawValues);
|
||||
}
|
||||
else if (equality == "BETWEEN" || equality == "NOT BETWEEN") {
|
||||
if (rawValues.length !== 2) {
|
||||
throw new Error(`\`${equality}\` on \`${finalFieldName}\` requires exactly two values`);
|
||||
}
|
||||
str = `${finalFieldName} ${equality} ? AND ?`;
|
||||
sqlSearhValues.push(...rawValues);
|
||||
}
|
||||
else {
|
||||
if (!items.length) {
|
||||
throw new Error(`Empty value array for \`${finalFieldName}\``);
|
||||
}
|
||||
const strArray = [];
|
||||
for (const val of items) {
|
||||
const operatorStrParam = grabValue(val);
|
||||
if (!operatorStrParam)
|
||||
continue;
|
||||
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);
|
||||
sqlSearhValues.push(...operatorStrParam.params);
|
||||
}
|
||||
str =
|
||||
"(" +
|
||||
strArray.join(` ${sqlOperator(queryObj.operator)} `) +
|
||||
")";
|
||||
}
|
||||
}
|
||||
else if (queryObj.raw_equality && queryObj.value) {
|
||||
str = `${finalFieldName} ${queryObj.raw_equality} ?`;
|
||||
sqlSearhValues.push(queryObj.value);
|
||||
else if (queryObj.raw_equality && isPresent(queryObj.value)) {
|
||||
const value = isValuesObject(queryObj.value)
|
||||
? queryObj.value.value
|
||||
: queryObj.value;
|
||||
str = `${finalFieldName} ${sqlRawEquality(queryObj.raw_equality)} ?`;
|
||||
sqlSearhValues.push(value ?? null);
|
||||
}
|
||||
else if (queryObj.between) {
|
||||
str = `${finalFieldName} BETWEEN ? AND ?`;
|
||||
sqlSearhValues.push(queryObj.between.min, queryObj.between.max);
|
||||
}
|
||||
else if (queryObj.not_between) {
|
||||
str = `${finalFieldName} NOT BETWEEN ? AND ?`;
|
||||
sqlSearhValues.push(queryObj.not_between.min, queryObj.not_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 || "");
|
||||
}
|
||||
const operatorStrParam = grabValue(queryObj.value) ||
|
||||
sqlGenOperatorGen({
|
||||
equality: queryObj.equality,
|
||||
fieldName: finalFieldName,
|
||||
value: undefined,
|
||||
queryObj,
|
||||
});
|
||||
str = operatorStrParam.str;
|
||||
sqlSearhValues.push(...operatorStrParam.params);
|
||||
}
|
||||
return { str, values: sqlSearhValues };
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,13 +1,14 @@
|
||||
import { sqlField, sqlIdent, sqlStringLiteral } from "./sql-safe";
|
||||
export default function sqlGenGrabConcatStr({ alias, field, separator = ",", distinct, }) {
|
||||
let gc = `GROUP_CONCAT(`;
|
||||
if (distinct) {
|
||||
gc += `DISTINCT `;
|
||||
}
|
||||
gc += `${field}`;
|
||||
gc += sqlField(field);
|
||||
if (!distinct) {
|
||||
gc += `, '${separator}'`;
|
||||
gc += `, ${sqlStringLiteral(String(separator))}`;
|
||||
}
|
||||
gc += `)`;
|
||||
gc += ` AS ${alias}`;
|
||||
gc += ` AS ${sqlIdent(alias, "alias")}`;
|
||||
return gc;
|
||||
}
|
||||
|
||||
+39
-39
@@ -1,53 +1,53 @@
|
||||
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
||||
import { sqlField, sqlIdent } from "./sql-safe";
|
||||
export default function sqlGenGrabSelectFieldSQL({ selectFields, append_table_names, table_name }) {
|
||||
let str = "";
|
||||
str += ` ${selectFields
|
||||
?.map((fld) => {
|
||||
if (typeof fld != "object") {
|
||||
return sqlField(fld);
|
||||
}
|
||||
const fld_name = sqlField(fld.fieldName);
|
||||
const final_fld_name = append_table_names && !fld_name.includes(".")
|
||||
? `${table_name}.${fld_name}`
|
||||
: fld_name;
|
||||
if (fld.group_concat) {
|
||||
return sqlGenGrabConcatStr({
|
||||
field: final_fld_name,
|
||||
alias: fld.group_concat.alias || fld.alias || fld_name,
|
||||
separator: fld.group_concat.separator,
|
||||
distinct: fld.group_concat.distinct,
|
||||
});
|
||||
}
|
||||
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}`;
|
||||
let alias = fld.alias;
|
||||
if (fld.count) {
|
||||
fld_str += `COUNT(${final_fld_name})`;
|
||||
if (!alias && typeof fld.count == "object") {
|
||||
alias = fld.count.alias;
|
||||
}
|
||||
}
|
||||
else if (fld.sum) {
|
||||
fld_str += `SUM(${final_fld_name})`;
|
||||
}
|
||||
else if (fld.average) {
|
||||
fld_str += `AVG(${final_fld_name})`;
|
||||
}
|
||||
else if (fld.max) {
|
||||
fld_str += `MAX(${final_fld_name})`;
|
||||
}
|
||||
else if (fld.min) {
|
||||
fld_str += `MIN(${final_fld_name})`;
|
||||
}
|
||||
else if (fld.distinct) {
|
||||
fld_str += `DISTINCT ${final_fld_name}`;
|
||||
}
|
||||
else {
|
||||
fld_str += final_fld_name;
|
||||
}
|
||||
if (alias) {
|
||||
fld_str += ` AS ${sqlIdent(alias, "alias")}`;
|
||||
}
|
||||
return fld_str;
|
||||
})
|
||||
.join(",")}`;
|
||||
|
||||
Vendored
+16
-2
@@ -8,18 +8,32 @@ type Param<T extends {
|
||||
tableName: string;
|
||||
dbFullName?: string;
|
||||
count?: boolean;
|
||||
/**
|
||||
* Restrict to a single row id. Always combined with AND, regardless of
|
||||
* `searchOperator`.
|
||||
*/
|
||||
targetId?: number | string;
|
||||
};
|
||||
type Return = {
|
||||
string: string;
|
||||
values: SQLInsertGenValueType[];
|
||||
/**
|
||||
* Just the `WHERE ...` clause (empty if there are no conditions) and its
|
||||
* bound values. Used by UPDATE and DELETE.
|
||||
*/
|
||||
where: {
|
||||
string: string;
|
||||
values: SQLInsertGenValueType[];
|
||||
};
|
||||
};
|
||||
/**
|
||||
* # SQL Query Generator
|
||||
* @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
|
||||
* @description Generates a parameterized SQLite SELECT query. All values are
|
||||
* returned in `values` to be bound; identifiers are validated.
|
||||
*/
|
||||
export default function sqlGenerator<T extends {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
[key: string]: any;
|
||||
}>({ tableName, genObject, dbFullName, count }: Param<T>): Return;
|
||||
}>({ tableName: passedTableName, genObject, count, targetId, }: Param<T>): Return;
|
||||
export {};
|
||||
|
||||
Vendored
+86
-88
@@ -1,29 +1,23 @@
|
||||
import sqlGenGenSearchStr from "./sql-generator-gen-search-str";
|
||||
import sqlGenGenQueryStr from "./sql-generator-gen-query-str";
|
||||
import { sqlField, sqlIdent, sqlInt, sqlOperator, sqlOrderStrategy, } from "./sql-safe";
|
||||
/**
|
||||
* # SQL Query Generator
|
||||
* @description Generates an SQL Query for node module `mysql` or `serverless-mysql`
|
||||
* @description Generates a parameterized SQLite SELECT query. All values are
|
||||
* returned in `values` to be bound; identifiers are validated.
|
||||
*/
|
||||
export default function sqlGenerator({ tableName, genObject, dbFullName, count }) {
|
||||
export default function sqlGenerator({ tableName: passedTableName, genObject, count, targetId, }) {
|
||||
const tableName = sqlIdent(passedTableName, "table name");
|
||||
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;
|
||||
const whereValues = [];
|
||||
if (genObject?.fullTextSearch) {
|
||||
throw new Error("`fullTextSearch` is not supported by SQLite. Use an FTS5 table and the `MATCH` equality instead.");
|
||||
}
|
||||
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);
|
||||
@@ -34,7 +28,8 @@ export default function sqlGenerator({ tableName, genObject, dbFullName, count }
|
||||
if (queryObj.__query) {
|
||||
const subQueryGroup = queryObj.__query;
|
||||
const subSearchKeys = Object.keys(subQueryGroup);
|
||||
const subSearchString = subSearchKeys.map((_field) => {
|
||||
const subSearchString = subSearchKeys
|
||||
.map((_field) => {
|
||||
const newSubQueryObj = subQueryGroup?.[_field];
|
||||
if (newSubQueryObj) {
|
||||
const { str, values } = sqlGenGenSearchStr({
|
||||
@@ -43,12 +38,15 @@ export default function sqlGenerator({ tableName, genObject, dbFullName, count }
|
||||
join: genObject?.join,
|
||||
table_name: tableName,
|
||||
});
|
||||
sqlSearhValues.push(...values);
|
||||
whereValues.push(...values);
|
||||
return str;
|
||||
}
|
||||
});
|
||||
})
|
||||
.filter((str) => typeof str == "string");
|
||||
if (!subSearchString.length)
|
||||
return;
|
||||
return ("(" +
|
||||
subSearchString.join(` ${queryObj.operator || "AND"} `) +
|
||||
subSearchString.join(` ${sqlOperator(queryObj.operator)} `) +
|
||||
")");
|
||||
}
|
||||
const { str, values } = sqlGenGenSearchStr({
|
||||
@@ -57,96 +55,96 @@ export default function sqlGenerator({ tableName, genObject, dbFullName, count }
|
||||
join: genObject?.join,
|
||||
table_name: tableName,
|
||||
});
|
||||
sqlSearhValues.push(...values);
|
||||
whereValues.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} `)}`;
|
||||
const whereConditions = [];
|
||||
if (cleanedUpSearchStr?.[0]) {
|
||||
const stringOperator = sqlOperator(genObject?.searchOperator);
|
||||
whereConditions.push(`(${cleanedUpSearchStr.join(` ${stringOperator} `)})`);
|
||||
}
|
||||
if (genObject?.fullTextSearch && fullTextSearchStr && fullTextMatchStr) {
|
||||
queryString += `${isSearchStr ? " AND" : " WHERE"} ${fullTextMatchStr}`;
|
||||
sqlSearhValues.push(fullTextSearchStr);
|
||||
if (targetId !== undefined && targetId !== null && targetId !== "") {
|
||||
const id = Number(targetId);
|
||||
if (!Number.isSafeInteger(id)) {
|
||||
throw new Error(`Invalid targetId: ${JSON.stringify(targetId)}`);
|
||||
}
|
||||
whereConditions.push(`${genObject?.join ? `${tableName}.` : ""}id = ?`);
|
||||
whereValues.push(id);
|
||||
}
|
||||
const whereString = whereConditions.length
|
||||
? `WHERE ${whereConditions.join(" AND ")}`
|
||||
: "";
|
||||
if (whereString) {
|
||||
queryString += ` ${whereString}`;
|
||||
sqlSearhValues.push(...whereValues);
|
||||
}
|
||||
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 += ",";
|
||||
}
|
||||
const grabGroupStr = (group) => {
|
||||
if (typeof group == "object") {
|
||||
return group.table
|
||||
? `${sqlIdent(group.table, "table name")}.${sqlIdent(group.field, "field name")}`
|
||||
: sqlField(group.field);
|
||||
}
|
||||
}
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
return sqlField(group);
|
||||
};
|
||||
const group_by_txt = Array.isArray(genObject.group)
|
||||
? genObject.group.map(grabGroupStr).join(",")
|
||||
: grabGroupStr(genObject.group);
|
||||
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;
|
||||
const field = genObject?.join && !String(order.field).includes(".")
|
||||
? `${tableName}.${sqlIdent(order.field, "order field")}`
|
||||
: sqlField(order.field, "order field");
|
||||
return ` ${field} ${sqlOrderStrategy(order.strategy)}`;
|
||||
}
|
||||
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);
|
||||
}
|
||||
const orders = (Array.isArray(genObject.order) ? genObject.order : [genObject.order]).filter(Boolean);
|
||||
orderSrt += orders.map(grabOrderString).join(",");
|
||||
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}`;
|
||||
/**
|
||||
* Pagination is skipped for count queries so the count covers every
|
||||
* matching row
|
||||
*/
|
||||
if (!count) {
|
||||
const { limit: rawLimit, offset: rawOffset, page } = genObject || {};
|
||||
/**
|
||||
* `limit: 0` has always meant "no limit"
|
||||
*/
|
||||
const limit = rawLimit ? sqlInt(rawLimit, "limit") : undefined;
|
||||
const offset = isSet(rawOffset)
|
||||
? sqlInt(rawOffset, "offset")
|
||||
: isSet(page) && limit !== undefined
|
||||
? Math.max(sqlInt(page, "page") - 1, 0) * limit
|
||||
: undefined;
|
||||
if (limit !== undefined) {
|
||||
queryString += ` LIMIT ${limit}`;
|
||||
}
|
||||
else if (offset) {
|
||||
/**
|
||||
* SQLite doesn't allow OFFSET without LIMIT
|
||||
*/
|
||||
queryString += ` LIMIT -1`;
|
||||
}
|
||||
if (offset) {
|
||||
queryString += ` OFFSET ${offset}`;
|
||||
}
|
||||
}
|
||||
return {
|
||||
string: queryString,
|
||||
values: sqlSearhValues,
|
||||
where: {
|
||||
string: whereString,
|
||||
values: whereString ? whereValues : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
function isSet(val) {
|
||||
return val !== undefined && val !== null;
|
||||
}
|
||||
// let queryString = (() => {
|
||||
// let str = "SELECT";
|
||||
// if (genObject?.select_sql) {
|
||||
|
||||
Vendored
+7
-1
@@ -1,5 +1,11 @@
|
||||
import type { SQLInsertGenParams, SQLInsertGenReturn } from "../types";
|
||||
import type { SQLInsertGenParams, SQLInsertGenReturn, SQLInsertGenValueType } from "../types";
|
||||
/**
|
||||
* # SQL Insert Generator
|
||||
*/
|
||||
export default function sqlInsertGenerator({ tableName, data, dbFullName, }: SQLInsertGenParams): SQLInsertGenReturn | undefined;
|
||||
/**
|
||||
* # Normalize a JS value for binding
|
||||
* @description Keeps `0`, `""` and `false` (as `0`) instead of turning them
|
||||
* into `NULL`
|
||||
*/
|
||||
export declare function sqlValue(value: unknown): SQLInsertGenValueType;
|
||||
|
||||
Vendored
+47
-51
@@ -1,60 +1,56 @@
|
||||
import { sqlIdent } from "./sql-safe";
|
||||
/**
|
||||
* # 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);
|
||||
}
|
||||
});
|
||||
const finalDbName = dbFullName
|
||||
? `${sqlIdent(dbFullName, "database name")}.`
|
||||
: "";
|
||||
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(sqlIdent(ky, "column name"));
|
||||
}
|
||||
});
|
||||
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 &&
|
||||
typeof finalValue !== "number" &&
|
||||
typeof finalValue !== "string") {
|
||||
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;
|
||||
}
|
||||
});
|
||||
let queryBatches = [];
|
||||
let queryValues = [];
|
||||
data.forEach((item) => {
|
||||
queryBatches.push(`(${insertKeys
|
||||
.map((ky) => {
|
||||
const value = item[ky];
|
||||
if (typeof value == "function") {
|
||||
const fnValue = value();
|
||||
queryValues.push(fnValue.value ?? null);
|
||||
return fnValue.placeholder;
|
||||
}
|
||||
queryValues.push(sqlValue(value));
|
||||
return "?";
|
||||
})
|
||||
.join(",")})`);
|
||||
});
|
||||
let query = `INSERT INTO ${finalDbName}${sqlIdent(tableName, "table name")} (${insertKeys.join(",")}) VALUES ${queryBatches.join(",")}`;
|
||||
return {
|
||||
query: query,
|
||||
values: queryValues,
|
||||
};
|
||||
}
|
||||
catch ( /** @type {any} */error) {
|
||||
console.log(`SQL insert gen ERROR: ${error.message}`);
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* # Normalize a JS value for binding
|
||||
* @description Keeps `0`, `""` and `false` (as `0`) instead of turning them
|
||||
* into `NULL`
|
||||
*/
|
||||
export function sqlValue(value) {
|
||||
if (value === undefined || value === null)
|
||||
return null;
|
||||
if (typeof value == "boolean")
|
||||
return value ? 1 : 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import { SQlComparisons, ServerQueryOperators } from "../types";
|
||||
declare const JOIN_TYPES: readonly ["INNER JOIN", "JOIN", "LEFT JOIN", "RIGHT JOIN"];
|
||||
/**
|
||||
* # Validate a bare SQL identifier (table, column, alias)
|
||||
* @description Identifiers can't be bound as parameters, so anything that
|
||||
* gets interpolated into SQL must pass this check first.
|
||||
*/
|
||||
export declare function sqlIdent(name: unknown, label?: string): string;
|
||||
/**
|
||||
* # Validate a column reference
|
||||
* @description Accepts `field`, `table.field` or `table.*`
|
||||
*/
|
||||
export declare function sqlField(name: unknown, label?: string): string;
|
||||
/**
|
||||
* # Quote an identifier for DDL statements
|
||||
*/
|
||||
export declare function sqlQuoteIdent(name: string): string;
|
||||
/**
|
||||
* # Quote a string literal
|
||||
*/
|
||||
export declare function sqlStringLiteral(value: string): string;
|
||||
/**
|
||||
* # Validate a non-negative integer (LIMIT, OFFSET, page)
|
||||
*/
|
||||
export declare function sqlInt(value: unknown, label?: string): number;
|
||||
export declare function sqlOrderStrategy(strategy: unknown): "ASC" | "DESC";
|
||||
export declare function sqlOperator(operator: unknown, fallback?: (typeof ServerQueryOperators)[number]): (typeof ServerQueryOperators)[number];
|
||||
export declare function sqlRawEquality(equality: unknown): (typeof SQlComparisons)[number];
|
||||
export declare function sqlJoinType(joinType: unknown): (typeof JOIN_TYPES)[number];
|
||||
export {};
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
import { SQlComparisons, ServerQueryOperators } from "../types";
|
||||
const IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
const FIELD_REGEX = /^([A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*|\*)$/;
|
||||
const JOIN_TYPES = ["INNER JOIN", "JOIN", "LEFT JOIN", "RIGHT JOIN"];
|
||||
/**
|
||||
* # Validate a bare SQL identifier (table, column, alias)
|
||||
* @description Identifiers can't be bound as parameters, so anything that
|
||||
* gets interpolated into SQL must pass this check first.
|
||||
*/
|
||||
export function sqlIdent(name, label = "identifier") {
|
||||
const str = String(name);
|
||||
if (typeof name == "symbol" || !IDENTIFIER_REGEX.test(str)) {
|
||||
throw new Error(`Invalid SQL ${label}: ${JSON.stringify(str)}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
* # Validate a column reference
|
||||
* @description Accepts `field`, `table.field` or `table.*`
|
||||
*/
|
||||
export function sqlField(name, label = "field name") {
|
||||
const str = String(name);
|
||||
if (typeof name == "symbol" || !FIELD_REGEX.test(str)) {
|
||||
throw new Error(`Invalid SQL ${label}: ${JSON.stringify(str)}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
* # Quote an identifier for DDL statements
|
||||
*/
|
||||
export function sqlQuoteIdent(name) {
|
||||
return `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
/**
|
||||
* # Quote a string literal
|
||||
*/
|
||||
export function sqlStringLiteral(value) {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
/**
|
||||
* # Validate a non-negative integer (LIMIT, OFFSET, page)
|
||||
*/
|
||||
export function sqlInt(value, label = "integer") {
|
||||
const num = Number(value);
|
||||
if (!Number.isSafeInteger(num) || num < 0) {
|
||||
throw new Error(`Invalid SQL ${label}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
export function sqlOrderStrategy(strategy) {
|
||||
if (strategy === undefined || strategy === null)
|
||||
return "ASC";
|
||||
const str = String(strategy).toUpperCase();
|
||||
if (str !== "ASC" && str !== "DESC") {
|
||||
throw new Error(`Invalid SQL order strategy: ${JSON.stringify(strategy)}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
export function sqlOperator(operator, fallback = "AND") {
|
||||
if (operator === undefined || operator === null)
|
||||
return fallback;
|
||||
const str = String(operator).toUpperCase();
|
||||
if (!ServerQueryOperators.includes(str)) {
|
||||
throw new Error(`Invalid SQL operator: ${JSON.stringify(operator)}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
export function sqlRawEquality(equality) {
|
||||
if (!SQlComparisons.includes(equality)) {
|
||||
throw new Error(`Invalid SQL comparison: ${JSON.stringify(equality)}`);
|
||||
}
|
||||
return equality;
|
||||
}
|
||||
export function sqlJoinType(joinType) {
|
||||
if (!JOIN_TYPES.includes(joinType)) {
|
||||
throw new Error(`Invalid SQL join type: ${JSON.stringify(joinType)}`);
|
||||
}
|
||||
return joinType;
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import type { BunSQLiteConfig } from "../types";
|
||||
type Params = {
|
||||
config: BunSQLiteConfig;
|
||||
};
|
||||
export default function trimExports({ config }: Params): void;
|
||||
export {};
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import _ from "lodash";
|
||||
import { AppData } from "../data/app-data";
|
||||
import grabDBDir from "./grab-db-dir";
|
||||
import grabSortedExports from "./grab-sorted-exports";
|
||||
export default function trimExports({ config }) {
|
||||
const { export_dir } = grabDBDir({ config });
|
||||
// Only consider archives written by `export` (`{db_name}-{timestamp}.tar.gz|zip`)
|
||||
const exportNamePattern = new RegExp(`^${_.escapeRegExp(config.db_name)}-\\d+\\.(tar\\.gz|zip)$`);
|
||||
const exports = grabSortedExports({ config }).filter((name) => exportNamePattern.test(name) &&
|
||||
fs.statSync(path.join(export_dir, name)).isFile());
|
||||
const max_exports = config.max_exports || AppData["MaxExports"];
|
||||
for (let i = 0; i < exports.length; i++) {
|
||||
const export_name = exports[i];
|
||||
if (!export_name)
|
||||
continue;
|
||||
if (i > max_exports - 1) {
|
||||
fs.unlinkSync(path.join(export_dir, export_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user