From fd85ed3393d3f173cc27dd0c8fb86b8c428a2685 Mon Sep 17 00:00:00 2001 From: Anton Date: Sat, 30 Nov 2024 12:34:00 +0300 Subject: [PATCH] fix: updated qb insert added isUseDefaultValues --- src/lib/mysql/model/queries.ts | 1 - src/lib/mysql/query-builder/query-builder.ts | 2 + src/lib/mysql/query-builder/query-handler.ts | 80 +++++++++--- src/lib/pg/model/queries.ts | 13 +- src/lib/pg/query-builder/query-builder.ts | 2 + src/lib/pg/query-builder/query-handler.ts | 123 ++++++++++--------- src/test/MYSQL/04/index.ts | 42 ++++--- src/test/PG/04/index.ts | 42 ++++--- 8 files changed, 194 insertions(+), 111 deletions(-) diff --git a/src/lib/mysql/model/queries.ts b/src/lib/mysql/model/queries.ts index 1c69c0d..f00ff32 100644 --- a/src/lib/mysql/model/queries.ts +++ b/src/lib/mysql/model/queries.ts @@ -6,7 +6,6 @@ export const generateTimestampQuery = (type: "timestamp" | "unix_timestamp") => return "UTC_TIMESTAMP()"; case "unix_timestamp": return "ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000)"; - default: throw new Error("Invalid type: " + type); } diff --git a/src/lib/mysql/query-builder/query-builder.ts b/src/lib/mysql/query-builder/query-builder.ts index 24de727..20e0433 100644 --- a/src/lib/mysql/query-builder/query-builder.ts +++ b/src/lib/mysql/query-builder/query-builder.ts @@ -135,6 +135,7 @@ export class QueryBuilder { * Inserts records into the database. * * @param options - The options for the insert operation. + * @param [options.isUseDefaultValues] - Use default values for missing columns when options.params is an array. Defaults to false. * @param [options.onConflict] - Conflict resolution strategy. * @param options.params - The parameters to insert. * @param [options.updateColumn] - Optional default system column for updates. @@ -142,6 +143,7 @@ export class QueryBuilder { * @returns The current QueryBuilder instance for method chaining. */ insert(options: { + isUseDefaultValues?: boolean; onConflict?: string; params: T | T[]; updateColumn?: { title: string; type: "unix_timestamp" | "timestamp"; } | null; diff --git a/src/lib/mysql/query-builder/query-handler.ts b/src/lib/mysql/query-builder/query-handler.ts index 4d59911..e4900b4 100644 --- a/src/lib/mysql/query-builder/query-handler.ts +++ b/src/lib/mysql/query-builder/query-handler.ts @@ -225,6 +225,7 @@ export class QueryHandler { * conflict handling using the `onConflict` option, and automatic updates of timestamp columns if specified. * * @param options - Options for constructing the INSERT query. + * @param [options.isUseDefaultValues] - Use default values for missing columns when options.params is an array. * @param options.params - The parameters for the INSERT operation, which can be a single object or an array of objects. * @param [options.onConflict] - Optional SQL clause to handle conflicts, typically used to specify `ON CONFLICT DO UPDATE`. * @param [options.updateColumn] - @@ -236,60 +237,101 @@ export class QueryHandler { * @throws {Error} Throws an error if parameters are invalid or if fields are undefined. */ insert(options: { + isUseDefaultValues?: boolean; onConflict?: string; params: T | T[]; updateColumn?: { title: string; type: "unix_timestamp" | "timestamp"; } | null; }): void { const v = []; - const k = []; const headers = new Set(); let insertQuery = ""; if (Array.isArray(options.params)) { - const [example] = options.params; + const k: [string, string | undefined][][] = []; - if (!example) throw new Error("Invalid parameters"); + const collectHeaders = (params: T[]) => { + for (const p of params) { + const keys = Object.keys(p); - const params = SharedHelpers.clearUndefinedFields(example); + for (const key of keys) { + headers.add(key); + } + } + + return headers; + }; - Object.keys(params).forEach((e) => headers.add(e)); + collectHeaders(options.params); - for (const pR of options.params) { - const params = SharedHelpers.clearUndefinedFields(pR); - const keys = Object.keys(params); + if (options.updateColumn) { + headers.add(options.updateColumn.title); + } - if (!keys.length) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(pR).join(", ")}`); + const headersArray = Array.from(headers); - for (const key of keys) { - if (!headers.has(key)) { - throw new Error(`Invalid params, all fields are undefined - ${Object.keys(pR).join(", ")}`); + for (const p of options.params) { + const keys: [string, string | undefined][] = []; + + const preparedParams = headersArray.reduce((acc, e) => { + const value = p[e]; + + if (options.updateColumn?.title === e) { + return acc; } - } - v.push(...Object.values(params)); + if (value === undefined) { + if (options.isUseDefaultValues) { + keys.push([e, "DEFAULT"]); + } else { + throw new Error(`Invalid parameters - ${e} is undefined at ${JSON.stringify(p)} for INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")})`); + } + + return acc; + } + + acc[e] = value; + + keys.push([e, undefined]); + + return acc; + }, {}); + + v.push(...Object.values(preparedParams)); if (options.updateColumn) { - keys.push(`${options.updateColumn.title} = ${generateTimestampQuery(options.updateColumn.type)}`); + keys.push([options.updateColumn.title, generateTimestampQuery(options.updateColumn.type)]); } k.push(keys); } - insertQuery += k.map((e) => e.map(() => "?")).join("),("); + insertQuery += k.map((e) => e.map((el) => { + if (el[1]) return el[1]; + + return "?"; + })).join("),("); } else { + const k: [string, string | undefined][] = []; + const params = SharedHelpers.clearUndefinedFields(options.params); - Object.keys(params).forEach((e) => { headers.add(e); k.push(e); }); + Object.keys(params).forEach((e) => { headers.add(e); k.push([e, undefined]); }); v.push(...Object.values(params)); if (!headers.size) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(options.params).join(", ")}`); if (options.updateColumn) { - k.push(`${options.updateColumn.title} = ${generateTimestampQuery(options.updateColumn.type)}`); + headers.add(options.updateColumn.title); + + k.push([options.updateColumn.title, generateTimestampQuery(options.updateColumn.type)]); } - insertQuery += k.map(() => "?").join(","); + insertQuery += k.map((e) => { + if (e[1]) return e[1]; + + return "?"; + }).join(","); } this.#mainQuery = `INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")}) VALUES(${insertQuery})`; diff --git a/src/lib/pg/model/queries.ts b/src/lib/pg/model/queries.ts index 1081051..1219da8 100644 --- a/src/lib/pg/model/queries.ts +++ b/src/lib/pg/model/queries.ts @@ -1,5 +1,16 @@ import * as SharedTypes from "../../../shared-types/index.js"; +export const generateTimestampQuery = (type: "timestamp" | "unix_timestamp") => { + switch (type) { + case "timestamp": + return "NOW()"; + case "unix_timestamp": + return "ROUND((EXTRACT(EPOCH FROM NOW()) * (1000)::NUMERIC))"; + default: + throw new Error("Invalid type: " + type); + } +}; + export default { /** * Generates an SQL `INSERT` statement for inserting multiple rows into a table. @@ -326,7 +337,7 @@ export default { * @param updateField.title - The name of the column for the timestamp. * @param updateField.type - The type of timestamp to insert. * @param [returning] - An optional array of column names to return after the update. - * + * * @returns The generated SQL `UPDATE` statement. * * @throws {Error} If an invalid `updateField.type` is provided. diff --git a/src/lib/pg/query-builder/query-builder.ts b/src/lib/pg/query-builder/query-builder.ts index 99b5b8a..af8a53f 100644 --- a/src/lib/pg/query-builder/query-builder.ts +++ b/src/lib/pg/query-builder/query-builder.ts @@ -135,6 +135,7 @@ export class QueryBuilder { * Inserts records into the database. * * @param options - The options for the insert operation. + * @param [options.isUseDefaultValues] - Use default values for missing columns when options.params is an array. Defaults to false. * @param [options.onConflict] - Conflict resolution strategy. * @param options.params - The parameters to insert. * @param [options.updateColumn] - Optional default system column for updates. @@ -142,6 +143,7 @@ export class QueryBuilder { * @returns The current QueryBuilder instance for method chaining. */ insert(options: { + isUseDefaultValues?: boolean; onConflict?: string; params: T | T[]; updateColumn?: { title: string; type: "unix_timestamp" | "timestamp"; } | null; diff --git a/src/lib/pg/query-builder/query-handler.ts b/src/lib/pg/query-builder/query-handler.ts index a30a1c7..fe44dd6 100644 --- a/src/lib/pg/query-builder/query-handler.ts +++ b/src/lib/pg/query-builder/query-handler.ts @@ -2,6 +2,7 @@ import * as Helpers from "../helpers/index.js"; import * as ModelTypes from "../model/types.js"; import * as SharedHelpers from "../../../shared-helpers/index.js"; import * as SharedTypes from "../../../shared-types/index.js"; +import { generateTimestampQuery } from "../model/queries.js"; /** * Class to handle SQL query construction. @@ -261,6 +262,7 @@ export class QueryHandler { * conflict handling using the `onConflict` option, and automatic updates of timestamp columns if specified. * * @param options - Options for constructing the INSERT query. + * @param [options.isUseDefaultValues] - Use default values for missing columns when options.params is an array. * @param options.params - The parameters for the INSERT operation, which can be a single object or an array of objects. * @param [options.onConflict] - Optional SQL clause to handle conflicts, typically used to specify `ON CONFLICT DO UPDATE`. * @param [options.updateColumn] - @@ -272,54 +274,70 @@ export class QueryHandler { * @throws {Error} Throws an error if parameters are invalid or if fields are undefined. */ insert(options: { + isUseDefaultValues?: boolean; onConflict?: string; params: T | T[]; updateColumn?: { title: string; type: "unix_timestamp" | "timestamp"; } | null; }): void { const v = []; - const k = []; const headers = new Set(); let insertQuery = ""; if (Array.isArray(options.params)) { - const [example] = options.params; + const k: [string, string | undefined][][] = []; - if (!example) throw new Error("Invalid parameters"); + const collectHeaders = (params: T[]) => { + for (const p of params) { + const keys = Object.keys(p); - const params = SharedHelpers.clearUndefinedFields(example); + for (const key of keys) { + headers.add(key); + } + } - Object.keys(params).forEach((e) => headers.add(e)); + return headers; + }; - for (const pR of options.params) { - const params = SharedHelpers.clearUndefinedFields(pR); - const keys = Object.keys(params); + collectHeaders(options.params); - if (!keys.length) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(pR).join(", ")}`); + if (options.updateColumn) { + headers.add(options.updateColumn.title); + } - for (const key of keys) { - if (!headers.has(key)) { - throw new Error(`Invalid params, all fields are undefined - ${Object.keys(pR).join(", ")}`); - } - } + const headersArray = Array.from(headers); - v.push(...Object.values(params)); + for (const p of options.params) { + const keys: [string, string | undefined][] = []; - if (options.updateColumn) { - switch (options.updateColumn.type) { - case "timestamp": { - keys.push(`${options.updateColumn.title} = NOW()`); - break; - } - case "unix_timestamp": { - keys.push(`${options.updateColumn.title} = ROUND((EXTRACT(EPOCH FROM NOW()) * (1000)::NUMERIC))`); - break; - } + const preparedParams = headersArray.reduce((acc, e) => { + const value = p[e]; - default: { - throw new Error("Invalid type: " + options.updateColumn.type); + if (options.updateColumn?.title === e) { + return acc; + } + + if (value === undefined) { + if (options.isUseDefaultValues) { + keys.push([e, "DEFAULT"]); + } else { + throw new Error(`Invalid parameters - ${e} is undefined at ${JSON.stringify(p)} for INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")})`); } + + return acc; } + + acc[e] = value; + + keys.push([e, undefined]); + + return acc; + }, {}); + + v.push(...Object.values(preparedParams)); + + if (options.updateColumn) { + keys.push([options.updateColumn.title, generateTimestampQuery(options.updateColumn.type)]); } k.push(keys); @@ -329,36 +347,38 @@ export class QueryHandler { let idx = valuesOrder; - insertQuery += k.map((e) => e.map(() => "$" + (++idx))).join("),("); + insertQuery += k.map((e) => e.map((el) => { + if (el[1]) return el[1]; + + return "$" + (++idx); + })).join("),("); } else { + const k: [string, string | undefined][] = []; + const params = SharedHelpers.clearUndefinedFields(options.params); - Object.keys(params).forEach((e) => { headers.add(e); k.push(e); }); + Object.keys(params).forEach((e) => { headers.add(e); k.push([e, undefined]); }); v.push(...Object.values(params)); if (!headers.size) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(options.params).join(", ")}`); if (options.updateColumn) { - switch (options.updateColumn.type) { - case "timestamp": { - k.push(`${options.updateColumn.title} = NOW()`); - break; - } + headers.add(options.updateColumn.title); - case "unix_timestamp": { - k.push(`${options.updateColumn.title} = ROUND((EXTRACT(EPOCH FROM NOW()) * (1000)::NUMERIC))`); - break; - } - - default: { - throw new Error("Invalid type: " + options.updateColumn.type); - } - } + k.push([options.updateColumn.title, generateTimestampQuery(options.updateColumn.type)]); } const valuesOrder = this.#valuesOrder; - insertQuery += k.map((_, idx) => "$" + (idx + 1 + valuesOrder)).join(","); + let idx = valuesOrder; + + insertQuery += k.map((e) => { + if (e[1]) return e[1]; + + idx += 1; + + return "$" + (idx); + }).join(","); } this.#mainQuery = `INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")}) VALUES(${insertQuery})`; @@ -425,20 +445,7 @@ export class QueryHandler { let updateQuery = k.map((e: string, idx: number) => `${e} = $${idx + 1 + valuesOrder}`).join(","); if (options.updateColumn) { - switch (options.updateColumn.type) { - case "timestamp": { - updateQuery += `, ${options.updateColumn.title} = NOW()`; - break; - } - case "unix_timestamp": { - updateQuery += `, ${options.updateColumn.title} = ROUND((EXTRACT(EPOCH FROM NOW()) * (1000)::NUMERIC))`; - break; - } - - default: { - throw new Error("Invalid type: " + options.updateColumn.type); - } - } + updateQuery += `, ${options.updateColumn.title} = ${generateTimestampQuery(options.updateColumn.type)}`; } this.#mainQuery = `UPDATE ${this.#dataSourceRaw} SET ${updateQuery}`; diff --git a/src/test/MYSQL/04/index.ts b/src/test/MYSQL/04/index.ts index 5c9f8bb..fb027b2 100644 --- a/src/test/MYSQL/04/index.ts +++ b/src/test/MYSQL/04/index.ts @@ -72,11 +72,17 @@ export const start = async (creds: MYSQL.ModelTypes.TDBCreds) => { const { affectedRows } = await User.model.queryBuilder() .insert({ - params: firstNames.map((e) => ({ first_name: e, id_user_role: userRole.id })), + isUseDefaultValues: true, + params: firstNames.map((e) => ({ + first_name: e, + id_user_role: userRole.id, + last_name: undefined, + })), + updateColumn: { title: "updated_at", type: "timestamp" }, }) .execute(); - assert.strictEqual(affectedRows, 5); + assert.strictEqual(affectedRows, firstNames.length); }, ); @@ -87,14 +93,16 @@ export const start = async (creds: MYSQL.ModelTypes.TDBCreds) => { const users = await User.getAll(); const firstUser = users.at(0); + if (!firstUser) throw new Error("FirstUser not found"); + assert.strictEqual(users.length, 7); - assert.strictEqual(typeof firstUser?.id, "number"); - assert.strictEqual(typeof firstUser?.id_user_role, "number"); - assert.strictEqual(typeof firstUser?.is_deleted, "number"); - assert.strictEqual(typeof firstUser?.first_name, "string"); - assert.strictEqual(firstUser?.last_name, null); - assert.strictEqual(typeof firstUser?.created_at, "object"); - assert.strictEqual(firstUser?.updated_at, null); + assert.strictEqual(typeof firstUser.id, "number"); + assert.strictEqual(typeof firstUser.id_user_role, "number"); + assert.strictEqual(typeof firstUser.is_deleted, "number"); + assert.strictEqual(typeof firstUser.first_name, "string"); + assert.strictEqual(firstUser.last_name, null); + assert.strictEqual(typeof firstUser.created_at, "object"); + assert.strictEqual(firstUser.updated_at, null); }, ); } @@ -106,14 +114,16 @@ export const start = async (creds: MYSQL.ModelTypes.TDBCreds) => { const users = await User.getAllNotDeletedWithRole(); const firstUser = users.at(0); + if (!firstUser) throw new Error("FirstUser not found"); + assert.strictEqual(users.length, 7); - assert.strictEqual(typeof firstUser?.id, "number"); - assert.strictEqual(typeof firstUser?.id_user_role, "number"); - assert.strictEqual(typeof firstUser?.is_deleted, "number"); - assert.strictEqual(typeof firstUser?.first_name, "string"); - assert.strictEqual(firstUser?.last_name, null); - assert.strictEqual(typeof firstUser?.created_at, "object"); - assert.strictEqual(firstUser?.updated_at, null); + assert.strictEqual(typeof firstUser.id, "number"); + assert.strictEqual(typeof firstUser.id_user_role, "number"); + assert.strictEqual(typeof firstUser.is_deleted, "number"); + assert.strictEqual(typeof firstUser.first_name, "string"); + assert.strictEqual(firstUser.last_name, null); + assert.strictEqual(typeof firstUser.created_at, "object"); + assert.strictEqual(firstUser.updated_at, null); }, ); } diff --git a/src/test/PG/04/index.ts b/src/test/PG/04/index.ts index 5dadb1f..ae85899 100644 --- a/src/test/PG/04/index.ts +++ b/src/test/PG/04/index.ts @@ -75,12 +75,18 @@ export const start = async (creds: PG.ModelTypes.TDBCreds) => { const users = await User.model.queryBuilder() .insert({ - params: firstNames.map((e) => ({ first_name: e, id_user_role: userRole.id })), + isUseDefaultValues: true, + params: firstNames.map((e) => ({ + first_name: e, + id_user_role: userRole.id, + last_name: undefined, + })), + updateColumn: { title: "updated_at", type: "timestamp" }, }) .returning(["id"]) .execute<{ id: string; }>(); - assert.strictEqual(users.length, 5); + assert.strictEqual(users.length, firstNames.length); }, ); @@ -91,14 +97,16 @@ export const start = async (creds: PG.ModelTypes.TDBCreds) => { const users = await User.getAll(); const firstUser = users.at(0); + if (!firstUser) throw new Error("FirstUser not found"); + assert.strictEqual(users.length, 7); - assert.strictEqual(typeof firstUser?.id, "string"); - assert.strictEqual(typeof firstUser?.id_user_role, "string"); - assert.strictEqual(typeof firstUser?.is_deleted, "boolean"); - assert.strictEqual(typeof firstUser?.first_name, "string"); - assert.strictEqual(firstUser?.last_name, null); - assert.strictEqual(typeof firstUser?.created_at, "object"); - assert.strictEqual(firstUser?.updated_at, null); + assert.strictEqual(typeof firstUser.id, "string"); + assert.strictEqual(typeof firstUser.id_user_role, "string"); + assert.strictEqual(typeof firstUser.is_deleted, "boolean"); + assert.strictEqual(typeof firstUser.first_name, "string"); + assert.strictEqual(firstUser.last_name, null); + assert.strictEqual(typeof firstUser.created_at, "object"); + assert.strictEqual(firstUser.updated_at, null); }, ); } @@ -110,14 +118,16 @@ export const start = async (creds: PG.ModelTypes.TDBCreds) => { const users = await User.getAllNotDeletedWithRole(); const firstUser = users.at(0); + if (!firstUser) throw new Error("FirstUser not found"); + assert.strictEqual(users.length, 7); - assert.strictEqual(typeof firstUser?.id, "string"); - assert.strictEqual(typeof firstUser?.id_user_role, "string"); - assert.strictEqual(typeof firstUser?.is_deleted, "boolean"); - assert.strictEqual(typeof firstUser?.first_name, "string"); - assert.strictEqual(firstUser?.last_name, null); - assert.strictEqual(typeof firstUser?.created_at, "object"); - assert.strictEqual(firstUser?.updated_at, null); + assert.strictEqual(typeof firstUser.id, "string"); + assert.strictEqual(typeof firstUser.id_user_role, "string"); + assert.strictEqual(typeof firstUser.is_deleted, "boolean"); + assert.strictEqual(typeof firstUser.first_name, "string"); + assert.strictEqual(firstUser.last_name, null); + assert.strictEqual(typeof firstUser.created_at, "object"); + assert.strictEqual(firstUser.updated_at, null); }, ); }