zet-lib 6.0.2 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/connection.js CHANGED
@@ -2,613 +2,680 @@ const { Pool } = require('./Pool')
2
2
  const config = require('dotenv').config()
3
3
  const Util = require('./Util')
4
4
 
5
- const configPG = {
5
+ const pool = new Pool({
6
6
  user: process.env.PGUSER,
7
7
  host: process.env.PGHOST,
8
8
  database: process.env.PGDATABASE,
9
9
  password: process.env.PGPASSWORD,
10
10
  port: process.env.PGPORT,
11
- max: 20, // set pool max size to 20
12
- idleTimeoutMillis: 1000, // close idle clients after 1 second
13
- connectionTimeoutMillis: 10000, // return an error after 10 second if connection could not be established
14
- maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
15
- poolSize: 50, // Default is 10 connections
16
- }
17
-
18
- const pool = new Pool(configPG)
11
+ max: 20,
12
+ idleTimeoutMillis: 1000,
13
+ connectionTimeoutMillis: 10000,
14
+ maxUses: 7500,
15
+ poolSize: 50,
16
+ })
19
17
 
20
18
  pool.on('error', (err) => {
21
19
  console.error('Unexpected error on idle client', err)
22
20
  })
23
21
 
24
- // Validasi identifier (nama tabel) untuk mencegah SQL injection
22
+ const SAFE_IDENTIFIER_PART = /^[a-zA-Z_][a-zA-Z0-9_]*$/
25
23
  const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?$/
