From f53a9bf00ec7eea347a0e95d1090816b6df09849 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Fri, 8 Nov 2024 19:54:14 +0100 Subject: [PATCH] Add Feature: SQL query generator --- functions/sql/sql-generator.d.ts | 8 ++ functions/sql/sql-generator.js | 164 +++++++++++++++++++++++++++++++ index.d.ts | 4 + index.js | 21 ++-- package-shared/types/index.d.ts | 74 ++++++++++++++ 5 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 functions/sql/sql-generator.d.ts create mode 100644 functions/sql/sql-generator.js diff --git a/functions/sql/sql-generator.d.ts b/functions/sql/sql-generator.d.ts new file mode 100644 index 0000000..2fed80c --- /dev/null +++ b/functions/sql/sql-generator.d.ts @@ -0,0 +1,8 @@ +export = sqlGenerator; +declare function sqlGenerator(Param0: { + genObject?: import("../../package-shared/types").ServerQueryParam; + tableName: string; +}): { + string: string; + values: string[]; +} | undefined; diff --git a/functions/sql/sql-generator.js b/functions/sql/sql-generator.js new file mode 100644 index 0000000..7dc0fa3 --- /dev/null +++ b/functions/sql/sql-generator.js @@ -0,0 +1,164 @@ +// @ts-check + +/** + * # SQL Query Generator + * @description Generates an SQL Query for node module `mysql` or `serverless-mysql` + * @type {import("../../package-shared/types").SqlGeneratorFn} + */ +function sqlGenerator({ tableName, genObject }) { + if (!genObject) return undefined; + + const finalQuery = genObject.query ? genObject.query : undefined; + + const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined; + + /** @type {string[]} */ + const sqlSearhValues = []; + const sqlSearhString = queryKeys?.map((field) => { + const queryObj = finalQuery?.[field]; + if (!queryObj) return; + + let str = `${field}=?`; + + if ( + typeof queryObj.value == "string" || + typeof queryObj.value == "number" + ) { + const valueParsed = String(queryObj.value); + if (queryObj.equality == "LIKE") { + str = `LOWER(${field}) LIKE LOWER('%${valueParsed}%')`; + } else { + sqlSearhValues.push(valueParsed); + } + } else if (Array.isArray(queryObj.value)) { + /** @type {string[]} */ + const strArray = []; + queryObj.value.forEach((val) => { + const valueParsed = val; + if (queryObj.equality == "LIKE") { + strArray.push( + `LOWER(${field}) LIKE LOWER('%${valueParsed}%')` + ); + } else { + strArray.push(`${field} = ?`); + sqlSearhValues.push(valueParsed); + } + }); + + str = "(" + strArray.join(` ${queryObj.operator || "AND"} `) + ")"; + } + + return str; + }); + + function generateJoinStr( + /** @type {import("../../package-shared/types").ServerQueryParamsJoinMatchObject} */ mtch, + /** @type {import("../../package-shared/types").ServerQueryParamsJoin} */ join + ) { + return `${ + typeof mtch.source == "object" ? mtch.source.tableName : tableName + }.${ + typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source + }=${ + typeof mtch.target == "object" + ? mtch.target.tableName + : join.tableName + }.${ + typeof mtch.target == "object" ? mtch.target.fieldName : mtch.target + }`; + } + + let queryString = (() => { + let str = "SELECT"; + if (genObject.selectFields?.[0]) { + if (genObject.join) { + str += ` ${genObject.selectFields + ?.map((fld) => `${tableName}.${fld}`) + .join(",")}`; + } else { + str += ` ${genObject.selectFields?.join(",")}`; + } + } else { + if (genObject.join) { + str += ` ${tableName}.*`; + } else { + str += " *"; + } + } + + if (genObject.join) { + str += + "," + + genObject.join + .map((joinObj) => { + if (joinObj.selectFields) { + return joinObj.selectFields + .map((slFld) => { + if (typeof slFld == "string") { + return `${joinObj.tableName}.${slFld}`; + } else if (typeof slFld == "object") { + let aliasSlctFld = `${joinObj.tableName}.${slFld.field}`; + if (slFld.alias) + aliasSlctFld += ` as ${slFld.alias}`; + return aliasSlctFld; + } + }) + .join(","); + } else { + return `${joinObj.tableName}.*`; + } + }) + .join(","); + } + + str += ` FROM ${tableName}`; + + if (genObject.join) { + str += + " " + + genObject.join + .map((join) => { + return ( + join.joinType + + " " + + join.tableName + + " ON " + + (() => { + if (Array.isArray(join.match)) { + return ( + "(" + + join.match + .map((mtch) => + generateJoinStr(mtch, join) + ) + .join(" AND ") + + ")" + ); + } else if (typeof join.match == "object") { + return generateJoinStr(join.match, join); + } + })() + ); + }) + .join(" "); + } + + return str; + })(); + + if (sqlSearhString) { + const stringOperator = genObject?.searchOperator || "AND"; + queryString += ` WHERE ${sqlSearhString.join(` ${stringOperator} `)} `; + } + + if (genObject.order) + queryString += ` ORDER BY ${genObject.order.field} ${genObject.order.strategy}`; + if (genObject.limit) queryString += ` LIMIT ${genObject.limit}`; + + return { + string: queryString, + values: sqlSearhValues, + }; +} + +module.exports = sqlGenerator; diff --git a/index.d.ts b/index.d.ts index 967880a..ed0efae 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,6 +24,9 @@ export namespace user { import getSchema = require("./utils/get-schema"); import sanitizeSql = require("./utils/functions/sanitizeSql"); import datasquirelClient = require("./client"); +export namespace sql { + export { sqlGenerator }; +} import uploadImage = require("./utils/upload-image"); import uploadFile = require("./utils/upload-file"); import deleteFile = require("./utils/delete-file"); @@ -39,4 +42,5 @@ import getToken = require("./users/get-token"); import validateToken = require("./users/validate-token"); import loginWithGoogle = require("./users/social/google-auth"); import loginWithGithub = require("./users/social/github-auth"); +import sqlGenerator = require("./functions/sql/sql-generator"); export { get, post, getSchema, sanitizeSql, datasquirelClient as client }; diff --git a/index.js b/index.js index cf2f4ff..5a9c3e0 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ const validateToken = require("./users/validate-token"); const sanitizeSql = require("./utils/functions/sanitizeSql"); const datasquirelClient = require("./client"); +const sqlGenerator = require("./functions/sql/sql-generator"); //////////////////////////////////////// //////////////////////////////////////// @@ -62,17 +63,25 @@ const media = { deleteFile: deleteFile, }; +/** + * SQL Utils + */ +const sql = { + sqlGenerator, +}; + /** * Main Export */ const datasquirel = { - get: get, - post: post, - media: media, - user: user, - getSchema: getSchema, - sanitizeSql: sanitizeSql, + get, + post, + media, + user, + getSchema, + sanitizeSql, client: datasquirelClient, + sql, }; module.exports = datasquirel; diff --git a/package-shared/types/index.d.ts b/package-shared/types/index.d.ts index baea3bd..6ce59c9 100644 --- a/package-shared/types/index.d.ts +++ b/package-shared/types/index.d.ts @@ -1134,3 +1134,77 @@ export type FetchApiReturn = { msg?: string; [key: string]: any; }; + +export type ServerQueryParam = { + selectFields?: string[]; + query?: ServerQueryQueryObject; + limit?: number; + order?: { + field: string; + strategy: "ASC" | "DESC"; + }; + searchOperator?: "AND" | "OR"; + searchEquality?: "EQUAL" | "LIKE"; + addUserId?: { + fieldName: string; + }; + join?: ServerQueryParamsJoin[]; +} & { + [key: string]: any; +}; + +export type ServerQueryQueryObject = { + [key: string]: { + value: string | string[]; + operator?: "AND" | "OR"; + equality?: "EQUAL" | "LIKE"; + }; +}; + +export type FetchDataParams = { + path: string; + method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + body?: object | string; + query?: AuthFetchQuery; +}; + +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: string | number | { [key: string]: any }; +}; + +export type ServerQueryParamsJoin = { + joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; + tableName: string; + match?: + | ServerQueryParamsJoinMatchObject + | ServerQueryParamsJoinMatchObject[]; + selectFields?: ( + | string + | { + field: string; + alias?: string; + } + )[]; +}; + +export type ServerQueryParamsJoinMatchObject = { + /** Field name from the **Root Table** */ + source: string | ServerQueryParamsJoinMatchSourceTargetObject; + /** Field name from the **Join Table** */ + target: string | ServerQueryParamsJoinMatchSourceTargetObject; +}; + +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; + +export type SqlGeneratorFn = (Param0: { + genObject?: ServerQueryParam; + tableName: string; +}) => + | { + string: string; + values: string[]; + } + | undefined;