From ca7f16a6196f0cd6072fb381120260a3435cfe59 Mon Sep 17 00:00:00 2001 From: ShootingStar91 Date: Sat, 24 Aug 2024 00:07:04 +0300 Subject: [PATCH] Add write handler and some locality writing --- backend/src/services/write/delete.ts | 35 +++------ backend/src/services/write/locality.ts | 33 +++++++++ backend/src/services/write/writeUtils.ts | 2 +- .../writeOperations/databaseHandler.ts | 73 +++++++++++++++---- backend/src/services/writeOperations/utils.ts | 26 +++++++ .../services/writeOperations/writeHandler.ts | 41 +++++++++++ 6 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 backend/src/services/write/locality.ts create mode 100644 backend/src/services/writeOperations/utils.ts create mode 100644 backend/src/services/writeOperations/writeHandler.ts diff --git a/backend/src/services/write/delete.ts b/backend/src/services/write/delete.ts index dddcaa58..ac6a2f85 100644 --- a/backend/src/services/write/delete.ts +++ b/backend/src/services/write/delete.ts @@ -1,27 +1,16 @@ -import { PoolConnection } from 'mariadb' -import { AllowedTables } from './writeUtils' -import { NOW_DB_NAME } from '../../utils/config' import { pool } from '../../utils/db' -import { logger } from '../../utils/logger' - -const deleteFromTable = async ( - table: AllowedTables, - idColumn: string, - id: string | number, - returnColumns: string[], - connection: PoolConnection -) => { - return await connection.query>( - `DELETE FROM ${NOW_DB_NAME}.${table} WHERE ${idColumn} = ? RETURNING ${returnColumns.join(', ')}`, - [id] - ) -} +import { DatabaseHandler } from '../writeOperations/databaseHandler' export const deleteLocality = async (id: string | number) => { - const connection = await pool.getConnection() - const deletedLocalitySpecies = await deleteFromTable('now_ls', 'lid', id, ['lid', 'species_id'], connection) - logger.info(`Deleted following locality species: ${deletedLocalitySpecies.toString()}`) - await deleteFromTable('now_loc', 'lid', id, ['lid'], connection) - // TODO: Log, inside a transaction - await connection.end() + const dbHandler = new DatabaseHandler(await pool.getConnection()) + await dbHandler.start() + const deletedLocalitySpecies = await dbHandler.delete('now_ls', [{ column: 'lid', value: id }], []) + const deletedLocality = await dbHandler.delete('now_loc', [{ column: 'lid', value: id }]) + + // eslint-disable-next-line no-console + console.log({ deletedLocalitySpecies, deletedLocality }) + + // TODO: Log deletion + // TODO: clarify: Do we need to log all relations, for example now_coll_meth if lid is removed? + await dbHandler.end() } diff --git a/backend/src/services/write/locality.ts b/backend/src/services/write/locality.ts new file mode 100644 index 00000000..20c22c9f --- /dev/null +++ b/backend/src/services/write/locality.ts @@ -0,0 +1,33 @@ +import { EditDataType, LocalityDetailsType, LocalitySpecies } from '../../../../frontend/src/backendTypes' +import { NOW_DB_NAME } from '../../utils/config' +import { getItemList } from '../writeOperations/utils' +import { WriteHandler } from '../writeOperations/writeHandler' + +export const writeLocality = async (locality: EditDataType) => { + const isNew: boolean = !!locality.lid + const writeHandler = new WriteHandler(NOW_DB_NAME, 'now_loc') + await writeHandler.start() + for (const ls of locality.now_ls) { + if (ls.rowState === 'new') { + await writeLocalitySpecies(writeHandler, ls) + } else if (ls.rowState === 'removed') { + await deleteLocalitySpecies(writeHandler, ls.lid!, ls.species_id!) + } + } +} + +/* Writes now_ls entries, and also any new species inside. */ +const writeLocalitySpecies = async (writeHandler: WriteHandler, localitySpecies: EditDataType) => { + if (!localitySpecies.species_id) { + // const { species_id } = await writeHandler.writeTable('com_species', ) + } + const items = getItemList(localitySpecies) + return await writeHandler.writeTable('now_ls', items, [], ['lid', 'species_id']) +} + +const deleteLocalitySpecies = async (writeHandler: WriteHandler, lid: number, species_id: number) => { + return await writeHandler.delete('now_ls', [ + { column: 'lid', value: lid }, + { column: 'species_id', value: species_id }, + ]) +} diff --git a/backend/src/services/write/writeUtils.ts b/backend/src/services/write/writeUtils.ts index 4cc1fb98..23009b03 100644 --- a/backend/src/services/write/writeUtils.ts +++ b/backend/src/services/write/writeUtils.ts @@ -22,7 +22,7 @@ const tableToUpdateTargets = { now_ls: ['now_loc', 'com_species'], } as Record -type PrimaryTables = 'now_loc' | 'com_species' | 'now_time_unit' | 'now_tu_bound' +export type PrimaryTables = 'now_loc' | 'com_species' | 'now_time_unit' | 'now_tu_bound' export type ActionType = 'delete' | 'update' | 'add' diff --git a/backend/src/services/writeOperations/databaseHandler.ts b/backend/src/services/writeOperations/databaseHandler.ts index 402324cc..8ec0e7a7 100644 --- a/backend/src/services/writeOperations/databaseHandler.ts +++ b/backend/src/services/writeOperations/databaseHandler.ts @@ -1,40 +1,81 @@ import { PoolConnection } from 'mariadb' import { AllowedTables, DbValue } from '../write/writeUtils' import { logger } from '../../utils/logger' -import { NOW_DB_NAME } from '../../utils/config' +import { pool } from '../../utils/db' + +export type DbWriteItem = { column: string; value: DbValue } + +const createWhereClause = (idColumns: string[]) => `${idColumns.map(idColumn => `${idColumn} = ?`).join(' AND ')}` export class DatabaseHandler { - connection: PoolConnection - - constructor(connection: PoolConnection) { - this.connection = connection + connection?: PoolConnection + dbName: string + constructor(dbName: string) { + this.dbName = dbName } - async start() { + this.connection = await pool.getConnection() await this.connection.beginTransaction() } async end() { + if (!this.connection) throw new Error('DB connection not initialized') await this.connection.commit() await this.connection.end() } - async insert(table: AllowedTables, items: Array<{ column: string; value: DbValue }>, returnColumns?: string[]) { + getValuesAndColumns(items: DbWriteItem[]) { + return { values: items.map(item => item.value), columns: items.map(item => item.column) } + } + + async insert(table: AllowedTables, items: DbWriteItem[], returnColumns?: string[]) { const returnString = returnColumns ? `RETURNING ${returnColumns.join(', ')}` : '' - const columns = items.map(item => item.column) - const values = items.map(item => item.value) - return (await this.executeQuery(`INSERT INTO ${NOW_DB_NAME}.${table} (?) VALUES (?) ${returnString}`, [ + const { columns, values } = this.getValuesAndColumns(items) + + return await this.executeQuery(`INSERT INTO ${this.dbName}.${table} (?) VALUES (?) ${returnString}`, [ columns, values, - ])) as { - [key: string]: DbValue - }[] + ]) } - async insert(table: AllowedTables, items: Array<{ column: string; value: DbValue }>, returnColumns?: string[]) {} + async update(table: AllowedTables, items: DbWriteItem[], ids: DbWriteItem[]) { + const { columns, values } = this.getValuesAndColumns(items) + const columnsString = columns.join(', ') + + const { columns: idColumns, values: idValues } = this.getValuesAndColumns(ids) + const whereClause = `${idColumns.map(idColumn => `${idColumn} = ?`).join(' AND ')}` + + await this.executeQuery(`UPDATE ${this.dbName}.${table} (${columnsString}) WHERE ${whereClause}`, [ + values, + idValues, + ]) + } + + /* If return columns are not defined, returns the id columns given as argument. */ + async delete(table: AllowedTables, ids: DbWriteItem[], returnColumns?: string[]) { + const { columns: idColumns, values: idValues } = this.getValuesAndColumns(ids) + const whereClause = createWhereClause(idColumns) + const returningClause = returnColumns?.join(' ,') ?? `${idColumns.join(', ')}` + return await this.executeQuery( + `DELETE FROM ${this.dbName}.${table} WHERE ${whereClause} RETURNING ${returningClause}`, + [idValues] + ) + } + + async select(table: AllowedTables, columns: string[], ids: DbWriteItem[]) { + const columnsString = columns.length === 0 ? '*' : columns.map(col => `${this.dbName}.${col}`).join(', ') + const { columns: idColumns, values: idValues } = this.getValuesAndColumns(ids) + const whereClause = createWhereClause(idColumns) + return await this.executeQuery(`SELECT ${columnsString} FROM ${this.dbName}.${table} WHERE ${whereClause}`, [ + idValues, + ]) + } + + async executeQuery(query: string, values?: Array) { + if (!this.connection) throw new Error('DB connection not initialized') - async executeQuery(query: string, values?: Array) { logger.info(`Executing SQL query: ${query}`) - return (await this.connection.query(query, values)) as unknown + + return await this.connection.query(query, values) } } diff --git a/backend/src/services/writeOperations/utils.ts b/backend/src/services/writeOperations/utils.ts new file mode 100644 index 00000000..a9f2af15 --- /dev/null +++ b/backend/src/services/writeOperations/utils.ts @@ -0,0 +1,26 @@ +import { DbValue } from '../write/writeUtils' +import { DbWriteItem } from './databaseHandler' + +const isEmptyValue = (value: unknown) => { + if (value === null || value === undefined) return true + if (typeof value === 'string' && value === '') return true + return false +} + +export const valueIsDifferent = (newValue: unknown, oldValue: unknown) => { + if (newValue === oldValue) return false + if (isEmptyValue(newValue) && isEmptyValue(oldValue)) return false + if (typeof oldValue === 'bigint' && typeof newValue === 'number' && BigInt(newValue) === oldValue) return false + return true +} + +/* Takes in an object and returns only the non-object fields with their field name (column) and value */ +export const getItemList = (object: Record) => { + const list: DbWriteItem[] = Object.entries(object) + .map(([field, value]) => ({ + column: field, + value: value as DbValue, + })) + .filter(item => typeof item.value === 'object') + return list +} diff --git a/backend/src/services/writeOperations/writeHandler.ts b/backend/src/services/writeOperations/writeHandler.ts new file mode 100644 index 00000000..404ada00 --- /dev/null +++ b/backend/src/services/writeOperations/writeHandler.ts @@ -0,0 +1,41 @@ +import { AllowedTables, PrimaryTables, WriteItem } from '../write/writeUtils' +import { DatabaseHandler, DbWriteItem } from './databaseHandler' +import { valueIsDifferent } from './utils' + +/* Handles writing logic that are agnostic of datatype, but not directly operations to db. */ +export class WriteHandler extends DatabaseHandler { + table: PrimaryTables + writeList: WriteItem[] = [] + + constructor(dbName: string, table: PrimaryTables) { + super(dbName) + this.table = table + } + + /* Checks each field for their old value and if they did not (meaningfully) change, + they are skipped, otherwise they are written to database. + table: Table to write to + items: Column & value pairs that should be written + ids: Column & value pairs that define which item to write to. Empty if new item. */ + async writeTable(table: AllowedTables, items: DbWriteItem[], ids: DbWriteItem[], idsToReturn: string[]) { + if (ids.length === 0) { + this.writeList.push({ table, type: 'add', items: items.map(item => ({ ...item, table })) }) + return await super.insert(table, items, idsToReturn) + } + + const oldObject = await super.select(table, [], ids) + const fieldsToWrite: DbWriteItem[] = [] + + for (const { column, value } of items) { + const oldValue = oldObject[column as keyof T] + + if (!valueIsDifferent(value, oldValue)) continue + + fieldsToWrite.push({ column, value }) + } + + this.writeList.push({ table, type: 'update', items: items.map(item => ({ ...item, table })) }) + + return await super.update(table, fieldsToWrite, ids) + } +}