26
- const sanitizeTableName = (table) => {
27
- if (typeof table !== 'string' || !table.trim()) return ''
28
- const t = table.trim()
29
- if (!SAFE_IDENTIFIER.test(t)) {
30
- throw new Error(`Invalid table name: ${t}`)
24
+ const SAFE_CAST = /^[a-z][a-z0-9_]*(\(\d+\))?$/
25
+ const SAFE_RETURNING = /^RETURNING \*$/i
26
+ const SAFE_RETURNING_COLS = /^RETURNING ([a-zA-Z_][a-zA-Z0-9_]*)(,\s*[a-zA-Z_][a-zA-Z0-9_]*)*$/i
27
+ const META_WHERE_KEYS = new Set(['type', 'cast'])
28
+ const JOIN_TYPES = new Set(['LEFT', 'RIGHT', 'INNER', 'FULL', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'CROSS'])
29
+ const OPERATOR_RE = '(>=|<=|<>|!=|=|>|<|NOT\\s+ILIKE|NOT\\s+LIKE|ILIKE|LIKE|NOT\\s+IN|IN|NOT\\s+BETWEEN|BETWEEN|IS\\s+NOT\\s+NULL|IS\\s+NULL|IS\\s+NOT|IS|@>)'
30
+ const STRING_OPERATOR_RE = new RegExp(`^${OPERATOR_RE}\\s+([\\s\\S]+)$`, 'i')
31
+ const KEY_OPERATOR_RE = new RegExp(`^(.+?)\\s+${OPERATOR_RE}$`, 'i')
32
+
33
+ const OPERATOR_NAME_MAP = {
34
+ '=': '=', EQ: '=',
35
+ '!=': '<>', '<>': '<>', NE: '<>',
36
+ '<': '<', LT: '<',
37
+ '>': '>', GT: '>',
38
+ '<=': '<=', LTE: '<=',
39
+ '>=': '>=', GTE: '>=',
40
+ LIKE: 'LIKE', ILIKE: 'ILIKE',
41
+ 'NOT LIKE': 'NOT LIKE', NOTLIKE: 'NOT LIKE',
42
+ 'NOT ILIKE': 'NOT ILIKE', NOTILIKE: 'NOT ILIKE',
43
+ IN: 'IN', 'NOT IN': 'NOT IN', NOTIN: 'NOT IN',
44
+ BETWEEN: 'BETWEEN', 'NOT BETWEEN': 'NOT BETWEEN', NOTBETWEEN: 'NOT BETWEEN',
45
+ IS: 'IS', 'IS NOT': 'IS NOT', ISNOT: 'IS NOT',
46
+ 'IS NULL': 'IS NULL', ISNULL: 'IS NULL',
47
+ 'IS NOT NULL': 'IS NOT NULL', ISNOTNULL: 'IS NOT NULL',
48
+ 'NOT NULL': 'IS NOT NULL', NOTNULL: 'IS NOT NULL',
49
+ '@>': '@>', CONTAINS: '@>',
50
+ }
51
+
52
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
53
+ const asArray = (val) => (Array.isArray(val) ? val : [])
54
+ const isPlainObject = (val) => !!val && typeof val === 'object' && !Array.isArray(val)
55
+ const rowsOf = (result) => (result && Array.isArray(result.rows) ? result.rows : [])
56
+
57
+ const logQueryError = (sql, params, err) => {
58
+ console.error(sql)
59
+ if (params !== undefined) console.error(params)
60
+ console.error(err && err.toString ? err.toString() : err)
61
+ }
62
+
63
+ const runQuery = async (sql, params) => {
64
+ const values = Array.isArray(params) && params.length ? params : undefined
65
+ try {
66
+ return await pool.query(sql, values)
67
+ } catch (err) {
68
+ logQueryError(sql, params, err)
69
+ throw err
31
70
  }
32
- return t
33
71
  }
34
72
 
35
- // Validasi limit/offset sebagai integer non-negatif
36
73
  const toSafeNonNegativeInt = (val, fallback) => {
37
- if (val === undefined || val === null) return fallback
74
+ if (val === undefined || val === null || val === '') return fallback
38
75
  const n = parseInt(val, 10)
39
- if (!Number.isInteger(n) || n < 0) return fallback
40
- return n
76
+ return Number.isInteger(n) && n >= 0 ? n : fallback
41
77
  }
42
78
 
43
- // Validasi satu segmen ORDER BY (identifier + optional ASC/DESC) untuk mencegah SQL injection
44
- const SAFE_IDENTIFIER_PART = /^[a-zA-Z_][a-zA-Z0-9_]*$/
45
- const sanitizeOrderBySegment = (segment) => {
46
- if (typeof segment !== 'string' || !segment.trim()) return ''
47
- const parts = segment.trim().split(/\s+/)
48
- const ident = parts[0]
49
- const dir = parts[1] ? parts[1].toUpperCase() : ''
50
- if (dir && dir !== 'ASC' && dir !== 'DESC') return ''
51
- const identParts = ident.split('.')
52
- const valid = identParts.length > 0 && identParts.every(p => SAFE_IDENTIFIER_PART.test(p))
53
- if (!valid) return ''
54
- const quotedIdent = identParts.map(p => `"${p}"`).join('.')
55
- return dir ? `${quotedIdent} ${dir}` : quotedIdent
79
+ const quoteIdent = (name) => {
80
+ if (name == null || typeof name !== 'string' || !name.trim()) return ''
81
+ const parts = name.trim().split('.')
82
+ if (!parts.length || !parts.every((part) => SAFE_IDENTIFIER_PART.test(part))) return ''
83
+ return parts.map((part) => `"${part}"`).join('.')
56
84
  }
57
85
 
58
- // Validasi nama kolom (identifier tunggal) untuk INSERT/UPDATE
59
- const sanitizeColumnName = (key) => SAFE_IDENTIFIER_PART.test(String(key).trim()) ? String(key).trim() : null
86
+ const sanitizeTableName = (table) => {
87
+ if (typeof table !== 'string' || !table.trim()) return ''
88
+ const t = table.trim()
89
+ return SAFE_IDENTIFIER.test(t) ? t : ''
90
+ }
91
+
92
+ const quoteTableName = (table) => {
93
+ const t = sanitizeTableName(table)
94
+ return t ? t.split('.').map((part) => `"${part}"`).join('.') : ''
95
+ }
96
+
97
+ const requireTableName = (table, action) => {
98
+ if (typeof table === 'string' && table.trim() && !SAFE_IDENTIFIER.test(table.trim())) {
99
+ throw new Error(`Invalid table name: ${table.trim()}`)
100
+ }
101
+ const quoted = quoteTableName(table)
102
+ if (!quoted) throw new Error(`Table name is required${action ? ` for ${action}` : ''}`)
103
+ return quoted
104
+ }
105
+
106
+ const sanitizeColumnName = (key) => {
107
+ const col = String(key == null ? '' : key).trim()
108
+ return SAFE_IDENTIFIER_PART.test(col) ? col : null
109
+ }
60
110
 
61
- // Validasi klausa RETURNING: hanya RETURNING * atau RETURNING col1, col2, ...
62
- const SAFE_RETURNING = /^RETURNING \*$/i
63
- const SAFE_RETURNING_COLS = /^RETURNING ([a-zA-Z_][a-zA-Z0-9_]*)(,\s*[a-zA-Z_][a-zA-Z0-9_]*)*$/i
64
111
  const sanitizeReturning = (clause) => {
65
112
  if (typeof clause !== 'string' || !clause.trim()) return 'RETURNING *'
66
113
  const s = clause.trim()
67
- if (SAFE_RETURNING.test(s)) return s
68
- if (SAFE_RETURNING_COLS.test(s)) return s
69
- return 'RETURNING *'
114
+ return (SAFE_RETURNING.test(s) || SAFE_RETURNING_COLS.test(s)) ? s : 'RETURNING *'
70
115
  }
71
116
 
72
- // Validasi field di WHERE (identifier atau schema.table)
73
- const sanitizeWhereField = (field) => {
74
- if (field == null || typeof field !== 'string' || !field.trim()) return ''
75
- const identParts = String(field).trim().split('.')
76
- const valid = identParts.length > 0 && identParts.every(p => SAFE_IDENTIFIER_PART.test(p))
77
- if (!valid) return ''
78
- return identParts.map(p => `"${p}"`).join('.')
117
+ const normalizeOperator = (op) => {
118
+ if (op == null) return ''
119
+ const key = String(op).trim().replace(/_/g, ' ').replace(/\s+/g, ' ').toUpperCase()
120
+ return OPERATOR_NAME_MAP[key] || ''
79
121
  }
80
122
 
81
- // Whitelist operator perbandingan untuk WHERE
82
- const SAFE_WHERE_OPERATORS = new Set(['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'ILIKE', 'IS NOT', 'IS'])
83
- const sanitizeWhereOption = (option) => {
84
- if (option == null || typeof option !== 'string') return ''
85
- const s = String(option).trim().toUpperCase()
86
- return SAFE_WHERE_OPERATORS.has(s) ? s : ''
87
- }
123
+ const sanitizeWhereOption = (option) => (typeof option === 'string' ? normalizeOperator(option) : '')
88
124
 
89
125
  const sanitizeWhereConnector = (op) => {
90
126
  if (op == null || typeof op !== 'string') return ' AND '
91
- const s = String(op).trim().toUpperCase()
92
- return (s === 'OR' ? ' OR ' : ' AND ')
127
+ return String(op).trim().toUpperCase() === 'OR' ? ' OR ' : ' AND '
93
128
  }
94
129
 
95
- const connection = {}
130
+ const sanitizeOrderBySegment = (segment) => {
131
+ if (typeof segment !== 'string' || !segment.trim()) return ''
132
+ const parts = segment.trim().split(/\s+/)
133
+ const ident = quoteIdent(parts[0])
134
+ if (!ident) return ''
135
+ const dir = parts[1] ? parts[1].toUpperCase() : ''
136
+ if (dir && dir !== 'ASC' && dir !== 'DESC') return ''
137
+ return dir ? `${ident} ${dir}` : ident
138
+ }
96
139
 
97
- connection.query = async (string, arr) => {
140
+ const escapeLiteral = (value) => String(value == null ? '' : value).replace(/'/g, "''")
141
+
142
+ const schemaSql = (table) => {
143
+ const t = sanitizeTableName(table)
144
+ if (!t) return ''
145
+ const name = t.includes('.') ? t.split('.').pop() : t
146
+ return `'${escapeLiteral(name)}'`
147
+ }
148
+
149
+ const safeJson = (value) => {
150
+ if (typeof value === 'string') return value
98
151
  try {
99
- /*console.log(string);
100
- console.log(JSON.stringify(arr))*/
101
- const result = await pool.query(string, arr)
102
- return result.rows
103
- } catch (e) {
104
- console.log(string)
105
- console.log(e)
152
+ return JSON.stringify(value)
153
+ } catch (err) {
154
+ throw new Error('Invalid JSON value for query parameter')
106
155
  }
107
156
  }
108
157
 
109
- const orderByFn = (obj) => {
110
- const objOrderby = !obj.orderBy ? [] : obj.orderBy
111
- let orderBy = ''
112
- if (objOrderby.length) {
113
- orderBy = ` ORDER BY `
114
- for (var i = 0; i < objOrderby.length; i++) {
115
- if (i % 2 == 0) {
116
- orderBy += ` "${obj.orderBy[i]}" `
117
- } else {
118
- orderBy += ` ${obj.orderBy[i]} `
119
- if (i == objOrderby.length - 1) {
120
- orderBy += ` `
121
- } else {
122
- orderBy += `, `
123
- }
124
- }
158
+ const stripQuotes = (value) => String(value == null ? '' : value).trim().replace(/^['"]|['"]$/g, '')
159
+
160
+ const parseListLiteral = (raw) => {
161
+ if (Array.isArray(raw)) return raw
162
+ const s = String(raw == null ? '' : raw).trim().replace(/^\(|\)$/g, '')
163
+ if (!s) return []
164
+ return s.split(',').map((item) => {
165
+ const t = stripQuotes(item)
166
+ return /^-?\d+(\.\d+)?$/.test(t) ? Number(t) : t
167
+ })
168
+ }
169
+
170
+ const parseRangeLiteral = (raw) => {
171
+ if (Array.isArray(raw)) return raw.slice(0, 2)
172
+ const s = String(raw == null ? '' : raw).trim()
173
+ if (!s) return []
174
+ const byAnd = s.split(/\s+AND\s+/i)
175
+ if (byAnd.length >= 2) return [stripQuotes(byAnd[0]), stripQuotes(byAnd.slice(1).join(' AND '))]
176
+ const byComma = s.split(',')
177
+ if (byComma.length >= 2) return [stripQuotes(byComma[0]), stripQuotes(byComma.slice(1).join(','))]
178
+ return []
179
+ }
180
+
181
+ const isOperatorMap = (value) => {
182
+ if (!isPlainObject(value) || value instanceof Date || value._isAMomentObject) return false
183
+ const keys = Object.keys(value)
184
+ return keys.length > 0 && keys.every((key) => !!normalizeOperator(key) || META_WHERE_KEYS.has(String(key).toLowerCase()))
185
+ }
186
+
187
+ const fieldSqlWithCast = (fieldSql, cast) => {
188
+ if (!cast) return fieldSql
189
+ const c = String(cast).trim().toLowerCase()
190
+ return SAFE_CAST.test(c) ? `${fieldSql}::${c}` : fieldSql
191
+ }
192
+
193
+ const pushWhereCondition = (parts, params, increment, fieldSql, operator, value) => {
194
+ const op = normalizeOperator(operator)
195
+ if (!fieldSql || !op || !Number.isInteger(increment) || increment < 1) return increment
196
+
197
+ if (op === 'IS NULL' || op === 'IS NOT NULL') {
198
+ parts.push(`${fieldSql} ${op}`)
199
+ return increment
200
+ }
201
+
202
+ if (op === 'IS' || op === 'IS NOT') {
203
+ if (value == null || /^null$/i.test(String(value))) {
204
+ parts.push(`${fieldSql} ${op} NULL`)
205
+ return increment
125
206
  }
207
+ parts.push(`${fieldSql} ${op} $${increment}`)
208
+ params.push(value)
209
+ return increment + 1
126
210
  }
127
- if (obj.hasOwnProperty('order_by') && Array.isArray(obj.order_by) && obj.order_by.length) {
128
- const segments = obj.order_by.map(sanitizeOrderBySegment).filter(Boolean)
129
- if (segments.length) {
130
- orderBy = ` ORDER BY ${segments.join(', ')} `
211
+
212
+ if (op === 'IN' || op === 'NOT IN') {
213
+ const list = Array.isArray(value) ? value : parseListLiteral(value)
214
+ if (!list.length) {
215
+ parts.push(op === 'IN' ? 'FALSE' : 'TRUE')
216
+ return increment
131
217
  }
218
+ parts.push(`${fieldSql} ${op} (${list.map((_, i) => `$${increment + i}`).join(', ')})`)
219
+ params.push(...list)
220
+ return increment + list.length
132
221
  }
133
222
 
134
- return orderBy
223
+ if (op === 'BETWEEN' || op === 'NOT BETWEEN') {
224
+ const range = parseRangeLiteral(value)
225
+ if (range.length < 2) return increment
226
+ parts.push(`${fieldSql} ${op} $${increment} AND $${increment + 1}`)
227
+ params.push(range[0], range[1])
228
+ return increment + 2
229
+ }
230
+
231
+ if (op === '@>') {
232
+ parts.push(`${fieldSql}::jsonb @> $${increment}::jsonb`)
233
+ params.push(safeJson(value))
234
+ return increment + 1
235
+ }
236
+
237
+ parts.push(`${fieldSql} ${op} $${increment}`)
238
+ params.push(value)
239
+ return increment + 1
135
240
  }
136
241
 
137
- const whereFn = (obj) => {
138
- const where = obj.where || {}
139
- //[{field:"your_field",option:"=>",value:"12",operator:"AND",type:"text,json,date, inline"}]
140
- const whereArray = obj.whereArray || []
141
- let increment = 1
142
- let arr = [],
143
- wherequery = []
144
- for (const key in where) {
145
- wherequery.push(key.indexOf('.') > -1 ? ` ${key} = $${increment} ` : ` "${key}" = $${increment}`)
146
- arr.push(where[key])
147
- increment++
242
+ const appendWhereEntry = (parts, params, increment, key, value) => {
243
+ let fieldName = key
244
+ let keyOp = ''
245
+ const keyMatch = typeof key === 'string' ? KEY_OPERATOR_RE.exec(key.trim()) : null
246
+ if (keyMatch) {
247
+ fieldName = keyMatch[1]
248
+ keyOp = normalizeOperator(keyMatch[2])
148
249
  }
149
- //console.log(whereArray);
150
- let hasWhere = false
151
- wherequery = arr.length ? wherequery.join(' AND ') : ''
152
- if (whereArray.length) {
153
- let andOr = wherequery ? ' AND ' : ''
154
- whereArray.map((item, index) => {
155
- let type = !item.type ? 'text' : item.type
156
- if (index > 0) {
157
- andOr = ''
158
- }
159
- let operator = !item.operator ? ' AND ' : item.operator
160
- if (index == whereArray.length - 1) {
161
- operator = ''
162
- }
163
- let fields = ''
164
- if (type == 'date') {
165
- field = item.field.indexOf('.') > -1 ? item.field + '::text ' : ` ${item.field}::text `
166
- } else {
167
- field = item.field.indexOf('.') > -1 ? item.field : item.field ? ` "${item.field}" ` : ''
168
- }
169
- // PostgreSQL: jsonb containment (@>) dengan parameter untuk keamanan
170
- if (item.isJSON) {
171
- wherequery += andOr + ` (${String(field).trim()})::jsonb @> $${increment}::jsonb ${operator}`
172
- arr.push(JSON.stringify(item.value))
173
- increment++
174
- hasWhere = true
175
- } else {
176
- if (type == 'json') {
177
- wherequery += andOr + ` (${String(field).trim()})::jsonb @> $${increment}::jsonb ${operator}`
178
- arr.push(JSON.stringify(item.value))
179
- increment++
180
- hasWhere = true
181
- } else if (type == 'inline') {
182
- //select * from attendance where employee_id = 4803 and date IN ('2023-12-21','2023-12-22')
183
- if (field) {
184
- wherequery += andOr + ` ${field} ${item.value} ${operator}`
185
- } else {
186
- wherequery += andOr + ` ${item.value} ${operator}`
187
- }
188
- hasWhere = true
189
- } else {
190
- if (item.option.includes('{{value}}')) {
191
- item.option = item.option.replace('{{value}}', `$${increment}`)
192
- wherequery += `${andOr} ${field} ${item.option} ${operator}`
193
- increment++
194
- } else if (item.option.includes('$')) {
195
- wherequery += `${andOr} ${field} ${item.option} ${operator}`
196
- increment++
197
- } else {
198
- wherequery += `${andOr} ${field} ${item.option} $${increment} ${operator}`
199
- increment++
200
- }
201
- let itemValue = item.value
202
- if (item.option == '=') {
203
- itemValue = Util.replaceAll(itemValue + '', '%', '')
204
- }
205
- arr.push(itemValue)
206
- }
207
- }
208
- })
209
- //console.log(arr)
250
+
251
+ const fieldSql = quoteIdent(fieldName)
252
+ if (!fieldSql) return increment
253
+ if (keyOp) return pushWhereCondition(parts, params, increment, fieldSql, keyOp, value)
254
+ if (value === null || value === undefined) {
255
+ parts.push(`${fieldSql} IS NULL`)
256
+ return increment
210
257
  }
211
- if (arr.length > 0) {
212
- hasWhere = true
258
+
259
+ if (Array.isArray(value)) {
260
+ if (value.length && typeof value[0] === 'string' && normalizeOperator(value[0])) {
261
+ const op = normalizeOperator(value[0])
262
+ const rest = value.slice(1)
263
+ const first = rest.length === 1 ? rest[0] : rest
264
+ if (op === 'IN' || op === 'NOT IN' || op === 'BETWEEN' || op === 'NOT BETWEEN') {
265
+ return pushWhereCondition(parts, params, increment, fieldSql, op, first)
266
+ }
267
+ return pushWhereCondition(parts, params, increment, fieldSql, op, rest[0])
268
+ }
269
+ return pushWhereCondition(parts, params, increment, fieldSql, 'IN', value)
213
270
  }
214
- let wheres = ''
215
- if (hasWhere) {
216
- wheres = `WHERE ${wherequery}`
271
+
272
+ if (isOperatorMap(value)) {
273
+ const castRaw = value.cast || value.type
274
+ const sqlField = fieldSqlWithCast(fieldSql, String(castRaw || '').toLowerCase() === 'date' ? 'date' : castRaw)
275
+ for (const [opKey, opVal] of Object.entries(value)) {
276
+ if (META_WHERE_KEYS.has(String(opKey).toLowerCase())) continue
277
+ increment = pushWhereCondition(parts, params, increment, sqlField, opKey, opVal)
278
+ }
279
+ return increment
217
280
  }
218
281
 
219
- let objAll = {
220
- where: wheres,
221
- arr: arr,
222
- increment: increment,
282
+ if (typeof value === 'string') {
283
+ const matched = STRING_OPERATOR_RE.exec(value.trim())
284
+ if (matched) {
285
+ const op = normalizeOperator(matched[1])
286
+ let raw = matched[2]
287
+ if (op === 'IN' || op === 'NOT IN') raw = parseListLiteral(raw)
288
+ else if (op === 'BETWEEN' || op === 'NOT BETWEEN') raw = parseRangeLiteral(raw)
289
+ else if ((op === 'IS' || op === 'IS NOT') && /^null$/i.test(String(raw))) raw = null
290
+ return pushWhereCondition(parts, params, increment, fieldSql, op, raw)
291
+ }
223
292
  }
224
- //console.log(obj)
225
- return objAll
293
+
294
+ return pushWhereCondition(parts, params, increment, fieldSql, '=', value)
226
295
  }
227
296
 
228
- connection.results = async (obj) => {
229
- const table = sanitizeTableName(obj.table || '')
230
- const select = obj.select || '*'
231
- const statement = obj.statement || ''
232
- const limitVal = toSafeNonNegativeInt(obj.limit, null)
233
- const offsetVal = obj.hasOwnProperty('offset') ? toSafeNonNegativeInt(obj.offset, 0) : (obj.limit ? 0 : null)
234
- const limit = limitVal !== null ? ` LIMIT ${limitVal} ` : ''
235
- const offset = offsetVal !== null ? ` OFFSET ${offsetVal} ` : ''
236
- const orderBy = orderByFn(obj)
237
- const values = obj.values || []
238
- const objJoin = obj.joins || []
239
- let join = ''
240
- if (objJoin.length) {
241
- join = objJoin.join(' ')
297
+ const appendWhereObject = (target, params, increment, source) => {
298
+ if (!isPlainObject(source)) return increment
299
+ for (const key of Object.keys(source)) {
300
+ increment = appendWhereEntry(target, params, increment, key, source[key])
242
301
  }
243
- const whereObj = whereFn(obj)
244
- const wheres = whereObj.where
245
- const arr = whereObj.arr
302
+ return increment
303
+ }
246
304
 
247
- const sql = `SELECT ${select} FROM "${table}" ${join} ${wheres} ${statement} ${orderBy} ${limit} ${offset}`.replace(/\s+/g, ' ').trim()
248
- try {
249
- const start = Date.now()
250
- const result = await pool.query(sql, arr.length ? arr : values.length ? values : null)
251
- return !result.rows ? [] : result.rows
252
- } catch (e) {
253
- console.log(sql)
254
- console.log(arr)
255
- console.log(e.toString())
256
- throw e
305
+ const normalizeWhereArrayItem = (raw) => {
306
+ if (Array.isArray(raw)) {
307
+ return { field: raw[0], option: raw[1], value: raw[2], operator: raw[3] || 'AND', type: raw[4] }
257
308
  }
309
+ return isPlainObject(raw) ? raw : null
258
310
  }
259
311
 
260
- connection.sql = async (obj) => {
261
- const select = obj.select || '*'
262
- const table = obj.table || ''
263
- //[{field:"your_field",option:"=>",value:"12",operator:"AND",type:"text,json,date"}]
264
- const statement = obj.statement || ''
265
- const limit = obj.limit ? ` LIMIT ${obj.limit} ` : ''
266
- const offset = obj.hasOwnProperty('offset') ? ` OFFSET ${obj.offset} ` : obj.limit ? 'OFFSET 0' : ''
267
- const orderBy = orderByFn(obj)
268
- const values = obj.values || []
269
- const objJoin = obj.joins || []
270
- let join = ''
271
- if (objJoin.length) {
272
- join = objJoin.join(' ')
312
+ const applyWhereArrayItem = (parts, params, increment, item) => {
313
+ const type = item.type || 'text'
314
+ const fieldSql = item.field ? quoteIdent(item.field) : ''
315
+ const field = type === 'date' && fieldSql ? `${fieldSql}::text` : fieldSql
316
+
317
+ if (item.isJSON || type === 'json' || type === 'jsonb') {
318
+ if (!fieldSql) return increment
319
+ parts.push(`${fieldSql}::jsonb @> $${increment}::jsonb`)
320
+ params.push(typeof item.value === 'string' ? item.value : safeJson(item.value))
321
+ return increment + 1
273
322
  }
274
- const whereObj = whereFn(obj)
275
- const wheres = whereObj.where
276
- const arr = whereObj.arr
277
- const sql = `SELECT ${select} FROM "${table}" ${join} ${wheres} ${statement} ${orderBy} ${limit} ${offset}`
278
- return { sql: sql, arr: arr }
279
- }
280
323
 
281
- connection.result = async (obj) => {
282
- const results = await connection.results(obj)
283
- if (results.length) {
284
- return results[0]
285
- } else {
286
- return []
324
+ if (type === 'inline') {
325
+ if (item.value == null) return increment
326
+ parts.push(field ? `${field} ${item.value}` : String(item.value))
327
+ return increment
287
328
  }
288
- }
289
329
 
290
- connection.insert = async (obj) => {
291
- const table = sanitizeTableName(obj.table || '')
292
- if (!table) throw new Error('Table name is required for insert')
293
- const data = { ...obj.data }
294
- const returning = sanitizeReturning(data.returning || 'RETURNING *')
295
- delete data.returning
296
- let increment = 1
297
- const datas = []
298
- const values = []
299
- const arr = []
300
- for (const key in data) {
301
- const col = sanitizeColumnName(key)
302
- if (col) {
303
- datas.push(col)
304
- values.push(`$${increment}`)
305
- arr.push(data[key])
306
- increment++
307
- }
330
+ const rawOption = item.option == null ? '=' : String(item.option)
331
+ if (rawOption.includes('{{value}}')) {
332
+ parts.push(`${field} ${rawOption.replace('{{value}}', `$${increment}`)}`.trim())
333
+ params.push(item.value)
334
+ return increment + 1
335
+ }
336
+ if (rawOption.includes('$')) {
337
+ parts.push(`${field} ${rawOption}`.trim())
338
+ params.push(item.value)
339
+ return increment + 1
308
340
  }
309
- if (datas.length === 0) throw new Error('At least one valid column is required for insert')
310
- const sql = `INSERT INTO "${table}" ("${datas.join('","')}") VALUES (${values.join(',')}) ${returning}`
311
341
 
312
- try {
313
- const results = await pool.query(sql, arr)
314
- return results.rows[0]
315
- } catch (e) {
316
- console.log(sql)
317
- console.log(arr)
318
- console.log(e.toString())
319
- throw e
342
+ const option = sanitizeWhereOption(rawOption) || '='
343
+ if (Array.isArray(item.value) && (option === '=' || option === 'IN' || option === 'NOT IN')) {
344
+ return pushWhereCondition(parts, params, increment, field, option === 'NOT IN' ? 'NOT IN' : 'IN', item.value)
345
+ }
346
+ if (option === '=' && item.value != null && typeof item.value !== 'object') {
347
+ const cleaned = Util && typeof Util.replaceAll === 'function'
348
+ ? Util.replaceAll(String(item.value), '%', '')
349
+ : String(item.value).split('%').join('')
350
+ return pushWhereCondition(parts, params, increment, field, option, cleaned)
320
351
  }
352
+ return pushWhereCondition(parts, params, increment, field, option, item.value)
321
353
  }
322
354
 
323
- connection.update = async (obj) => {
324
- const table = sanitizeTableName(obj.table || '')
325
- if (!table) throw new Error('Table name is required for update')
326
- const data = { ...obj.data }
327
- const returning = sanitizeReturning(data.returning || 'RETURNING *')
328
- delete data.returning
329
-
330
- const where = obj.where || {}
331
- const whereArray = obj.whereArray || []
332
- const arr = []
333
- const dataArr = []
334
- let wherequery = []
335
- let increment = 1
355
+ const whereFn = (obj, startIncrement = 1) => {
356
+ const incrementStart = Number.isInteger(startIncrement) && startIncrement > 0 ? startIncrement : 1
357
+ const params = []
358
+ const parts = []
359
+ let increment = incrementStart
360
+
361
+ increment = appendWhereObject(parts, params, increment, obj && obj.where)
362
+
363
+ const orParts = []
364
+ increment = appendWhereObject(orParts, params, increment, obj && (obj.orWhere || obj.or_where))
365
+ if (orParts.length) parts.push(`(${orParts.join(' OR ')})`)
366
+
367
+ const arrayParts = []
368
+ const connectors = []
369
+ asArray(obj && obj.whereArray).forEach((raw) => {
370
+ const item = normalizeWhereArrayItem(raw)
371
+ if (!item) return
372
+ const itemParts = []
373
+ increment = applyWhereArrayItem(itemParts, params, increment, item)
374
+ if (!itemParts.length) return
375
+ arrayParts.push(itemParts.join(' AND '))
376
+ connectors.push(sanitizeWhereConnector(item.operator))
377
+ })
378
+
379
+ let sql = parts.join(' AND ')
380
+ if (arrayParts.length) {
381
+ const arraySql = arrayParts.reduce((acc, part, index) => (
382
+ index === 0 ? part : `${acc}${connectors[index - 1]}${part}`
383
+ ), '')
384
+ sql = sql ? `${sql} AND ${arraySql}` : arraySql
385
+ }
336
386
 
337
- for (const key in data) {
338
- const col = sanitizeColumnName(key)
339
- if (col) {
340
- dataArr.push(`"${col}" = $${increment}`)
341
- arr.push(data[key])
342
- increment++
343
- }
387
+ return {
388
+ where: sql ? `WHERE ${sql}` : '',
389
+ arr: params,
390
+ increment,
344
391
  }
345
- if (dataArr.length === 0) throw new Error('At least one valid column is required for update')
392
+ }
346
393
 
347
- for (const key in where) {
348
- const col = sanitizeColumnName(key)
349
- if (col) {
350
- wherequery.push(`"${col}" = $${increment}`)
351
- arr.push(where[key])
352
- increment++
353
- }
394
+ const formatSelect = (select) => {
395
+ if (Array.isArray(select)) {
396
+ const cols = select
397
+ .filter((item) => typeof item === 'string' && item.trim())
398
+ .map((item) => quoteIdent(item.trim()) || item.trim())
399
+ return cols.length ? cols.join(', ') : '*'
354
400
  }
355
- wherequery = wherequery.length ? wherequery.join(' AND ') : ''
356
-
357
- if (whereArray.length) {
358
- let andOr = wherequery ? ' AND ' : ''
359
- whereArray.forEach((item, index) => {
360
- if (index > 0) andOr = ''
361
- const field = sanitizeWhereField(item.field)
362
- const option = sanitizeWhereOption(item.option)
363
- const connector = sanitizeWhereConnector(item.operator)
364
- if (field && option) {
365
- wherequery += `${andOr} ${field} ${option} $${increment} ${connector}`
366
- arr.push(item.value)
367
- increment++
368
- }
401
+ return typeof select === 'string' && select.trim() ? select : '*'
402
+ }
403
+
404
+ const formatJoins = (joins) => asArray(joins).map((item) => {
405
+ if (typeof item === 'string') return item.trim()
406
+ if (!isPlainObject(item)) return ''
407
+ const type = String(item.type || 'LEFT').trim().toUpperCase()
408
+ if (!JOIN_TYPES.has(type)) return ''
409
+ const table = quoteTableName(item.table || '')
410
+ if (!table) return ''
411
+ const alias = item.as ? ` ${quoteIdent(item.as) || ''}`.trimEnd() : ''
412
+ if (type === 'CROSS') return `CROSS JOIN ${table}${alias}`
413
+ const on = String(item.on || '').trim()
414
+ return on ? `${type} JOIN ${table}${alias} ON ${on}` : ''
415
+ }).filter(Boolean).join(' ')
416
+
417
+ const groupByFn = (obj) => {
418
+ const value = obj && (obj.groupBy || obj.group_by)
419
+ if (!value) return ''
420
+ const cols = (Array.isArray(value) ? value : String(value).split(','))
421
+ .map((item) => {
422
+ const trimmed = String(item || '').trim()
423
+ return quoteIdent(trimmed) || sanitizeOrderBySegment(trimmed)
369
424
  })
370
- if (wherequery.endsWith(' AND ')) wherequery = wherequery.slice(0, -5)
371
- else if (wherequery.endsWith(' OR ')) wherequery = wherequery.slice(0, -4)
372
- }
425
+ .filter(Boolean)
426
+ return cols.length ? ` GROUP BY ${cols.join(', ')}` : ''
427
+ }
373
428
 
374
- const wheres = wherequery ? ' WHERE ' + wherequery : ''
375
- const sql = `UPDATE "${table}" SET ${dataArr.join(', ')}${wheres} ${returning}`
429
+ const havingFn = (obj, increment, params) => {
430
+ const parts = []
431
+ const next = appendWhereObject(parts, params, increment, obj && obj.having)
432
+ return { sql: parts.length ? ` HAVING ${parts.join(' AND ')}` : '', increment: next }
433
+ }
376
434
 
377
- try {
378
- const result = await pool.query(sql, arr)
379
- return result.rows[0]
380
- } catch (e) {
381
- console.log(sql)
382
- console.log(arr)
383
- console.log(e.toString())
384
- throw e
435
+ const orderByFn = (obj) => {
436
+ if (!isPlainObject(obj)) return ''
437
+
438
+ if (hasOwn(obj, 'order_by') && Array.isArray(obj.order_by) && obj.order_by.length) {
439
+ const segments = obj.order_by.map(sanitizeOrderBySegment).filter(Boolean)
440
+ return segments.length ? ` ORDER BY ${segments.join(', ')}` : ''
385
441
  }
386
- }
387
442
 
388
- connection.delete = async (obj) => {
389
- const table = sanitizeTableName(obj.table || '')
390
- if (!table) throw new Error('Table name is required for delete')
391
- const where = obj.where || {}
392
- const arr = []
393
- const wherequery = []
394
- let increment = 1
395
- for (const key in where) {
396
- const col = sanitizeColumnName(key)
397
- if (col) {
398
- wherequery.push(`"${col}" = $${increment}`)
399
- arr.push(where[key])
400
- increment++
401
- }
443
+ const pairs = asArray(obj.orderBy)
444
+ const segments = []
445
+ for (let i = 0; i < pairs.length; i += 2) {
446
+ const col = sanitizeOrderBySegment(String(pairs[i] == null ? '' : pairs[i]))
447
+ if (!col) continue
448
+ const dir = pairs[i + 1] != null ? String(pairs[i + 1]).trim().toUpperCase() : ''
449
+ segments.push(dir === 'ASC' || dir === 'DESC' ? (col.includes(' ') ? col : `${col} ${dir}`) : col)
402
450
  }
403
- const whereClause = wherequery.length ? wherequery.join(' AND ') : ''
404
- const wheres = whereClause ? ' WHERE ' + whereClause : ''
405
- const sql = `DELETE FROM "${table}"${wheres} RETURNING *`
406
- try {
407
- return await pool.query(sql, arr)
408
- } catch (e) {
409
- console.log(sql)
410
- console.log(arr)
411
- console.log(e.toString())
412
- throw e
451
+ return segments.length ? ` ORDER BY ${segments.join(', ')}` : ''
452
+ }
453
+
454
+ const buildSelectQuery = (obj) => {
455
+ if (!isPlainObject(obj)) throw new Error('Query options must be an object')
456
+ const tableSql = requireTableName(obj.table, 'select')
457
+ const whereObj = whereFn(obj)
458
+ const havingObj = havingFn(obj, whereObj.increment, whereObj.arr)
459
+ const limitVal = toSafeNonNegativeInt(obj.limit, null)
460
+ const offsetVal = hasOwn(obj, 'offset') ? toSafeNonNegativeInt(obj.offset, 0) : (obj.limit ? 0 : null)
461
+ const sql = [
462
+ `SELECT ${obj.distinct ? 'DISTINCT ' : ''}${formatSelect(obj.select)}`,
463
+ `FROM ${tableSql}`,
464
+ formatJoins(obj.joins || obj.join),
465
+ whereObj.where,
466
+ groupByFn(obj),
467
+ havingObj.sql,
468
+ obj.statement || '',
469
+ orderByFn(obj),
470
+ limitVal !== null ? `LIMIT ${limitVal}` : '',
471
+ offsetVal !== null ? `OFFSET ${offsetVal}` : '',
472
+ ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim()
473
+
474
+ return {
475
+ sql,
476
+ arr: whereObj.arr,
477
+ values: asArray(obj.values),
413
478
  }
414
479
  }
415
480
 
416
- connection.insertData = async(tableName, data) => {
417
- try {
418
- // Validasi input
419
- if (!data || typeof data !== 'object') {
420
- throw new Error('Invalid data provided for insert');
421
- }
481
+ const requireDataObject = (data, action) => {
482
+ if (!isPlainObject(data)) throw new Error(`Data object is required for ${action}`)
483
+ return { ...data }
484
+ }
422
485
 
423
- const columns = Object.keys(data);
424
- if (columns.length === 0) {
425
- throw new Error('No columns found in the data object');
426
- }
486
+ const collectColumns = (data) => {
487
+ const cols = []
488
+ const values = []
489
+ Object.keys(data).forEach((key) => {
490
+ const col = sanitizeColumnName(key)
491
+ if (!col) return
492
+ cols.push(col)
493
+ values.push(data[key])
494
+ })
495
+ if (!cols.length) throw new Error('At least one valid column is required')
496
+ return { cols, values }
497
+ }
498
+
499
+ const connection = {}
427
500
 
428
- const placeholders = columns.map((_, index) => `$${index + 1}`).join(',');
429
- const insertQuery = `
430
- INSERT INTO "${tableName}" (${columns.map(col => `"${col}"`).join(',')})
431
- VALUES (${placeholders})
432
- RETURNING *
433
- `;
434
- const values = columns.map(col => data[col]);
435
- const result = await pool.query(insertQuery, values);
436
- console.log(`Successfully inserted 1 record into ${tableName}`);
437
- return result.rows[0];
438
- } catch (error) {
439
- console.error(`Error inserting record into ${tableName}:`, error);
440
- throw error;
441
- } finally {
501
+ connection.query = async (string, arr) => {
502
+ try {
503
+ if (typeof string !== 'string' || !string.trim()) return []
504
+ const result = await pool.query(string, Array.isArray(arr) && arr.length ? arr : undefined)
505
+ return rowsOf(result)
506
+ } catch (err) {
507
+ logQueryError(string, arr, err)
508
+ return []
442
509
  }
443
510
  }
444
511
 
445
- connection.deleteData = async(tableName, where) => {
512
+ connection.results = async (obj) => {
513
+ const { sql, arr, values } = buildSelectQuery(obj)
514
+ const result = await runQuery(sql, arr.length ? arr : values)
515
+ return rowsOf(result)
516
+ }
517
+
518
+ connection.sql = async (obj) => {
446
519
  try {
447
- // Validasi input
448
- if (!where || typeof where !== 'object') {
449
- throw new Error('Invalid where clause provided for delete');
450
- }
520
+ const built = buildSelectQuery(obj)
521
+ return { sql: built.sql, arr: built.arr }
522
+ } catch (err) {
523
+ return { sql: '', arr: [], error: err.message }
524
+ }
525
+ }
451
526
 
452
- const whereColumns = Object.keys(where);
453
- if (whereColumns.length === 0) {
454
- throw new Error('No where conditions provided for delete');
455
- }
527
+ connection.result = async (obj) => {
528
+ const results = await connection.results(obj)
529
+ return Array.isArray(results) && results.length ? results[0] : []
530
+ }
456
531
 
457
- const whereConditions = whereColumns.map((col, index) => `"${col}" = $${index + 1}`).join(' AND ');
458
- const deleteQuery = `
459
- DELETE FROM "${tableName}"
460
- WHERE ${whereConditions}
461
- RETURNING *
462
- `;
463
- const values = whereColumns.map(col => where[col]);
464
- const result = await pool.query(deleteQuery, values);
465
- console.log(`Successfully deleted ${result.rowCount} records from ${tableName}`);
466
- return result.rowCount;
467
- } catch (error) {
468
- console.error(`Error deleting records from ${tableName}:`, error);
469
- throw error;
470
- } finally {
471
- }
532
+ connection.insert = async (obj) => {
533
+ if (!isPlainObject(obj)) throw new Error('Insert options must be an object')
534
+ const tableSql = requireTableName(obj.table, 'insert')
535
+ const data = requireDataObject(obj.data, 'insert')
536
+ const returning = sanitizeReturning(data.returning || 'RETURNING *')
537
+ delete data.returning
538
+ const { cols, values } = collectColumns(data)
539
+ const placeholders = cols.map((_, i) => `$${i + 1}`)
540
+ const sql = `INSERT INTO ${tableSql} ("${cols.join('","')}") VALUES (${placeholders.join(',')}) ${returning}`
541
+ return rowsOf(await runQuery(sql, values))[0]
472
542
  }
473
543
 
474
- connection.insertMultipleRecords = async(tableName,records) => {
475
- try {
476
- // Validasi input
477
- if (!records || !Array.isArray(records) || records.length === 0) {
478
- console.warn(`No records to insert into ${tableName}`);
479
- return [];
480
- }
544
+ connection.update = async (obj) => {
545
+ if (!isPlainObject(obj)) throw new Error('Update options must be an object')
546
+ const tableSql = requireTableName(obj.table, 'update')
547
+ const data = requireDataObject(obj.data, 'update')
548
+ const returning = sanitizeReturning(data.returning || 'RETURNING *')
549
+ delete data.returning
550
+ const { cols, values } = collectColumns(data)
551
+ const setSql = cols.map((col, i) => `"${col}" = $${i + 1}`).join(', ')
552
+ const whereObj = whereFn(obj, cols.length + 1)
553
+ const sql = `UPDATE ${tableSql} SET ${setSql}${whereObj.where ? ` ${whereObj.where}` : ''} ${returning}`
554
+ return rowsOf(await runQuery(sql, values.concat(whereObj.arr)))[0]
555
+ }
481
556
 
482
- // Validasi bahwa semua records memiliki struktur yang sama
483
- const firstRecord = records[0];
484
- if (!firstRecord || typeof firstRecord !== 'object') {
485
- throw new Error('First record is invalid or empty');
486
- }
557
+ connection.delete = async (obj) => {
558
+ if (!isPlainObject(obj)) throw new Error('Delete options must be an object')
559
+ const tableSql = requireTableName(obj.table, 'delete')
560
+ const whereObj = whereFn(obj, 1)
561
+ const sql = `DELETE FROM ${tableSql}${whereObj.where ? ` ${whereObj.where}` : ''} RETURNING *`
562
+ return runQuery(sql, whereObj.arr)
563
+ }
487
564
 
488
- const columns = Object.keys(firstRecord);
489
- if (columns.length === 0) {
490
- throw new Error('No columns found in the first record');
491
- }
565
+ connection.count = async (obj) => {
566
+ if (!isPlainObject(obj)) throw new Error('Count options must be an object')
567
+ const countSelect = obj.select && /count\s*\(/i.test(String(obj.select))
568
+ ? obj.select
569
+ : `COUNT(${obj.countField ? (quoteIdent(obj.countField) || '*') : '*'}) AS count`
570
+ const row = await connection.result({
571
+ ...obj,
572
+ select: countSelect,
573
+ limit: undefined,
574
+ offset: undefined,
575
+ orderBy: undefined,
576
+ order_by: undefined,
577
+ distinct: undefined,
578
+ })
579
+ if (!row || Array.isArray(row)) return 0
580
+ const n = Number(row.count)
581
+ return Number.isFinite(n) ? n : 0
582
+ }
492
583
 
493
- // Validasi bahwa semua records memiliki kolom yang sama
494
- for (let i = 1; i < records.length; i++) {
495
- const record = records[i];
496
- if (!record || typeof record !== 'object') {
497
- throw new Error(`Record at index ${i} is invalid`);
498
- }
499
- const recordColumns = Object.keys(record);
500
- if (recordColumns.length !== columns.length || !recordColumns.every(col => columns.includes(col))) {
501
- throw new Error(`Record at index ${i} has different structure than the first record`);
502
- }
503
- }
584
+ connection.insertData = async (tableName, data) => connection.insert({ table: tableName, data })
504
585
 
505
- const placeholders = records.map((_, rowIndex) =>
506
- `(${columns.map((_, colIndex) => `$${rowIndex * columns.length + colIndex + 1}`).join(',')})`
507
- ).join(',');
508
-
509
- // Create the INSERT query
510
- const insertQuery = `
511
- INSERT INTO "${tableName}" (${columns.map(col => `"${col}"`).join(',')})
512
- VALUES ${placeholders}
513
- RETURNING *
514
- `;
515
- const values = records.flatMap(record => columns.map(col => record[col]));
516
- const result = await pool.query(insertQuery, values);
517
- console.log(`Successfully inserted ${records.length} records into ${tableName}`);
518
- return result.rows;
519
- } catch (error) {
520
- console.error(`Error inserting multiple records into ${tableName}:`, error);
521
- throw error;
522
- } finally {
586
+ connection.deleteData = async (tableName, where) => {
587
+ if (!isPlainObject(where) || !Object.keys(where).length) {
588
+ throw new Error('Invalid where clause provided for delete')
523
589
  }
590
+ const result = await connection.delete({ table: tableName, where })
591
+ return result && Number.isInteger(result.rowCount) ? result.rowCount : 0
524
592
  }
525
593
 
526
- connection.driver = config.driver
527
- connection.showTables = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
528
- connection.showFullFields = (tableRelations) => {
529
- return `SELECT
530
- column_name AS "Field", concat(data_type,'(',character_maximum_length,')') AS "Type" , is_nullable AS "Null"
531
- FROM
532
- information_schema.COLUMNS
533
- WHERE
534
- TABLE_NAME = '${tableRelations}';`
594
+ connection.insertMultipleRecords = async (tableName, records) => {
595
+ const tableSql = requireTableName(tableName, 'insert')
596
+ const rows = asArray(records)
597
+ if (!rows.length) return []
598
+ if (!isPlainObject(rows[0])) throw new Error('First record is invalid or empty')
599
+
600
+ const rawCols = Object.keys(rows[0])
601
+ const cols = rawCols.map(sanitizeColumnName)
602
+ if (!cols.length || cols.some((col) => !col)) throw new Error('No valid columns found in the first record')
603
+
604
+ rows.forEach((record, index) => {
605
+ if (!isPlainObject(record)) throw new Error(`Record at index ${index} is invalid`)
606
+ const recordCols = Object.keys(record)
607
+ if (recordCols.length !== rawCols.length || !recordCols.every((col) => rawCols.includes(col))) {
608
+ throw new Error(`Record at index ${index} has different structure than the first record`)
609
+ }
610
+ })
611
+
612
+ const placeholders = rows.map((_, rowIndex) =>
613
+ `(${cols.map((_, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(',')})`
614
+ ).join(',')
615
+ const values = rows.flatMap((record) => cols.map((col) => record[col]))
616
+ const sql = `INSERT INTO ${tableSql} ("${cols.join('","')}") VALUES ${placeholders} RETURNING *`
617
+ return rowsOf(await runQuery(sql, values))
535
618
  }
536
619
 
537
- connection.describeTable = (table) => {
538
- return connection.showFullFields(table)
620
+ connection.driver = (config && config.parsed && config.parsed.DRIVER) || 'pg'
621
+ connection.showTables = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
622
+
623
+ connection.showFullFields = (table) => {
624
+ const name = schemaSql(table)
625
+ if (!name) return ''
626
+ return `SELECT column_name AS "Field", concat(data_type,'(',character_maximum_length,')') AS "Type", is_nullable AS "Null"
627
+ FROM information_schema.COLUMNS
628
+ WHERE TABLE_NAME = ${name};`
539
629
  }
540
630
 
631
+ connection.describeTable = (table) => connection.showFullFields(table)
632
+
541
633
  connection.showComments = (table) => {
542
- return ` SELECT c.table_schema,c.table_name,c.column_name as "COLUMN_NAME",pgd.description as "COLUMN_COMMENT"
634
+ const name = schemaSql(table)
635
+ if (!name) return ''
636
+ return `SELECT c.table_schema,c.table_name,c.column_name as "COLUMN_NAME",pgd.description as "COLUMN_COMMENT"
543
637
  FROM pg_catalog.pg_statio_all_tables as st
544
638
  inner join pg_catalog.pg_description pgd on (pgd.objoid=st.relid)
545
639
  inner join information_schema.columns c on (pgd.objsubid=c.ordinal_position
546
640
  and c.table_schema=st.schemaname and c.table_name=st.relname)
547
- WHERE c.table_name = '${table}' ORDER BY c.column_name`
641
+ WHERE c.table_name = ${name} ORDER BY c.column_name`
548
642
  }
549
643
 
550
644
  connection.showFields = (table) => {
551
- return `
552
- SELECT
553
- tc.table_name AS "TABLE_NAME",
554
- kcu.column_name AS "COLUMN_NAME",
555
- tc.constraint_name AS "CONSTRAINT_NAME",
556
- ccu.table_name AS "REFERENCED_TABLE_NAME",
557
- ccu.column_name AS "REFERENCED_COLUMN_NAME",
558
- tc.table_schema
559
- FROM
560
- information_schema.table_constraints AS tc
645
+ const name = schemaSql(table)
646
+ if (!name) return ''
647
+ return `SELECT tc.table_name AS "TABLE_NAME", kcu.column_name AS "COLUMN_NAME", tc.constraint_name AS "CONSTRAINT_NAME",
648
+ ccu.table_name AS "REFERENCED_TABLE_NAME", ccu.column_name AS "REFERENCED_COLUMN_NAME", tc.table_schema
649
+ FROM information_schema.table_constraints AS tc
561
650
  JOIN information_schema.key_column_usage AS kcu
562
- ON tc.constraint_name = kcu.constraint_name
563
- AND tc.table_schema = kcu.table_schema
651
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
564
652
  JOIN information_schema.constraint_column_usage AS ccu
565
- ON ccu.constraint_name = tc.constraint_name
566
- AND ccu.table_schema = tc.table_schema
567
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='${table}';`
653
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
654
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name=${name};`
568
655
  }
569
656
 
570
- //list constraint list
571
657
  connection.constraintList = (table, schema = 'public') => {
572
- return `
573
- SELECT con.*
658
+ const name = schemaSql(table)
659
+ const schemaName = sanitizeTableName(schema)
660
+ if (!name || !schemaName) return ''
661
+ return `SELECT con.*
574
662
  FROM pg_catalog.pg_constraint con
575
- INNER JOIN pg_catalog.pg_class rel
576
- ON rel.oid = con.conrelid
577
- INNER JOIN pg_catalog.pg_namespace nsp
578
- ON nsp.oid = connamespace
579
- WHERE nsp.nspname = '${schema}' AND rel.relname = '${table}'; `
663
+ INNER JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
664
+ INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
665
+ WHERE nsp.nspname = '${escapeLiteral(schemaName)}' AND rel.relname = ${name};`
580
666
  }
581
667
 
582
- //find foreign key
583
668
  connection.foreignKeyList = (table) => {
584
- return `SELECT
585
- tc.table_schema,
586
- tc.constraint_name,
587
- tc.table_name,
588
- kcu.column_name,
589
- ccu.table_schema AS foreign_table_schema,
590
- ccu.table_name AS foreign_table_name,
591
- ccu.column_name AS foreign_column_name
592
- FROM information_schema.table_constraints AS tc
669
+ const name = schemaSql(table)
670
+ if (!name) return ''
671
+ return `SELECT tc.table_schema, tc.constraint_name, tc.table_name, kcu.column_name,
672
+ ccu.table_schema AS foreign_table_schema, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
673
+ FROM information_schema.table_constraints AS tc
593
674
  JOIN information_schema.key_column_usage AS kcu
594
- ON tc.constraint_name = kcu.constraint_name
595
- AND tc.table_schema = kcu.table_schema
675
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
596
676
  JOIN information_schema.constraint_column_usage AS ccu
597
677
  ON ccu.constraint_name = tc.constraint_name
598
- WHERE tc.constraint_type = 'FOREIGN KEY'
599
- AND tc.table_name='${table}';`
600
- }
601
-
602
- var toNumber = function (num) {
603
- num = num + ''
604
- var t = replaceAll(num, '.', '')
605
- if (t) {
606
- return parseFloat(t)
607
- } else return 0
608
- }
609
-
610
- function replaceAll(str, find, replace) {
611
- return str.replace(new RegExp(find, 'g'), replace)
678
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name=${name};`
612
679
  }
613
680
 
614
681
  module.exports = connection