import type { ServerQueryParam, ServerQueryParamOrder, SQLInsertGenValueType, } from "../types"; 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"; type Param = { genObject?: ServerQueryParam; 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 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: passedTableName, genObject, count, targetId, }: Param): Return { const tableName = sqlIdent(passedTableName, "table name"); const finalQuery = genObject?.query ? genObject.query : undefined; const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined; const sqlSearhValues: SQLInsertGenValueType[] = []; const whereValues: SQLInsertGenValueType[] = []; 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, 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, }); whereValues.push(...values); return str; } }) .filter((str) => typeof str == "string"); if (!subSearchString.length) return; return ( "(" + subSearchString.join(` ${sqlOperator(queryObj.operator)} `) + ")" ); } const { str, values } = sqlGenGenSearchStr({ queryObj, field: queryObj.fieldName || field, join: genObject?.join, table_name: tableName, }); whereValues.push(...values); return str; }); const cleanedUpSearchStr = sqlSearhString?.filter( (str) => typeof str == "string", ); const whereConditions: string[] = []; if (cleanedUpSearchStr?.[0]) { const stringOperator = sqlOperator(genObject?.searchOperator); whereConditions.push( `(${cleanedUpSearchStr.join(` ${stringOperator} `)})`, ); } 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) { const grabGroupStr = ( group: string | number | symbol | { field: unknown; table?: string }, ) => { if (typeof group == "object") { return group.table ? `${sqlIdent(group.table, "table name")}.${sqlIdent(group.field, "field name")}` : sqlField(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: ServerQueryParamOrder) { 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`; const orders = ( Array.isArray(genObject.order) ? genObject.order : [genObject.order] ).filter(Boolean); orderSrt += orders.map(grabOrderString).join(","); queryString += ` ${orderSrt}`; } /** * 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: V): val is NonNullable { return val !== undefined && val !== null; } // 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; // })();