import knex, { Knex } from "knex" import { ModelTemplate } from "./models" import schemaInspector from 'knex-schema-inspector'; import { randomUUID } from "node:crypto" import moment from "moment"; export namespace Internals { export function context(c: T): T extends undefined ? void : T { const context = this.queryContext() ?? Object.create(null) if (context._userContext === undefined) { context._userContext = Object.create(null) } if (c === undefined) { return context._userContext } context._userContext = c return this.queryContext(context) } export function cleanStringToComment(content: string): string { return content .normalize('NFD') .replace(/\*|\-|\@|\?|[\u0300-\u036f]/g, '') .toUpperCase() ; } export function compileComments(comments: Record): string | null { const keysNumber = Object.keys(comments).length if (keysNumber === 0) { return null } const stringCommentBuilder = new Array() let i = 0 for (const key in comments) { if (!Object.hasOwn(comments, key)) continue const newKey = Internals.cleanStringToComment(key) const content = Internals.cleanStringToComment(comments[key]) stringCommentBuilder.push(`${newKey}: (${content})`) } return stringCommentBuilder.join(' ') } export function includeOne(table: string, type: Sql.Joins,s: Knex.JoinCallback) { const t: Knex.QueryBuilder = this const context = t.queryContext() ?? Object.create(null) if (context._joins === undefined) { context._joins = [] } context._joins.push(table) const q = t.queryContext(context) switch(type) { case 'CROSS JOIN': return q.crossJoin(table, s) break case 'FULL OUTER JOIN': return q.fullOuterJoin(table, s) break case 'INNER JOIN': return q.innerJoin(table, s) break case 'JOIN': return q.join(table, s) break case 'LEFT JOIN': return q.leftJoin(table, s) break case 'LEFT OUTER JOIN': return q.leftOuterJoin(table, s) break case 'OUTER JOIN': return q.outerJoin(table, s) break case 'RIGHT JOIN': return q.rightJoin(table, s) break } } export function getConnTable(this: Knex.QueryBuilder, table: string, comments?: Record) { let conn = this.table(table) if (comments !== undefined && conn.comment !== undefined) { const stringComment = Internals.compileComments(comments) if (stringComment !== null) { conn = conn.comment(stringComment) } } return conn } } export class Driver implements PlataClosable { public readonly config: Knex.Config public readonly inTransaction: InTransaction public readonly conn: InTransaction extends true ? Knex.Transaction : Knex public readonly trxConn: InTransaction extends true ? Knex.Transaction : Knex public readonly cacheTable: Map public readonly logQuerys: boolean public readonly trxName: string | undefined constructor(config: Knex.Config, inTransaction: InTransaction, trx?: Knex.Transaction, cacheTable?: Map) { this.inTransaction = inTransaction this.config = config this.logQuerys = Plata.config.ENV === 'debug' || Plata.config._PLATA_SQL_SHOWQUERY === '1' if (!this.inTransaction) { if (config.pool === undefined) config.pool = { min: 1, max: 1 } ; if (!Plata._knexExtended) { knex.QueryBuilder.extend('context', Internals.context) knex.QueryBuilder.extend('includeOne', Internals.includeOne) knex.QueryBuilder.extend('getConnTable', Internals.getConnTable) Plata._knexExtended = true } const trxPool = { min: 1, max: 1 } if (Plata.config.__PLATA_SQL_TRX_MIN_POOL) { trxPool.min = +Plata.config.__PLATA_SQL_TRX_MIN_POOL } if (Plata.config.__PLATA_SQL_TRX_MAX_POOL) { trxPool.max = +Plata.config.__PLATA_SQL_TRX_MAX_POOL } this.conn = knex(config) as any this.trxConn = knex({ ...config, pool: trxPool }) as any } else { this.conn = trx as any this.trxConn = trx as any } this.trxName = this.conn?.userParams?.trxName if (this.logQuerys) { this.conn.on('query', (...args) => console.log(...args)) this.conn.on('query-error', (...args) => console.error(...args)) } this.cacheTable = cacheTable ?? new Map() } public async close(): Promise { if (this.inTransaction !== true) { await Promise.allSettled([ this.conn.destroy().catch((...err) => console.error('[PLATA-SQL] [AUTO-CLOSE-CONN]: ', JSON.stringify(err))), this.trxConn.destroy().catch((...err) => console.error('[PLATA-SQL] [AUTO-CLOSE-TRX]: ', JSON.stringify(err))), ]) } } public async onOpenTrx(trx: Driver): PlataPromise> { return trx } private compileCommentsIntoSql(sql: string, comments: Record): string { if (this.trxName !== undefined) { comments.TRX = this.trxName } const stringComment = Internals.compileComments(comments) if (stringComment === null) { return sql } return `/* ${stringComment} */${sql}` } public runRawWithComment(query: { sql: string, bindings: readonly any[] }, comments?: Record) { return this.conn.raw( this.compileCommentsIntoSql(query.sql, comments ?? Object.create(null)), query.bindings ) } public select>(table: string, model: ModelTemplate, options?: Sql.SelectOptions) { return { build: (builder: Sql.Builder) => new Query( builder(this.conn.queryBuilder().table(table)), model, table, this, options, ) , } } public async rawMappedSelect, T>(queryInfo: I): PlataPromise[]> { let queryBuilder = this.conn.queryBuilder().from(queryInfo.table) const validades = new Set() for (const col in queryInfo.cols) { queryBuilder = queryInfo.cols[col][0](queryBuilder as any) as any if (queryInfo.cols[col][2] === 'required') { validades.add(col) } } const sql = queryInfo.query(queryBuilder as any, {} as any).toSQL() const result: PlataResult = await this.runRawWithComment(sql, queryInfo.comments).then( r => r, err => ({ errorID: 'PLSQLRWSEL001', msg: 'Unexpected error while select', error:{ message: err?.toString(), stack: new Error().stack }, }) ) if (result.errorID !== undefined) { return result } if (validades.size !== 0) { for (let i = 0; i < result.length; i++) { const row = result[i] for (const col of validades) { if (row[col] === null || row[col] === undefined) { return { errorID: 'PLSQLRWSEL002', msg: 'Not valid select', error:{ message: `${col} is NULL`, stack: new Error().stack }, } } } } } return result } public async insert>(table: string, model: ModelTemplate, values: Model.ParcialTemplateType[], options?: Sql.InsertOptions): PlataPromise[]> { if (this.config.client === 'mssql') { const sql = this.conn.queryBuilder().table(table).insert( values, '*', { includeTriggerModifications: options?.ignoreTrigger === undefined } ).toSQL() return this.runRawWithComment(sql, options?.comments).then( r => r, err => Plata.BuildPlataError({ errorID: 'PLSQL001', msg: 'Unexpected error while inserting', error:{ message: err?.toString(), stack: new Error().stack }, }) ) } const comments: Record = options?.comments ?? Object.create(null) if (this.trxName !== undefined) { comments.TRX = this.trxName } // TODO ver a necessidade de comentario de trx nas outras conexões if (options?.ignoreTrigger === true) return this.conn.queryBuilder().getConnTable(table, comments).insert( values, '*', { includeTriggerModifications: options?.ignoreTrigger } ).then( r => r, err => Plata.BuildPlataError({ errorID: 'PLSQLINS001', msg: 'Unexpected error while inserting', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack }, }) ) ; const key: PlataResult = await schemaInspector(this.conn).primary(table).then( r => { if (r === null) { return Plata.BuildPlataError({ errorID: 'PLSQL007', msg: 'Tabela não tem chave primária' }) } return r }, err => Plata.BuildPlataError({ errorID: 'PLSQL006', msg: 'Erro ao consultar a chave primária da tabela', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) ) if (key.errorID !== undefined) { return key } return this.transaction( async (trx) => { if (comments.TRX !== undefined) { comments.TRX = `${trx.trxName}<->${this.trxName}` } else { comments.TRX_INSERT = trx.trxName ?? '' } const result: number[] = await trx.conn.queryBuilder().getConnTable(table, comments).insert( values, ) return trx.conn.select('*').from(table).whereIn(key, result) } ) } public update>(table: string, model: ModelTemplate, options?: Sql.UpdateOptions) { return { build: async (s: Sql.Builder, set: Model.ParcialTemplateType): PlataPromise => { const sql = s(this.conn.queryBuilder().table(table)).update(set as any).toSQL() return this.runRawWithComment(sql, options?.comments).then( () => true, err => Plata.BuildPlataError({ errorID: 'PLSQL002', msg: 'Unexpected error while update', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) ) }, } } public log(text: string, ...extras: any[]) { if (this.logQuerys === true) { console.log(text, ...extras) } } /** @deprecated */ public trasation(transaction: Sql.TransactionFunction, T>, config?: Knex.TransactionConfig) { return this.conn.transaction(async trx => { const transactionDriver = new Driver(this.config, true, trx, this.cacheTable) const result = await transaction(transactionDriver) return result }, config).then( r => r, err => { if (!err) { err = Object.create(null) } err.originalError = undefined return Plata.BuildPlataError({ errorID: 'PLSQL003', msg: 'Unexpected error in transation', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack }, }) } ) } public async getTransactionDriver(config?: Knex.TransactionConfig): PlataPromise> { const trxName = randomUUID() const provider = await this.trxConn.transactionProvider({ ...config, doNotRejectOnRollback: true, }) this.log(`[PLATA-SQL] [TRX-${trxName}] - Inciando provider`) // Inicia a transação const trx: PlataResult = await provider().catch( e => ({ errorID: 'PLSQLGETTRX001', msg: 'Erro ao iniciar a transação', error: { message: e?.toString(), stack: e?.stack ?? new Error().stack, trxName } }) ) if (trx.errorID !== undefined) { this.log(`[PLATA-SQL] [TRX-${trxName}] - Conexão não foi aberta - ${JSON.stringify(trx)}`) return trx } trx.userParams.trxName = trxName this.log(`[PLATA-SQL] [TRX-${trxName}] - Conexão foi aberta`) return this.onOpenTrx(new Driver(this.config, true, trx, this.cacheTable)) } public async transaction(transaction: Sql.TransactionResultFunction, T>, config?: Knex.TransactionConfig): PlataPromise { const driver = await this.getTransactionDriver(config) if (driver.errorID !== undefined) { return driver } const transPromise = Plata.FastPromise(() => driver.conn.executionPromise.catch((...args) => console.log(...args))) try { const resultTransation = await transaction(driver) if (resultTransation.errorID !== undefined) { this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Roollback -> Erro da API na transação - ${JSON.stringify(resultTransation)}`) await driver.conn.rollback().catch((...err) => console.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}]`, ...err)) await transPromise this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Roollback realizado`) return resultTransation } this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Commit -> Realizando Commit`) const resultCommit: PlataResult = await driver.conn.commit().then( () => true, err => ({ errorID: 'PLSQLTRX003', msg: 'Erro inesperado ao realizar o commit', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) ) if (resultCommit.errorID !== undefined) { this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Rollback -> Erro no commit, realizando Rollback`) await driver.conn.rollback().catch((...err) => console.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}]`, ...err)) await transPromise this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Roollback realizado`) return resultCommit } await transPromise this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Commit -> Commit Realizado `) return resultTransation } catch (err) { this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Rollback -> Erro inesperado: `, err) await driver.conn.rollback().catch((...err) => console.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}]`, ...err)) await transPromise this.log(`[PLATA-SQL] [TRX-${driver.conn.userParams?.trxName}] - Rollback -> Roollback realizado`) return { errorID: 'PLSQLTRX002', msg: 'Erro inesperado ao realizar a transação ', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } } } } public async getColumns(table: string): PlataPromise> { if (Plata.config._PLATA_SQL_CACHETABLE !== '1') return this.getColumsBanco(table) ; const cache = this.cacheTable.get(table) if (cache === undefined) { const result = await this.getColumsBanco(table) if (result.errorID !== undefined) { return result } this.cacheTable.set(table, { coluns: result, date: new Date() }) return result } const ttl = (+(Plata.config._PLATA_SQL_CACHETABLETIME || 60) || 60) * 60 * 1000 if ((new Date().getTime() - cache.date.getTime()) > ttl) { const result = await this.getColumsBanco(table) if (result.errorID !== undefined) { return result } this.cacheTable.set(table, { coluns: result, date: new Date() }) return result } return cache.coluns } public async getColumsBanco(table: string): PlataPromise> { return this.conn(table).columnInfo().catch( err => Plata.BuildPlataError({ errorID: 'PLSQL004', msg: 'Erro ao consultar a estrutura da tabela', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) ) } private getDefaultValue(type: string, value: any | null): any { if (value !== null) { return value } switch (type) { case 'image': return '' case 'int': case 'integer': return 0 case 'datetime2': return null case 'decimal': return 0 case 'varbinary': return '' case 'text': return '' case 'smallint': return 0 case 'datetimeoffset': return 0 case 'varchar': return '' case 'binary': return 0 case 'datetime': return null case 'time': return null case 'numeric': return 0 case 'uniqueidentifier': return 0 case 'tinyint': return 0 case 'nchar': return '' case 'smalldatetime': return null case 'float': return 0 case 'date': return null case 'char': return '' case 'bigint': return 0 case 'nvarchar': return '' case 'sysname': return '' case 'bit': return 0 default: return '' } } public async autoInsert>(table: string, model: ModelTemplate, values: (Model.ParcialTemplateType)[], ignore: StringKeyOfObject[], options?: Sql.InsertOptions): PlataPromise[]> { const columns = await this.getColumns(table) if (columns.errorID !== undefined) { return columns } const newValues: any[] = [] for (let i = 0; i < values.length; i++) { const v = Object.create(null) for (const key in values[i]) { v[key.toUpperCase()] = values[i][key] } const insertValues = Object.create(null) for (const key in columns) { const n = key.toUpperCase() if (ignore.find(c => c.toString().toUpperCase() === n) !== undefined) { continue } const value = this.getDefaultValue( columns[key].type.toLowerCase(), v[n] ?? null ) if (value === null) { continue } insertValues[n] = value } newValues.push(insertValues) } const inserted: PlataResult = await this.insert( table, {} as any, newValues, options, ) if (inserted.errorID !== undefined) { return inserted } const insertedRows: any[] = [] for (let i = 0; i < inserted.length; i++) { const item = inserted[i]; const insertedRow = Object.create(null) for (const key in item) { insertedRow[key.toUpperCase()] = item[key] } insertedRows.push(insertedRow) } return insertedRows } public async now(): PlataPromise { const query = this.conn.select(this.conn.fn.now()).toSQL() console.log(query) const result: PlataResult<{ CURRENT_TIMESTAMP: string }[]> = await this.runRawWithComment({ bindings: query.bindings, sql: query.sql, }).catch(err => ({ errorID: 'PLSQLNOW001', msg: 'Erro ao conectar no banco', error: err })) if (result.errorID !== undefined) { return result } if (result.length !== 1) { return { errorID: 'PLSQLNOW002', msg: 'INESPERADO: knex.fn.now não retornou nenhuma linha', error: query } } return new Date(result[0].CURRENT_TIMESTAMP) } } export class Query> { public query: { sql: string, bindings: readonly any[] } public readonly model: ModelTemplate public readonly table: string public readonly joins: string[] private readonly options: Sql.SelectOptions | undefined private readonly conn: Driver constructor( query: Sql.ModelQueryBuilder, model: ModelTemplate, table: string, conn: Driver, options?: Sql.SelectOptions ) { const context = query.queryContext() ?? Object.create(null) this.joins = context._joins ?? [] this.table = table this.model = model this.query = this._buildSelect(query, this.model.template) this.conn = conn this.options = options } private _buildSelect(query: Sql.ModelQueryBuilder, template: T): { sql: string, bindings: readonly any[] } { const cs: string[] = [] for (const key in template) { if (!Array.isArray(template[key])) { continue } if (Array.isArray(template[key][0])) { continue } if (typeof template[key][0] === 'function') { cs.push(`${this.table}.${key} as ${this.table}_${key}`) continue } if (!this.joins.includes(key)) { continue } for (const column in template[key][0]) { cs.push(`${key}.${column} as ${key}_${column}`) } } return query.select(...cs).toSQL() } private _buildModel(row: any): typeof this.model.type { const template = this.model.template const r: typeof this.model.type = Object.create(null) as any for (const key in template) { if (!Array.isArray(template[key])) { continue } if (Array.isArray(template[key][0])) { r[key] = [] as any continue } if (typeof template[key][0] === 'function') { r[key] = row[`${this.table}_${key}`] ?? null continue } if (!this.joins.includes(key)) { r[key] = [] as any continue } r[key] = [Object.create(null)] as any for (const column in template[key][0]) { r[key][0][column] = row[`${key}_${column}`] ?? null } } return r } public _handleStream(handler: Sql.StreamHandler): PlataPromise { return new Promise(resolve => { let shoudContinue = true const stream = this.conn.runRawWithComment(this.query, this.options?.comments).stream() stream.on('close', async () => { return resolve(handler.close()) }) stream.on('data', async row => { if (shoudContinue) { shoudContinue = await handler.row(this._buildModel(row)) } else { stream.pause() } }) stream.on('pause', () => { stream.destroy() }) stream.on('error', async err => { shoudContinue = false if (handler.error !== undefined) await handler.error(err) return resolve({ errorID: 'PLSQLQ001', msg: 'Unexpected error in query', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) }) }) } private breakInfo(r: unknown[] | unknown[][]): unknown[] { if (r.length === 0) { return [] } if (Array.isArray(r[0])) { return r[0] } return r } public async toListAsync(): Sql.ModelList { const errors: PlataError[] = [] const itens: any[] = [] const result: PlataResult = await this.conn.runRawWithComment(this.query, this.options?.comments).then( r => { const promises: Promise[] = [] const rows = this.breakInfo(r) for (let i = 0; i < rows.length; i++) { promises.push(Plata.FastPromise(async () => { const v = await this.model.validate(this._buildModel(rows[i])) if (v.errors !== undefined) { errors.push(...v.errors) } itens.push(v.value) return true })) } return Promise.all(promises) }, err => Plata.BuildPlataError({ errorID: 'PLSQLQ005', msg: 'Erro ao execultar a query', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } }) ) if (result.errorID !== undefined) { return result } if (errors.length !== 0) { return { errors, itens, } } return { errors: undefined, itens, } } public async eachRowAsync(c: Sql.EachRowCallBack): PlataPromise { const promises: Promise[] = [] let shoudContinue = true let error: PlataError | undefined = undefined return this._handleStream({ row: async row => { promises.push(Plata.FastPromise(() => this.model.validate(row)) .then( lineResult => { c(lineResult).then(r => { if (r.errorID !== undefined) { shoudContinue = false error = r return } shoudContinue = shoudContinue ? r : shoudContinue }) } ) .catch(err => { shoudContinue = false error = { errorID: 'PLSQLQ003', msg: 'Unexpected error in callback', error: { message: err?.toString(), stack: err?.stack ?? new Error().stack } } }) ) return shoudContinue }, close: async (): PlataPromise => { return Promise.all(promises).then( () => error ?? true, err => Plata.BuildPlataError({ errorID: 'PLSQLQ004', msg: 'Unexpected Error in select', error: err }) ) } }) } }