Add write handler and some locality writing

This commit is contained in:
ShootingStar91
2024-08-24 00:07:04 +03:00
parent c925c6fbcc
commit ca7f16a619
6 changed files with 170 additions and 40 deletions
+12 -23
View File
@@ -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<Array<{ [key: string]: string | number }>>(
`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()
}
+33
View File
@@ -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<LocalityDetailsType>) => {
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<LocalitySpecies>) => {
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 },
])
}
+1 -1
View File
@@ -22,7 +22,7 @@ const tableToUpdateTargets = {
now_ls: ['now_loc', 'com_species'],
} as Record<AllowedTables, PrimaryTables[] | undefined>
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'
@@ -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<T>(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<T>(`SELECT ${columnsString} FROM ${this.dbName}.${table} WHERE ${whereClause}`, [
idValues,
])
}
async executeQuery<T>(query: string, values?: Array<DbValue[]>) {
if (!this.connection) throw new Error('DB connection not initialized')
async executeQuery(query: string, values?: Array<DbValue[]>) {
logger.info(`Executing SQL query: ${query}`)
return (await this.connection.query(query, values)) as unknown
return await this.connection.query<T>(query, values)
}
}
@@ -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<string, unknown>) => {
const list: DbWriteItem[] = Object.entries(object)
.map(([field, value]) => ({
column: field,
value: value as DbValue,
}))
.filter(item => typeof item.value === 'object')
return list
}
@@ -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<T>(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<T>(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)
}
}