mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Add write handler and some locality writing
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user