feat: Filemagedon
Some checks failed
Build, check & Test / run (push) Failing after 1m45s
Lint / run (push) Failing after 48s
Build Docker Image / build_docker (push) Failing after 3m18s

Signed-off-by: Avior <git@avior.me>
This commit is contained in:
2024-09-11 14:38:58 +02:00
parent 3e91597dca
commit bc97d9106b
45 changed files with 4548 additions and 64 deletions

View File

@ -0,0 +1,324 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { objectFind, objectKeys, objectLoop, objectMap, objectRemap, objectValues } from '@dzeio/object-util'
import type Schema from 'libs/Schema'
import type { SchemaInfer } from 'libs/Schema'
import type SchemaItem from 'libs/Schema/SchemaItem'
import crypto from 'node:crypto'
import PostgresClient from '../Clients/PostgresClient'
import { Sort, type Query } from '../Query'
import { filter } from './AdapterUtils'
import type { DBPull } from './DaoAdapter'
import DaoAdapter from './DaoAdapter'
const specialKeywords = ['user', 'end'] as const
export default class PostgresAdapter<T extends Schema> extends DaoAdapter<T['model']> {
private id: Array<string> = []
public constructor(
/**
* the schema used by Cassandra
*/
public readonly schema: T,
/**
* the table name
*/
public readonly table: string,
/**
* additionnal options to make the adapter work
*/
private readonly options?: {
/**
* log the requests made to cassandra
*/
debug?: boolean
}
) {
super()
objectLoop(this.schema.model, (schema, key) => {
if (schema.attributes.includes('db:auto')) {
this.id.push(key)
}
})
}
// TODO: make it clearer what it does
public async create(obj: Partial<SchemaInfer<T>>): Promise<SchemaInfer<T> | null> {
// handle automated values
objectLoop(this.schema.model, (item, key) => {
if (item.attributes.includes('db:created') || item.attributes.includes('db:updated')) {
// @ts-expect-error things get validated anyway
obj[key] = new Date()
} else if (item.attributes.includes('db:auto') && !obj[key]) {
if (item.isOfType('')) {
// @ts-expect-error things get validated anyway
obj[key] = crypto.randomBytes(16).toString('hex')
} else if (item.isOfType(123)) {
// @ts-expect-error things get validated anyway
obj[key] = crypto.randomBytes(16).readUint32BE()
} else {
throw new Error('cannot generate ID because it is not compatible with it')
}
}
})
// parse the data with the Schema
const { object: clone, error} = this.schema.validate(obj)
if (error) {
console.error(error)
throw new Error('Invalid data given to create the final object')
}
// prepare the database query
const keys = objectKeys(clone)
.map((it) => {
if (specialKeywords.includes(it)) { // handle the special keyword
return `"${it}"`
}
return it
})
const keysStr = keys.join(', ')
const values = keys.map((_, idx) => `$${idx+1}`).join(', ')
const req = `INSERT INTO ${this.table} (${keysStr}) VALUES (${values});`
const client = await PostgresClient.get()
const params = objectMap(clone as any, (value, key) => this.valueToDB(key as any, value))
if (this.options?.debug) {
console.log(req, params) // 27 from 1 36 from 0
}
// send to the database
try {
await client.execute(req, params)
} catch (e) {
console.log(e, req, params)
return null
}
return this.schema.validate(clone).object ?? null
}
// eslint-disable-next-line complexity
public async read(query?: Query<SchemaInfer<T>> | undefined): Promise<DBPull<T>> {
// prepare the request to the database based on the query parameters
let req: Array<string> = ['SELECT', '*', 'FROM', this.table]
const client = await PostgresClient.get()
if (this.options?.debug) {
console.log(req)
}
// read from the database
let res: Array<Record<string, any>>
try {
res = await client.execute(`${req.join(' ')}`)
} catch (error) {
console.error('error running request')
console.error(req)
throw error
}
if (!res) {
return {
rows: 0,
pageTotal: 0,
page: 1,
rowsTotal: 0,
data: []
}
}
if (this.options?.debug) {
console.log('preEdits', res)
}
// post-process the data from the database
const raw = res
.map((obj) => {
// remap to use system value instead of db values
obj = objectRemap(this.schema.model, (_, key) => ({
key,
value: this.dbToValue(key as any, (obj as any)[key])
}))
// validate the schema
const res = this.schema.validate(obj)
if (res.object) {
return res.object
}
console.log(res.error)
return null
})
.filter((it): it is SchemaInfer<T> => !!it)
// temp modification of comportement to use the new and better query system
if ((!query || !query?.$sort) && objectFind(this.schema.model, (_, key) => key === 'created')) {
// temp fix for the sorting algorithm
if (!query) {
// @ts-expect-error normal currently
query = { $sort: { created: Sort.DESC }}
} else {
query.$sort = { created: Sort.DESC }
}
}
let dataset = raw
if (this.options?.debug) {
console.log('preFilters', dataset)
}
if (query) {
dataset = filter(query, dataset, this.options).filtered
}
return {
rows: dataset.length ?? 0,
rowsTotal: res.length ?? 0,
page: 1,
pageTotal: 1,
// page: page,
// pageTotal: pageLimit ? res.rowLength / pageLimit : 1,
data: dataset
}
}
public async update(obj: SchemaInfer<T>): Promise<SchemaInfer<T> | null> {
return this.patch(obj)
}
public async patch(id: Partial<SchemaInfer<T>>): Promise<SchemaInfer<T> | null>
public async patch(id: string, obj: Partial<SchemaInfer<T>>): Promise<SchemaInfer<T> | null>
// eslint-disable-next-line complexity
public async patch(id: string | Partial<SchemaInfer<T>>, obj?: Partial<SchemaInfer<T>>): Promise<SchemaInfer<T> | null> {
if (!obj) {
if (typeof id === 'string') {
return null
}
obj = {...id} as Partial<SchemaInfer<T>>
}
// const tmp = this.schema.validate(obj)
// // if (tmp.error) {
// // throw new Error(`obj invalid can\'t patch ${JSON.stringify(tmp.error)}`)
// // }
// obj = tmp.object
// update the updated time
objectLoop(this.schema.model, (item, key) => {
if (item.attributes.includes('db:updated')) {
// @ts-expect-error things get validated anyway
obj[key] = new Date()
}
})
// build the request parts
const parts: Array<string> = ['UPDATE', this.table, 'SET']
const params: Array<any> = []
// remove ids
for (const tmp of this.id) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete obj[tmp]
}
// map the items to update
const keys = objectMap(obj as {}, (_, key, idx) => {
if (specialKeywords.includes(key)) {
return `"${key}"=$${idx+1}`
}
return `${key}=$${idx+1}`
})
parts.push(keys.join(', '))
params.push(...objectValues(obj as {}))
// filter by the ids
parts.push('WHERE')
const read: Partial<any> = {}
for (let idx = 0; idx < this.id.length; idx++) {
const key = this.id[idx] as string
if (idx > 0) {
parts.push('AND')
}
parts.push(`${key}=$${params.length+1}`)
const value = obj[key] ?? (typeof id === 'string' ? id : id[key])
read[key] = this.valueToDB(key as any, value)
if (!value) {
throw new Error(`Missing id (${key})`)
}
params.push(value)
}
const req = parts.join(' ')
const client = await PostgresClient.get()
if (this.options?.debug) {
console.log(req, params)
}
try {
const res = await client!.execute(req, params)
// console.log(res, req)
if (this.options?.debug) {
console.log('post patch result', res, req)
}
return (await this.read(read)).data[0] ?? null
} catch (e) {
console.log(e, req, params)
}
return null
}
public async delete(obj: SchemaInfer<T>): Promise<boolean> {
const parts = ['DELETE', 'FROM', this.table, 'WHERE']
objectLoop(obj as {}, (value, key, idx) => {
if (idx > 0) {
parts.push('AND')
}
parts.push(`${key}=${value}`)
})
const client = await PostgresClient.get()
if (this.options?.debug) {}
try {
await client!.execute(`${parts.join(' ')}`)
} catch (e) {
console.error(e, parts)
throw e
}
return true
}
private valueToDB(key: keyof T, value: any): string | number | boolean | Date {
const item: SchemaItem<unknown> = (this.schema.model as any)[key]
if (item.isOfType({})) {
return JSON.stringify(value)
}
return value
}
private dbToValue(key: keyof T, value: string | number | boolean | Date): any {
const item: SchemaItem<unknown> = (this.schema.model as any)[key]
if (item.isOfType(543) && typeof value === 'string') {
return parseFloat(value)
}
if (item.isOfType({}) && typeof value === 'string') {
return JSON.parse(value)
}
return value
}
}