diff --git a/.dockerignore b/.dockerignore index 30e192f..2b8b584 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,7 @@ config* docker* Docker* README* -node_modules/ +**node_modules/ .* pnpm-lock* dist diff --git a/Dockerfile.bot b/Dockerfile.bot index 051ef2b..fb99468 100644 --- a/Dockerfile.bot +++ b/Dockerfile.bot @@ -1,6 +1,6 @@ FROM node:23-alpine3.20 -COPY . /app +COPY ./app/. /app/. WORKDIR /app @@ -10,4 +10,4 @@ RUN pnpm install RUN npm run build -CMD npm start \ No newline at end of file +CMD npm start diff --git a/AuctionHandler.worker.ts b/app/AuctionHandler.worker.ts similarity index 100% rename from AuctionHandler.worker.ts rename to app/AuctionHandler.worker.ts diff --git a/env.d.ts b/app/env.d.ts similarity index 100% rename from env.d.ts rename to app/env.d.ts diff --git a/index.ts b/app/index.ts similarity index 100% rename from index.ts rename to app/index.ts diff --git a/package.json b/app/package.json similarity index 100% rename from package.json rename to app/package.json diff --git a/app/sql/init.sql b/app/sql/init.sql new file mode 100644 index 0000000..83f6be8 --- /dev/null +++ b/app/sql/init.sql @@ -0,0 +1,68 @@ +-- Enable Extensions +CREATE EXTENSION IF NOT EXISTS pg_cron; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create table +CREATE TABLE if NOT EXISTS auctions ( + id UUID PRIMARY KEY, + auctionid VARCHAR(255) NOT NULL, + lbin DECIMAL(65,5), + modified TIMESTAMP(2), + UNIQUE (auctionid) +); + +-- Update trigger function +CREATE OR REPLACE FUNCTION public.fn_update_modified_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.modified = now(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Update trigger +DROP TRIGGER IF EXISTS update_auction_modtime on auctions; +CREATE TRIGGER update_auction_modtime +BEFORE UPDATE ON auctions +FOR EACH ROW +EXECUTE PROCEDURE public.fn_update_modified_column(); + +-- Cleanup function +CREATE OR REPLACE FUNCTION public.cleanup_old_entries() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + temp_id UUID; +BEGIN + FOR temp_id IN + SELECT id + FROM auctions + WHERE EXTRACT(EPOCH FROM (NOW() - modified))/3600 >= 6 -- 6 hours + LOOP + DELETE FROM auctions + WHERE auctions.id = temp_id; + END LOOP; + + RETURN; +END; +$$; + +-- Add job to pg_cron +DO $outer$ +DECLARE job_exists boolean := false; +BEGIN + SELECT (count(*) > 0) + INTO job_exists + FROM cron.job + WHERE jobname = 'cleanup_old_entries_event'; + + IF NOT job_exists THEN + PERFORM cron.schedule( + 'cleanup_old_entries_event', + '*/2 * * * *', + $sql$ SELECT public.cleanup_old_entries(); $sql$ + ); + END IF; +END; +$outer$; diff --git a/src/Types.ts b/app/src/Types.ts similarity index 100% rename from src/Types.ts rename to app/src/Types.ts diff --git a/src/auctionType.ts b/app/src/auctionType.ts similarity index 100% rename from src/auctionType.ts rename to app/src/auctionType.ts diff --git a/src/configLoader.ts b/app/src/configLoader.ts similarity index 100% rename from src/configLoader.ts rename to app/src/configLoader.ts diff --git a/src/errorHandler.ts b/app/src/errorHandler.ts similarity index 100% rename from src/errorHandler.ts rename to app/src/errorHandler.ts diff --git a/src/helperFunctions.ts b/app/src/helperFunctions.ts similarity index 100% rename from src/helperFunctions.ts rename to app/src/helperFunctions.ts diff --git a/app/src/sqlFunctions.ts b/app/src/sqlFunctions.ts new file mode 100644 index 0000000..912f837 --- /dev/null +++ b/app/src/sqlFunctions.ts @@ -0,0 +1,256 @@ +import { Pool, PoolClient, Query, QueryResult, QueryResultRow } from 'pg'; +import { loadConfig } from './configLoader'; +import { unifyArrays, randomArray } from './helperFunctions' +import fs from 'fs'; +const config = loadConfig(); + +class SqlSystem { + + private static pool: Pool; + + + public static async InitDB() { + this.pool = new Pool({ + database: 'postgres', + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + max: config.data.worker_count ?? 10 + }); + let conn: PoolClient | undefined; + try { + conn = await this.pool.connect(); + try { + await conn.query('BEGIN'); + let qry: string = fs.readFileSync('/app/sql/init.sql', 'utf8'); + await conn.query(qry); + await conn.query('COMMIT'); + } catch (error) { + console.error("InitDB Error: ", error); + if (conn) await conn.query('ROLLBACK'); + if (conn) conn.release(); + this.pool.end(); + throw error; + } + + //await this.TimeoutThread(10000) + + await conn.query('BEGIN'); + + console.log("Inserting random numbers into Pool SETUP"); + let dummy_auctionid: string[] = randomArray(10, 8), dummy_lbin: number[] = randomArray(10, 5); + await this.Upsert(dummy_auctionid, dummy_lbin); + + await conn.query('COMMIT'); + + console.log("Requesting All from table auctions PRE REMOVE"); + console.log(await this.Query("SELECT * FROM auctions", false)); + + await conn.query('BEGIN'); + + await this.RemoveAll() + + await conn.query('COMMIT') + + console.log("Requesting All from table auctions POST REMOVE"); + console.log(await this.Query("SELECT * FROM auctions", false)); + } + catch (error) { + console.error("InitDB Error: ", error); + if (conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + } + } + public static async Upsert(auctionid: string | string[], lbin: number | number[]) { + let conn: PoolClient | undefined; + try { + conn = await this.pool.connect(); + await conn.query('BEGIN'); + if (Array.isArray(auctionid) && Array.isArray(lbin)) + if (auctionid.length === lbin.length) { + await conn.query(` + INSERT INTO auctions (id, auctionid, lbin) + SELECT uuid_generate_v4()::UUID, unnest($1::text[]), unnest($2::decimal[]) + ON CONFLICT (auctionid) + DO UPDATE SET lbin = EXCLUDED.lbin; + `, [auctionid, lbin]); + } + else if (Array.isArray(auctionid) || Array.isArray(lbin)) + throw Error(`Upsert SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); + else { + await conn.query(` + INSERT INTO auctions (id, auctionid, lbin) + VALUES uuid_generate_v4()::UUID, $1::text, $2::decimal + ON CONFLICT (auctionid) + DO UPDATE SET lbin = EXCLUDED.lbin; + `, [auctionid, lbin]); + } + + await conn.query('COMMIT'); + } + catch (error) { + console.error("message_before_error: ", error); + if (conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + } + } + //REMOVE ALL ELEMENTS IN AUCTIONS TABLE + public static async RemoveAll() { + let conn: PoolClient | undefined; + try { + conn = await this.pool.connect(); + await conn.query('BEGIN'); + await conn.query('DELETE FROM auctions') + await conn.query('COMMIT'); + } + catch (error) { + console.error("InitDB Error: ", error); + if (conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + } + } + //REMOVE ELEMENT IN AUCTIONS TABLE BY AUCTION-ID + public static async RemoveByAuctionId(auctionid: string | string[]) { + let conn: PoolClient | undefined; + try { + conn = await this.pool.connect(); + await conn.query('BEGIN'); + if (Array.isArray(auctionid)) { + await conn.query('DELETE FROM auctions WHERE auctionid IN ($1);', [auctionid]); + } + else { + await conn.query('DELETE FROM auctions WHERE auctionid = $1;', [auctionid]); + } + await conn.query('COMMIT'); + } + catch (error) { + console.error("InitDB Error: ", error); + if (conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + } + } + + public static async Query(query: string, IsWriting: boolean = false) { + let conn: PoolClient | undefined; + let result: any; + try { + conn = await this.pool.connect() + if (IsWriting) { + await conn.query('BEGIN'); + conn.query(query); + await conn.query('COMMIT'); + } + else { + console.log('TRYING TO QUERY'); + result = (await conn.query(query)).rows; + } + } + catch (error) { + console.error("InitDB Error: ", error); + if (IsWriting && conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + if (!IsWriting) return result; + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////// NOT FIXED / UNTESTED FUNCTIONS + //MATCH PROVIDED ELEMENTS IN AUCTIONS TABLE - returns true/false + public static async Match(auctionid: string | string[], lbin: number | number[]): Promise { + let conn: PoolClient | undefined; + let result; + try { + conn = await this.pool.connect(); + if (Array.isArray(auctionid) && Array.isArray(lbin)) + if (auctionid.length === lbin.length) { + result = await conn.query(` + CREATE TEMPORARY TABLE IF NOT EXISTS TEMP ( + auctionID VARCHAR(255) PRIMARY KEY, + lbin DECIMAL(65,5) + ); + INSERT INTO TEMP (auctionID, lbin) VALUES ? + SELECT tmp.* + FROM TEMP AS tmp + WHERE NOT EXISTS ( + SELECT 1 + FROM auctions AS auc + WHERE auc.auctionID = tmp.auctionID + AND auc.lbin = tmp.lbin); + `, [await unifyArrays(auctionid, lbin)]); + return result.rows; + } + else if (Array.isArray(auctionid) || Array.isArray(lbin)) + throw Error(`Match SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); + else { + result = await conn.query(` + SELECT CASE + WHEN COUNT(*) > 0 + THEN true + ELSE false + END AS user_exists + FROM auctions WHERE auctionID = ?; + `, [auctionid]); + return Boolean(result.rows.length); + } + } + catch (error) { + console.error("message_before_error: ", error); + throw error; + } + finally { + if (conn) conn.release(); + } + } + //INSERT ELEMENT IN AUCTIONS TABLE OR UPDATE IF IT ALREADY EXISTS + public static async deprecated_Upsert(auctionid: string | string[], lbin: number | number[]) { + let conn: PoolClient | undefined; + try { + conn = await this.pool.connect(); + await conn.query('BEGIN'); + if (Array.isArray(auctionid) && Array.isArray(lbin)) + if (auctionid.length === lbin.length) { + await conn.query(` + INSERT INTO auctions (auctionid, LBin) + VALUES ($1,$2) + ON CONFLICT (auctionid) + DO UPDATE SET LBin = $2; + `, [auctionid, lbin]); + } + else if (Array.isArray(auctionid) || Array.isArray(lbin)) + throw Error(`Upsert SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); + else { + await conn.query(` + INSERT INTO auctions (auctionid, LBin) + VALUES ($1,$2) + ON CONFLICT (auctionid) + DO UPDATE SET LBin = $2; + `, [auctionid, lbin]); + } + await conn.query('COMMIT'); + } + catch (error) { + console.error("message_before_error: ", error); + if (conn) await conn.query('ROLLBACK'); + throw error; + } + finally { + if (conn) conn.release(); + } + } +} +export { + SqlSystem +} diff --git a/tsconfig.json b/app/tsconfig.json similarity index 100% rename from tsconfig.json rename to app/tsconfig.json diff --git a/docker-compose.yml b/docker-compose.yml index 8535b13..81a0d57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: context: . dockerfile: Dockerfile.db restart: unless-stopped + ports: + - 5432:5432 environment: POSTGRES_PASSWORD: example # Change this to a secure password. Has to be identical to DB_PASSWORD in the bot service volumes: diff --git a/src/sqlFunctions.ts b/src/sqlFunctions.ts deleted file mode 100644 index a3efd05..0000000 --- a/src/sqlFunctions.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { Pool, PoolClient, Query, QueryResult, QueryResultRow } from 'pg'; -import { loadConfig } from './configLoader'; -import { unifyArrays, randomArray } from './helperFunctions' -const config = loadConfig(); - -class SqlSystem { - - private static pool: Pool; - - - public static async InitDB() { - this.pool = new Pool({ - database: 'postgres', - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - max: config.data.worker_count ?? 10 - }); - let conn: PoolClient | undefined; - try { - conn = await this.pool.connect(); - //Setup tables for auctions and lifetimes - await conn.query(` - CREATE TABLE if NOT EXISTS auctions ( - id UUID PRIMARY KEY, - auctionid VARCHAR(255) NOT NULL, - lbin DECIMAL(65,5), - UNIQUE (auctionid) - ); - CREATE TABLE if NOT EXISTS lifetimes (id UUID PRIMARY KEY, insertedon TIMESTAMP(2)); - `); - //Setup lifetime Trigger - await conn.query(` - -- Insert trigger function - CREATE OR REPLACE FUNCTION public.fn_insert_lifetime() - RETURNS TRIGGER AS $$ - BEGIN - INSERT INTO lifetimes (id, insertedon) - VALUES (NEW.id, NOW()); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - -- Update trigger function - CREATE OR REPLACE FUNCTION public.fn_update_lifetime() - RETURNS TRIGGER AS $$ - BEGIN - UPDATE lifetimes - SET insertedon = NOW() - WHERE id = NEW.id; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - -- Remove trigger function - CREATE OR REPLACE FUNCTION public.fn_remove_lifetime() - RETURNS TRIGGER AS $$ - BEGIN - DELETE FROM lifetimes - WHERE id = OLD.id; - RETURN OLD; - END; - $$ LANGUAGE plpgsql; - `); - await conn.query(` - -- Insert trigger - DROP TRIGGER IF EXISTS insert_lifetime ON auctions; - CREATE TRIGGER insert_lifetime - AFTER INSERT ON auctions - FOR EACH ROW - EXECUTE PROCEDURE public.fn_insert_lifetime(); - - -- Update trigger - DROP TRIGGER IF EXISTS update_lifetime ON auctions; - CREATE TRIGGER update_lifetime - AFTER UPDATE ON auctions - FOR EACH ROW - EXECUTE PROCEDURE public.fn_update_lifetime(); - - -- Remove trigger - DROP TRIGGER IF EXISTS remove_lifetime ON auctions; - CREATE TRIGGER remove_lifetime - BEFORE DELETE ON auctions - FOR EACH ROW - EXECUTE PROCEDURE public.fn_remove_lifetime(); - `); - await conn.query(` - CREATE EXTENSION IF NOT EXISTS pg_cron; - CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - `); - //Setup Table sanitation - await conn.query(` - CREATE OR REPLACE FUNCTION public.cleanup_old_entries() - RETURNS void - LANGUAGE plpgsql - AS $$ - DECLARE - temp_id UUID; - BEGIN - FOR temp_id IN - SELECT id - FROM lifetimes - WHERE EXTRACT(EPOCH FROM (NOW() - insertedon))/3600 >= 6 -- 6 hours - LOOP - -- Removes matching entries from auctions - DELETE FROM auctions - WHERE auctions.id = temp_id; - END LOOP; - - RETURN; - END; - $$; - `); - await conn.query(` - DO $outer$ - DECLARE job_exists boolean := false; - BEGIN - SELECT (count(*) > 0) - INTO job_exists - FROM cron.job - WHERE jobname = 'cleanup_old_entries_event'; - - IF NOT job_exists THEN - PERFORM cron.schedule( - 'cleanup_old_entries_event', - '*/2 * * * *', - $sql$ SELECT public.cleanup_old_entries(); $sql$ - ); - END IF; - END; - $outer$; - `); - - //await this.TimeoutThread(10000) - - await conn.query('BEGIN'); - - console.log("Inserting random numbers into Pool SETUP"); - let dummy_auctionid: string[] = randomArray(10,8),dummy_lbin: number[] = randomArray(10,5); - await this.Upsert(dummy_auctionid,dummy_lbin); - - await conn.query('COMMIT'); - - console.log("Requesting All from table auctions PRE REMOVE"); - console.log(await this.Query("SELECT * FROM auctions",false)); - - await conn.query('BEGIN'); - - await this.RemoveAll() - - await conn.query('COMMIT') - - console.log("Requesting All from table auctions POST REMOVE"); - console.log(await this.Query("SELECT * FROM auctions",false)); - } - catch (error) { - console.error("InitDB Error: ", error); - if (conn) await conn.query('ROLLBACK'); - throw error; - } - finally { - if (conn) conn.release(); - } - } - public static async Upsert(auctionid: string|string[], lbin:number|number[]) - { - let conn: PoolClient | undefined; - try - { - conn = await this.pool.connect(); - await conn.query('BEGIN'); - if(Array.isArray(auctionid) && Array.isArray(lbin)) - if(auctionid.length === lbin.length) { - await conn.query(` - INSERT INTO auctions (id, auctionid, lbin) - SELECT uuid_generate_v4()::UUID, unnest($1::text[]), unnest($2::decimal[]) - ON CONFLICT (auctionid) - DO UPDATE SET lbin = EXCLUDED.lbin; - `,[auctionid,lbin]); - } - else if(Array.isArray(auctionid) || Array.isArray(lbin)) - throw Error(`Upsert SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); - else { - await conn.query(` - INSERT INTO auctions (id, auctionid, lbin) - VALUES uuid_generate_v4()::UUID, $1::text, $2::decimal - ON CONFLICT (auctionid) - DO UPDATE SET lbin = EXCLUDED.lbin; - `, [auctionid, lbin]); - } - - await conn.query('COMMIT'); - } - catch(error) - { - console.error("message_before_error: ", error); - if (conn) await conn.query('ROLLBACK'); - throw error; - } - finally - { - if (conn) conn.release(); - } - } -//REMOVE ALL ELEMENTS IN AUCTIONS TABLE -public static async RemoveAll() { - let conn: PoolClient | undefined; - try { - conn = await this.pool.connect(); - await conn.query('BEGIN'); - await conn.query('DELETE FROM auctions') - await conn.query('COMMIT'); - } - catch (error) { - console.error("InitDB Error: ", error); - if (conn) await conn.query('ROLLBACK'); - throw error; - } - finally { - if (conn) conn.release(); - } -} -//REMOVE ELEMENT IN AUCTIONS TABLE BY AUCTION-ID - public static async RemoveByAuctionId(auctionid: string|string[]) { - let conn: PoolClient | undefined; - try { - conn = await this.pool.connect(); - await conn.query('BEGIN'); - if(Array.isArray(auctionid)) - { - await conn.query('DELETE FROM auctions WHERE auctionid IN ($1);',[auctionid]); - } - else - { - await conn.query('DELETE FROM auctions WHERE auctionid = $1;',[auctionid]); - } - await conn.query('COMMIT'); - } - catch (error) { - console.error("InitDB Error: ", error); - if (conn) await conn.query('ROLLBACK'); - throw error; - } - finally { - if (conn) conn.release(); - } - } - - public static async Query(query:string,IsWriting:boolean = false) { - let conn: PoolClient | undefined; - let result: any; - try{ - conn = await this.pool.connect() - if(IsWriting) - { - await conn.query('BEGIN'); - conn.query(query); - await conn.query('COMMIT'); - } - else - { - console.log('TRYING TO QUERY'); - result = (await conn.query(query)).rows; - } - } - catch(error) { - console.error("InitDB Error: ", error); - if (IsWriting && conn) await conn.query('ROLLBACK'); - throw error; - } - finally - { - if(conn) conn.release(); - if(!IsWriting) return result; - } - } - ////////////////////////////////////////////////////////////////////////////////////////////////////// NOT FIXED / UNTESTED FUNCTIONS - //MATCH PROVIDED ELEMENTS IN AUCTIONS TABLE - returns true/false - public static async Match(auctionid:string|string[],lbin:number|number[]): Promise { - let conn: PoolClient | undefined; - let result; - try{ - conn = await this.pool.connect(); - if(Array.isArray(auctionid) && Array.isArray(lbin)) - if(auctionid.length === lbin.length) { - result = await conn.query(` - CREATE TEMPORARY TABLE IF NOT EXISTS TEMP ( - auctionID VARCHAR(255) PRIMARY KEY, - lbin DECIMAL(65,5) - ); - INSERT INTO TEMP (auctionID, lbin) VALUES ? - SELECT tmp.* - FROM TEMP AS tmp - WHERE NOT EXISTS ( - SELECT 1 - FROM auctions AS auc - WHERE auc.auctionID = tmp.auctionID - AND auc.lbin = tmp.lbin); - `,[await unifyArrays(auctionid,lbin)]); - return result.rows; - } - else if(Array.isArray(auctionid) || Array.isArray(lbin)) - throw Error(`Match SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); - else { - result = await conn.query(` - SELECT CASE - WHEN COUNT(*) > 0 - THEN true - ELSE false - END AS user_exists - FROM auctions WHERE auctionID = ?; - `,[auctionid]); - return Boolean(result.rows.length); - } - } - catch(error) - { - console.error("message_before_error: ", error); - throw error; - } - finally - { - if (conn) conn.release(); - } - } - //INSERT ELEMENT IN AUCTIONS TABLE OR UPDATE IF IT ALREADY EXISTS - public static async deprecated_Upsert(auctionid: string|string[], lbin: number|number[]) { - let conn: PoolClient | undefined; - try { - conn = await this.pool.connect(); - await conn.query('BEGIN'); - if(Array.isArray(auctionid) && Array.isArray(lbin)) - if(auctionid.length === lbin.length) { - await conn.query(` - INSERT INTO auctions (auctionid, LBin) - VALUES ($1,$2) - ON CONFLICT (auctionid) - DO UPDATE SET LBin = $2; - `, [auctionid, lbin]); - } - else if(Array.isArray(auctionid) || Array.isArray(lbin)) - throw Error(`Upsert SQL Function error - cannot unify collection ${Array.isArray(auctionid) ? auctionid : lbin} to singlet ${!Array.isArray(auctionid) ? auctionid : lbin}`); - else { - await conn.query(` - INSERT INTO auctions (auctionid, LBin) - VALUES ($1,$2) - ON CONFLICT (auctionid) - DO UPDATE SET LBin = $2; - `, [auctionid, lbin]); - } - await conn.query('COMMIT'); - } - catch(error) - { - console.error("message_before_error: ", error); - if (conn) await conn.query('ROLLBACK'); - throw error; - } - finally - { - if (conn) conn.release(); - } - } -} -export { - SqlSystem -}