Security: - select: bind parameters instead of mysql.format (backslash escaping allowed injection on SQLite); drop the mysql dependency - validate all interpolated identifiers, limit/offset/page, order strategy, operators, join types and raw equalities (new sql-safe.ts) - quote identifiers and bind values in schema manager DDL/queries Data loss: - schema sync runs with foreign keys off inside a transaction, with foreign_key_check before commit (DROP TABLE no longer cascades) - parentTableName tables: stop mutating the parent schema and copy inherited columns on rebuild - keep column data on type changes (match columns by name) - backups and schema snapshots use VACUUM INTO (includes WAL data); restore closes the DB, clears -wal/-shm, and swaps atomically - backup listing/trimming only touches <db_name>-<timestamp> files Correctness: - update/delete use the generator's WHERE clause instead of a regex - keep 0/""/false values; booleans stored as 0/1 - IN/NOT IN and BETWEEN arrays, NOT LIKE wrapping, AVG, select aliases - OFFSET without LIMIT, count ignores pagination - MATCH emits `field MATCH fn(?)` for sqlite-vec - sql() returns rows for any row-producing statement - targetId no longer mutates the caller's query - init throws instead of process.exit; CLI reports errors Co-Authored-By: Claude Opus 5.5 <[email protected]>
50 lines
1.7 KiB
TypeScript
50 lines
1.7 KiB
TypeScript
import init from "../functions/init";
|
|
import { sqlIdent, sqlQuoteIdent } from "../utils/sql-safe";
|
|
|
|
type Params = {
|
|
sql: string;
|
|
table: string;
|
|
data: any | any[];
|
|
};
|
|
|
|
export default async function ({ sql: passed_sql, table, data }: Params) {
|
|
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)
|
|
.filter((field) => field !== "updated_at")
|
|
.map((field) => {
|
|
const column = sqlIdent(field, "column name");
|
|
return `${column} = excluded.${column}`;
|
|
});
|
|
|
|
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];
|
|
if (!field?.fieldName) continue;
|
|
sql += ` ON CONFLICT(${sqlQuoteIdent(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];
|
|
const columns = constraint?.constraintTableFields
|
|
?.map((c) => sqlQuoteIdent(String(c.value)))
|
|
?.join(", ");
|
|
if (!columns) continue;
|
|
sql += ` ON CONFLICT(${columns}) DO UPDATE SET ${set_sql}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return sql;
|
|
}
|