zet-lib 6.1.0 → 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,127 +2,116 @@ 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
+ }
60
91
 
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
- const sanitizeReturning = (clause) => {
65
- if (typeof clause !== 'string' || !clause.trim()) return 'RETURNING *'
66
- const s = clause.trim()
67
- if (SAFE_RETURNING.test(s)) return s
68
- if (SAFE_RETURNING_COLS.test(s)) return s
69
- return 'RETURNING *'
92
+ const quoteTableName = (table) => {
93
+ const t = sanitizeTableName(table)
94
+ return t ? t.split('.').map((part) => `"${part}"`).join('.') : ''
70
95
  }
71
96
 
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('.')
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
79
104
  }
80
105
 
81
- // Whitelist operator perbandingan untuk WHERE
82
- const SAFE_WHERE_OPERATORS = new Set([
83
- '=', '!=', '<>', '<', '>', '<=', '>=',
84
- 'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE',
85
- 'IS NOT', 'IS', 'IN', 'NOT IN',
86
- 'BETWEEN', 'NOT BETWEEN', '@>',
87
- ])
106
+ const sanitizeColumnName = (key) => {
107
+ const col = String(key == null ? '' : key).trim()
108
+ return SAFE_IDENTIFIER_PART.test(col) ? col : null
109
+ }
88
110
 
89
- const OPERATOR_NAME_MAP = {
90
- '=': '=',
91
- EQ: '=',
92
- '!=': '<>',
93
- '<>': '<>',
94
- NE: '<>',
95
- '<': '<',
96
- LT: '<',
97
- '>': '>',
98
- GT: '>',
99
- '<=': '<=',
100
- LTE: '<=',
101
- '>=': '>=',
102
- GTE: '>=',
103
- LIKE: 'LIKE',
104
- ILIKE: 'ILIKE',
105
- 'NOT LIKE': 'NOT LIKE',
106
- NOTLIKE: 'NOT LIKE',
107
- 'NOT ILIKE': 'NOT ILIKE',
108
- NOTILIKE: 'NOT ILIKE',
109
- IN: 'IN',
110
- 'NOT IN': 'NOT IN',
111
- NOTIN: 'NOT IN',
112
- BETWEEN: 'BETWEEN',
113
- 'NOT BETWEEN': 'NOT BETWEEN',
114
- NOTBETWEEN: 'NOT BETWEEN',
115
- IS: 'IS',
116
- 'IS NOT': 'IS NOT',
117
- ISNOT: 'IS NOT',
118
- 'IS NULL': 'IS NULL',
119
- ISNULL: 'IS NULL',
120
- 'IS NOT NULL': 'IS NOT NULL',
121
- ISNOTNULL: 'IS NOT NULL',
122
- 'NOT NULL': 'IS NOT NULL',
123
- NOTNULL: 'IS NOT NULL',
124
- '@>': '@>',
125
- CONTAINS: '@>',
111
+ const sanitizeReturning = (clause) => {
112
+ if (typeof clause !== 'string' || !clause.trim()) return 'RETURNING *'
113
+ const s = clause.trim()
114
+ return (SAFE_RETURNING.test(s) || SAFE_RETURNING_COLS.test(s)) ? s : 'RETURNING *'
126
115
  }
127
116
 
128
117
  const normalizeOperator = (op) => {
@@ -131,79 +120,79 @@ const normalizeOperator = (op) => {
131
120
  return OPERATOR_NAME_MAP[key] || ''
132
121
  }
133
122
 
134
- const sanitizeWhereOption = (option) => {
135
- if (option == null || typeof option !== 'string') return ''
136
- const s = String(option).trim().toUpperCase().replace(/\s+/g, ' ')
137
- return SAFE_WHERE_OPERATORS.has(s) ? s : (normalizeOperator(s) || '')
138
- }
123
+ const sanitizeWhereOption = (option) => (typeof option === 'string' ? normalizeOperator(option) : '')
139
124
 
140
125
  const sanitizeWhereConnector = (op) => {
141
126
  if (op == null || typeof op !== 'string') return ' AND '
142
- const s = String(op).trim().toUpperCase()
143
- return (s === 'OR' ? ' OR ' : ' AND ')
127
+ return String(op).trim().toUpperCase() === 'OR' ? ' OR ' : ' AND '
144
128
  }
145
129
 
146
- const quoteIdent = (name) => sanitizeWhereField(name)
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
+ }
147
139
 
148
- const quoteTableName = (table) => {
149
- const t = sanitizeTableName(table || '')
140
+ const escapeLiteral = (value) => String(value == null ? '' : value).replace(/'/g, "''")
141
+
142
+ const schemaSql = (table) => {
143
+ const t = sanitizeTableName(table)
150
144
  if (!t) return ''
151
- return t.split('.').map((p) => `"${p}"`).join('.')
145
+ const name = t.includes('.') ? t.split('.').pop() : t
146
+ return `'${escapeLiteral(name)}'`
152
147
  }
153
148
 
154
- const META_WHERE_KEYS = new Set(['type', 'cast'])
155
- const STRING_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|@>)\s+([\s\S]+)$/i
156
- const KEY_OPERATOR_RE = /^(.+?)\s+(>=|<=|<>|!=|=|>|<|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|@>)$/i
149
+ const safeJson = (value) => {
150
+ if (typeof value === 'string') return value
151
+ try {
152
+ return JSON.stringify(value)
153
+ } catch (err) {
154
+ throw new Error('Invalid JSON value for query parameter')
155
+ }
156
+ }
157
+
158
+ const stripQuotes = (value) => String(value == null ? '' : value).trim().replace(/^['"]|['"]$/g, '')
157
159
 
158
160
  const parseListLiteral = (raw) => {
159
161
  if (Array.isArray(raw)) return raw
160
162
  const s = String(raw == null ? '' : raw).trim().replace(/^\(|\)$/g, '')
161
163
  if (!s) return []
162
- return s.split(',').map((v) => {
163
- const t = v.trim().replace(/^['"]|['"]$/g, '')
164
+ return s.split(',').map((item) => {
165
+ const t = stripQuotes(item)
164
166
  return /^-?\d+(\.\d+)?$/.test(t) ? Number(t) : t
165
167
  })
166
168
  }
167
169
 
168
170
  const parseRangeLiteral = (raw) => {
169
- if (Array.isArray(raw)) return raw
171
+ if (Array.isArray(raw)) return raw.slice(0, 2)
170
172
  const s = String(raw == null ? '' : raw).trim()
173
+ if (!s) return []
171
174
  const byAnd = s.split(/\s+AND\s+/i)
172
- if (byAnd.length >= 2) {
173
- return [
174
- byAnd[0].trim().replace(/^['"]|['"]$/g, ''),
175
- byAnd.slice(1).join(' AND ').trim().replace(/^['"]|['"]$/g, ''),
176
- ]
177
- }
175
+ if (byAnd.length >= 2) return [stripQuotes(byAnd[0]), stripQuotes(byAnd.slice(1).join(' AND '))]
178
176
  const byComma = s.split(',')
179
- if (byComma.length >= 2) {
180
- return [
181
- byComma[0].trim().replace(/^['"]|['"]$/g, ''),
182
- byComma.slice(1).join(',').trim().replace(/^['"]|['"]$/g, ''),
183
- ]
184
- }
185
- return [s, s]
177
+ if (byComma.length >= 2) return [stripQuotes(byComma[0]), stripQuotes(byComma.slice(1).join(','))]
178
+ return []
186
179
  }
187
180
 
188
181
  const isOperatorMap = (value) => {
189
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false
190
- if (value instanceof Date) return false
191
- if (value._isAMomentObject) return false
182
+ if (!isPlainObject(value) || value instanceof Date || value._isAMomentObject) return false
192
183
  const keys = Object.keys(value)
193
- if (!keys.length) return false
194
- return keys.every((k) => !!normalizeOperator(k) || META_WHERE_KEYS.has(String(k).toLowerCase()))
184
+ return keys.length > 0 && keys.every((key) => !!normalizeOperator(key) || META_WHERE_KEYS.has(String(key).toLowerCase()))
195
185
  }
196
186
 
197
187
  const fieldSqlWithCast = (fieldSql, cast) => {
198
188
  if (!cast) return fieldSql
199
189
  const c = String(cast).trim().toLowerCase()
200
- if (!/^[a-z][a-z0-9_]*(\(\d+\))?$/.test(c)) return fieldSql
201
- return `${fieldSql}::${c}`
190
+ return SAFE_CAST.test(c) ? `${fieldSql}::${c}` : fieldSql
202
191
  }
203
192
 
204
193
  const pushWhereCondition = (parts, params, increment, fieldSql, operator, value) => {
205
194
  const op = normalizeOperator(operator)
206
- if (!fieldSql || !op) return increment
195
+ if (!fieldSql || !op || !Number.isInteger(increment) || increment < 1) return increment
207
196
 
208
197
  if (op === 'IS NULL' || op === 'IS NOT NULL') {
209
198
  parts.push(`${fieldSql} ${op}`)
@@ -211,7 +200,7 @@ const pushWhereCondition = (parts, params, increment, fieldSql, operator, value)
211
200
  }
212
201
 
213
202
  if (op === 'IS' || op === 'IS NOT') {
214
- if (value === null || value === undefined || /^null$/i.test(String(value))) {
203
+ if (value == null || /^null$/i.test(String(value))) {
215
204
  parts.push(`${fieldSql} ${op} NULL`)
216
205
  return increment
217
206
  }
@@ -226,15 +215,14 @@ const pushWhereCondition = (parts, params, increment, fieldSql, operator, value)
226
215
  parts.push(op === 'IN' ? 'FALSE' : 'TRUE')
227
216
  return increment
228
217
  }
229
- const placeholders = list.map((_, i) => `$${increment + i}`).join(', ')
230
- parts.push(`${fieldSql} ${op} (${placeholders})`)
218
+ parts.push(`${fieldSql} ${op} (${list.map((_, i) => `$${increment + i}`).join(', ')})`)
231
219
  params.push(...list)
232
220
  return increment + list.length
233
221
  }
234
222
 
235
223
  if (op === 'BETWEEN' || op === 'NOT BETWEEN') {
236
- const range = Array.isArray(value) ? value : parseRangeLiteral(value)
237
- if (!range || range.length < 2) return increment
224
+ const range = parseRangeLiteral(value)
225
+ if (range.length < 2) return increment
238
226
  parts.push(`${fieldSql} ${op} $${increment} AND $${increment + 1}`)
239
227
  params.push(range[0], range[1])
240
228
  return increment + 2
@@ -242,7 +230,7 @@ const pushWhereCondition = (parts, params, increment, fieldSql, operator, value)
242
230
 
243
231
  if (op === '@>') {
244
232
  parts.push(`${fieldSql}::jsonb @> $${increment}::jsonb`)
245
- params.push(typeof value === 'string' ? value : JSON.stringify(value))
233
+ params.push(safeJson(value))
246
234
  return increment + 1
247
235
  }
248
236
 
@@ -254,7 +242,7 @@ const pushWhereCondition = (parts, params, increment, fieldSql, operator, value)
254
242
  const appendWhereEntry = (parts, params, increment, key, value) => {
255
243
  let fieldName = key
256
244
  let keyOp = ''
257
- const keyMatch = KEY_OPERATOR_RE.exec(String(key).trim())
245
+ const keyMatch = typeof key === 'string' ? KEY_OPERATOR_RE.exec(key.trim()) : null
258
246
  if (keyMatch) {
259
247
  fieldName = keyMatch[1]
260
248
  keyOp = normalizeOperator(keyMatch[2])
@@ -262,11 +250,7 @@ const appendWhereEntry = (parts, params, increment, key, value) => {
262
250
 
263
251
  const fieldSql = quoteIdent(fieldName)
264
252
  if (!fieldSql) return increment
265
-
266
- if (keyOp) {
267
- return pushWhereCondition(parts, params, increment, fieldSql, keyOp, value)
268
- }
269
-
253
+ if (keyOp) return pushWhereCondition(parts, params, increment, fieldSql, keyOp, value)
270
254
  if (value === null || value === undefined) {
271
255
  parts.push(`${fieldSql} IS NULL`)
272
256
  return increment
@@ -276,13 +260,9 @@ const appendWhereEntry = (parts, params, increment, key, value) => {
276
260
  if (value.length && typeof value[0] === 'string' && normalizeOperator(value[0])) {
277
261
  const op = normalizeOperator(value[0])
278
262
  const rest = value.slice(1)
279
- if (op === 'IN' || op === 'NOT IN') {
280
- const list = rest.length === 1 && Array.isArray(rest[0]) ? rest[0] : rest
281
- return pushWhereCondition(parts, params, increment, fieldSql, op, list)
282
- }
283
- if (op === 'BETWEEN' || op === 'NOT BETWEEN') {
284
- const range = rest.length === 1 && Array.isArray(rest[0]) ? rest[0] : rest
285
- return pushWhereCondition(parts, params, increment, fieldSql, op, range)
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)
286
266
  }
287
267
  return pushWhereCondition(parts, params, increment, fieldSql, op, rest[0])
288
268
  }
@@ -291,8 +271,7 @@ const appendWhereEntry = (parts, params, increment, key, value) => {
291
271
 
292
272
  if (isOperatorMap(value)) {
293
273
  const castRaw = value.cast || value.type
294
- const cast = String(castRaw || '').toLowerCase() === 'date' ? 'date' : castRaw
295
- const sqlField = fieldSqlWithCast(fieldSql, cast)
274
+ const sqlField = fieldSqlWithCast(fieldSql, String(castRaw || '').toLowerCase() === 'date' ? 'date' : castRaw)
296
275
  for (const [opKey, opVal] of Object.entries(value)) {
297
276
  if (META_WHERE_KEYS.has(String(opKey).toLowerCase())) continue
298
277
  increment = pushWhereCondition(parts, params, increment, sqlField, opKey, opVal)
@@ -315,379 +294,276 @@ const appendWhereEntry = (parts, params, increment, key, value) => {
315
294
  return pushWhereCondition(parts, params, increment, fieldSql, '=', value)
316
295
  }
317
296
 
318
- const formatSelect = (select) => {
319
- if (Array.isArray(select)) {
320
- const cols = select.map((item) => {
321
- if (typeof item !== 'string') return ''
322
- const trimmed = item.trim()
323
- if (!trimmed) return ''
324
- return quoteIdent(trimmed) || trimmed
325
- }).filter(Boolean)
326
- return cols.length ? cols.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])
327
301
  }
328
- if (typeof select === 'string' && select.trim()) return select
329
- return '*'
330
- }
331
-
332
- const formatJoins = (joins) => {
333
- if (!Array.isArray(joins) || !joins.length) return ''
334
- return joins.map((item) => {
335
- if (typeof item === 'string') return item
336
- if (!item || typeof item !== 'object') return ''
337
- const type = String(item.type || 'LEFT').trim().toUpperCase()
338
- const allowed = new Set(['LEFT', 'RIGHT', 'INNER', 'FULL', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'CROSS'])
339
- if (!allowed.has(type)) return ''
340
- const table = quoteTableName(item.table || '')
341
- if (!table) return ''
342
- const alias = item.as ? ` ${quoteIdent(item.as) || String(item.as).trim()}` : ''
343
- if (type === 'CROSS') return `CROSS JOIN ${table}${alias}`
344
- const on = String(item.on || '').trim()
345
- if (!on) return ''
346
- return `${type} JOIN ${table}${alias} ON ${on}`
347
- }).filter(Boolean).join(' ')
302
+ return increment
348
303
  }
349
304
 
350
- const groupByFn = (obj) => {
351
- const g = obj.groupBy || obj.group_by
352
- if (!g) return ''
353
- const list = Array.isArray(g) ? g : String(g).split(',')
354
- const cols = list.map((item) => {
355
- const trimmed = String(item || '').trim()
356
- return quoteIdent(trimmed) || sanitizeOrderBySegment(trimmed)
357
- }).filter(Boolean)
358
- return cols.length ? ` GROUP BY ${cols.join(', ')} ` : ''
359
- }
360
-
361
- const havingFn = (obj, increment, arr) => {
362
- const having = obj.having || {}
363
- if (!having || typeof having !== 'object' || Array.isArray(having)) {
364
- return { sql: '', increment }
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] }
365
308
  }
366
- const parts = []
367
- for (const key in having) {
368
- increment = appendWhereEntry(parts, arr, increment, key, having[key])
369
- }
370
- return { sql: parts.length ? ` HAVING ${parts.join(' AND ')} ` : '', increment }
309
+ return isPlainObject(raw) ? raw : null
371
310
  }
372
311
 
373
- const connection = {}
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
374
316
 
375
- connection.query = async (string, arr) => {
376
- try {
377
- /*console.log(string);
378
- console.log(JSON.stringify(arr))*/
379
- const result = await pool.query(string, arr)
380
- return result.rows
381
- } catch (e) {
382
- console.log(string)
383
- console.log(e)
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
384
322
  }
385
- }
386
323
 
387
- const orderByFn = (obj) => {
388
- const objOrderby = !obj.orderBy ? [] : obj.orderBy
389
- let orderBy = ''
390
- if (objOrderby.length) {
391
- orderBy = ` ORDER BY `
392
- for (var i = 0; i < objOrderby.length; i++) {
393
- if (i % 2 == 0) {
394
- orderBy += ` "${obj.orderBy[i]}" `
395
- } else {
396
- orderBy += ` ${obj.orderBy[i]} `
397
- if (i == objOrderby.length - 1) {
398
- orderBy += ` `
399
- } else {
400
- orderBy += `, `
401
- }
402
- }
403
- }
324
+ if (type === 'inline') {
325
+ if (item.value == null) return increment
326
+ parts.push(field ? `${field} ${item.value}` : String(item.value))
327
+ return increment
404
328
  }
405
- if (obj.hasOwnProperty('order_by') && Array.isArray(obj.order_by) && obj.order_by.length) {
406
- const segments = obj.order_by.map(sanitizeOrderBySegment).filter(Boolean)
407
- if (segments.length) {
408
- orderBy = ` ORDER BY ${segments.join(', ')} `
409
- }
329
+
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
410
340
  }
411
341
 
412
- return orderBy
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)
351
+ }
352
+ return pushWhereCondition(parts, params, increment, field, option, item.value)
413
353
  }
414
354
 
415
355
  const whereFn = (obj, startIncrement = 1) => {
416
- const where = obj.where || {}
417
- const orWhere = obj.orWhere || obj.or_where || {}
418
- // [{field:"join_date",option:">=",value:"2025-02-12",operator:"AND",type:"text,json,date,inline"}]
419
- // atau bentuk singkat: ["join_date", ">=", "2025-02-12"]
420
- const whereArray = obj.whereArray || []
421
- let increment = startIncrement
422
- const arr = []
356
+ const incrementStart = Number.isInteger(startIncrement) && startIncrement > 0 ? startIncrement : 1
357
+ const params = []
423
358
  const parts = []
359
+ let increment = incrementStart
424
360
 
425
- if (where && typeof where === 'object' && !Array.isArray(where)) {
426
- for (const key in where) {
427
- increment = appendWhereEntry(parts, arr, increment, key, where[key])
428
- }
429
- }
361
+ increment = appendWhereObject(parts, params, increment, obj && obj.where)
430
362
 
431
363
  const orParts = []
432
- if (orWhere && typeof orWhere === 'object' && !Array.isArray(orWhere)) {
433
- for (const key in orWhere) {
434
- increment = appendWhereEntry(orParts, arr, increment, key, orWhere[key])
435
- }
436
- }
437
- if (orParts.length) {
438
- parts.push(`(${orParts.join(' OR ')})`)
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
439
385
  }
440
386
 
441
- let wherequery = parts.join(' AND ')
442
- let hasWhere = parts.length > 0
443
-
444
- if (whereArray.length) {
445
- let andOr = wherequery ? ' AND ' : ''
446
- whereArray.forEach((rawItem, index) => {
447
- let item = rawItem
448
- if (Array.isArray(item)) {
449
- item = {
450
- field: item[0],
451
- option: item[1],
452
- value: item[2],
453
- operator: item[3] || 'AND',
454
- type: item[4],
455
- }
456
- }
457
- if (!item || typeof item !== 'object') return
387
+ return {
388
+ where: sql ? `WHERE ${sql}` : '',
389
+ arr: params,
390
+ increment,
391
+ }
392
+ }
458
393
 
459
- const type = !item.type ? 'text' : item.type
460
- if (index > 0) {
461
- andOr = ''
462
- }
463
- let operator = !item.operator ? ' AND ' : item.operator
464
- if (index == whereArray.length - 1) {
465
- operator = ''
466
- }
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(', ') : '*'
400
+ }
401
+ return typeof select === 'string' && select.trim() ? select : '*'
402
+ }
467
403
 
468
- const quotedField = item.field ? quoteIdent(item.field) : ''
469
- let field = ''
470
- if (type == 'date') {
471
- field = quotedField ? `${quotedField}::text ` : ''
472
- } else {
473
- field = quotedField || ''
474
- }
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(' ')
475
416
 
476
- if (item.isJSON || type == 'json' || type == 'jsonb') {
477
- if (quotedField) {
478
- wherequery += andOr + ` (${quotedField})::jsonb @> $${increment}::jsonb ${operator}`
479
- arr.push(typeof item.value === 'string' ? item.value : JSON.stringify(item.value))
480
- increment++
481
- hasWhere = true
482
- }
483
- } else if (type == 'inline') {
484
- if (field) {
485
- wherequery += andOr + ` ${field} ${item.value} ${operator}`
486
- } else {
487
- wherequery += andOr + ` ${item.value} ${operator}`
488
- }
489
- hasWhere = true
490
- } else {
491
- const rawOption = item.option == null ? '=' : String(item.option)
492
- if (rawOption.includes('{{value}}')) {
493
- const option = rawOption.replace('{{value}}', `$${increment}`)
494
- wherequery += `${andOr} ${field} ${option} ${operator}`
495
- increment++
496
- arr.push(item.value)
497
- hasWhere = true
498
- } else if (rawOption.includes('$')) {
499
- wherequery += `${andOr} ${field} ${rawOption} ${operator}`
500
- increment++
501
- arr.push(item.value)
502
- hasWhere = true
503
- } else {
504
- const option = sanitizeWhereOption(rawOption) || '='
505
- if (Array.isArray(item.value) && (option === '=' || option === 'IN' || option === 'NOT IN')) {
506
- const listOp = option === 'NOT IN' ? 'NOT IN' : 'IN'
507
- const list = item.value
508
- if (!list.length) {
509
- wherequery += `${andOr} ${listOp === 'NOT IN' ? 'TRUE' : 'FALSE'} ${operator}`
510
- } else {
511
- const placeholders = list.map((_, i) => `$${increment + i}`).join(', ')
512
- wherequery += `${andOr} ${field} ${listOp} (${placeholders}) ${operator}`
513
- arr.push(...list)
514
- increment += list.length
515
- }
516
- hasWhere = true
517
- } else if (option === 'BETWEEN' || option === 'NOT BETWEEN') {
518
- const range = Array.isArray(item.value) ? item.value : parseRangeLiteral(item.value)
519
- if (range && range.length >= 2) {
520
- wherequery += `${andOr} ${field} ${option} $${increment} AND $${increment + 1} ${operator}`
521
- arr.push(range[0], range[1])
522
- increment += 2
523
- hasWhere = true
524
- }
525
- } else if ((option === 'IS' || option === 'IS NOT') && (item.value == null || /^null$/i.test(String(item.value)))) {
526
- wherequery += `${andOr} ${field} ${option} NULL ${operator}`
527
- hasWhere = true
528
- } else if (option === 'IS NULL' || option === 'IS NOT NULL') {
529
- wherequery += `${andOr} ${field} ${option} ${operator}`
530
- hasWhere = true
531
- } else {
532
- wherequery += `${andOr} ${field} ${option} $${increment} ${operator}`
533
- increment++
534
- let itemValue = item.value
535
- if (option === '=') {
536
- itemValue = Util.replaceAll(itemValue + '', '%', '')
537
- }
538
- arr.push(itemValue)
539
- hasWhere = true
540
- }
541
- }
542
- }
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)
543
424
  })
544
- }
425
+ .filter(Boolean)
426
+ return cols.length ? ` GROUP BY ${cols.join(', ')}` : ''
427
+ }
545
428
 
546
- if (arr.length > 0) {
547
- hasWhere = true
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
+ }
434
+
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(', ')}` : ''
548
441
  }
549
442
 
550
- return {
551
- where: hasWhere && wherequery ? `WHERE ${wherequery}` : '',
552
- arr,
553
- increment,
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)
554
450
  }
451
+ return segments.length ? ` ORDER BY ${segments.join(', ')}` : ''
555
452
  }
556
453
 
557
454
  const buildSelectQuery = (obj) => {
558
- const tableSql = obj.table ? quoteTableName(obj.table) : '""'
559
- const select = formatSelect(obj.select)
560
- const distinct = obj.distinct ? 'DISTINCT ' : ''
561
- const statement = obj.statement || ''
562
- const limitVal = toSafeNonNegativeInt(obj.limit, null)
563
- const offsetVal = obj.hasOwnProperty('offset') ? toSafeNonNegativeInt(obj.offset, 0) : (obj.limit ? 0 : null)
564
- const limit = limitVal !== null ? ` LIMIT ${limitVal} ` : ''
565
- const offset = offsetVal !== null ? ` OFFSET ${offsetVal} ` : ''
566
- const orderBy = orderByFn(obj)
567
- const values = obj.values || []
568
- const join = formatJoins(obj.joins || obj.join || [])
569
- const groupBy = groupByFn(obj)
455
+ if (!isPlainObject(obj)) throw new Error('Query options must be an object')
456
+ const tableSql = requireTableName(obj.table, 'select')
570
457
  const whereObj = whereFn(obj)
571
458
  const havingObj = havingFn(obj, whereObj.increment, whereObj.arr)
572
- const arr = whereObj.arr
573
- const sql = `SELECT ${distinct}${select} FROM ${tableSql} ${join} ${whereObj.where} ${groupBy} ${havingObj.sql} ${statement} ${orderBy} ${limit} ${offset}`.replace(/\s+/g, ' ').trim()
574
- return { sql, arr, values }
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),
478
+ }
575
479
  }
576
480
 
577
- connection.results = async (obj) => {
578
- const { sql, arr, values } = buildSelectQuery(obj)
481
+ const requireDataObject = (data, action) => {
482
+ if (!isPlainObject(data)) throw new Error(`Data object is required for ${action}`)
483
+ return { ...data }
484
+ }
485
+
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 = {}
500
+
501
+ connection.query = async (string, arr) => {
579
502
  try {
580
- const result = await pool.query(sql, arr.length ? arr : values.length ? values : null)
581
- return !result.rows ? [] : result.rows
582
- } catch (e) {
583
- console.log(sql)
584
- console.log(arr)
585
- console.log(e.toString())
586
- throw e
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 []
587
509
  }
588
510
  }
589
511
 
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
+
590
518
  connection.sql = async (obj) => {
591
- const { sql, arr } = buildSelectQuery(obj)
592
- return { sql, arr }
519
+ try {
520
+ const built = buildSelectQuery(obj)
521
+ return { sql: built.sql, arr: built.arr }
522
+ } catch (err) {
523
+ return { sql: '', arr: [], error: err.message }
524
+ }
593
525
  }
594
526
 
595
527
  connection.result = async (obj) => {
596
528
  const results = await connection.results(obj)
597
- if (results.length) {
598
- return results[0]
599
- } else {
600
- return []
601
- }
529
+ return Array.isArray(results) && results.length ? results[0] : []
602
530
  }
603
531
 
604
532
  connection.insert = async (obj) => {
605
- const tableSql = quoteTableName(obj.table || '')
606
- if (!tableSql) throw new Error('Table name is required for insert')
607
- const data = { ...obj.data }
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')
608
536
  const returning = sanitizeReturning(data.returning || 'RETURNING *')
609
537
  delete data.returning
610
- let increment = 1
611
- const datas = []
612
- const values = []
613
- const arr = []
614
- for (const key in data) {
615
- const col = sanitizeColumnName(key)
616
- if (col) {
617
- datas.push(col)
618
- values.push(`$${increment}`)
619
- arr.push(data[key])
620
- increment++
621
- }
622
- }
623
- if (datas.length === 0) throw new Error('At least one valid column is required for insert')
624
- const sql = `INSERT INTO ${tableSql} ("${datas.join('","')}") VALUES (${values.join(',')}) ${returning}`
625
-
626
- try {
627
- const results = await pool.query(sql, arr)
628
- return results.rows[0]
629
- } catch (e) {
630
- console.log(sql)
631
- console.log(arr)
632
- console.log(e.toString())
633
- throw e
634
- }
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]
635
542
  }
636
543
 
637
544
  connection.update = async (obj) => {
638
- const tableSql = quoteTableName(obj.table || '')
639
- if (!tableSql) throw new Error('Table name is required for update')
640
- const data = { ...obj.data }
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')
641
548
  const returning = sanitizeReturning(data.returning || 'RETURNING *')
642
549
  delete data.returning
643
-
644
- const arr = []
645
- const dataArr = []
646
- let increment = 1
647
-
648
- for (const key in data) {
649
- const col = sanitizeColumnName(key)
650
- if (col) {
651
- dataArr.push(`"${col}" = $${increment}`)
652
- arr.push(data[key])
653
- increment++
654
- }
655
- }
656
- if (dataArr.length === 0) throw new Error('At least one valid column is required for update')
657
-
658
- const whereObj = whereFn(obj, increment)
659
- arr.push(...whereObj.arr)
660
- const wheres = whereObj.where ? ` ${whereObj.where}` : ''
661
- const sql = `UPDATE ${tableSql} SET ${dataArr.join(', ')}${wheres} ${returning}`
662
-
663
- try {
664
- const result = await pool.query(sql, arr)
665
- return result.rows[0]
666
- } catch (e) {
667
- console.log(sql)
668
- console.log(arr)
669
- console.log(e.toString())
670
- throw e
671
- }
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]
672
555
  }
673
556
 
674
557
  connection.delete = async (obj) => {
675
- const tableSql = quoteTableName(obj.table || '')
676
- if (!tableSql) throw new Error('Table name is required for delete')
558
+ if (!isPlainObject(obj)) throw new Error('Delete options must be an object')
559
+ const tableSql = requireTableName(obj.table, 'delete')
677
560
  const whereObj = whereFn(obj, 1)
678
- const wheres = whereObj.where ? ` ${whereObj.where}` : ''
679
- const sql = `DELETE FROM ${tableSql}${wheres} RETURNING *`
680
- try {
681
- return await pool.query(sql, whereObj.arr)
682
- } catch (e) {
683
- console.log(sql)
684
- console.log(whereObj.arr)
685
- console.log(e.toString())
686
- throw e
687
- }
561
+ const sql = `DELETE FROM ${tableSql}${whereObj.where ? ` ${whereObj.where}` : ''} RETURNING *`
562
+ return runQuery(sql, whereObj.arr)
688
563
  }
689
564
 
690
565
  connection.count = async (obj) => {
566
+ if (!isPlainObject(obj)) throw new Error('Count options must be an object')
691
567
  const countSelect = obj.select && /count\s*\(/i.test(String(obj.select))
692
568
  ? obj.select
693
569
  : `COUNT(${obj.countField ? (quoteIdent(obj.countField) || '*') : '*'}) AS count`
@@ -701,205 +577,105 @@ connection.count = async (obj) => {
701
577
  distinct: undefined,
702
578
  })
703
579
  if (!row || Array.isArray(row)) return 0
704
- return Number(row.count || 0)
580
+ const n = Number(row.count)
581
+ return Number.isFinite(n) ? n : 0
705
582
  }
706
583
 
707
- connection.insertData = async(tableName, data) => {
708
- try {
709
- // Validasi input
710
- if (!data || typeof data !== 'object') {
711
- throw new Error('Invalid data provided for insert');
712
- }
584
+ connection.insertData = async (tableName, data) => connection.insert({ table: tableName, data })
713
585
 
714
- const columns = Object.keys(data);
715
- if (columns.length === 0) {
716
- throw new Error('No columns found in the data object');
717
- }
718
-
719
- const placeholders = columns.map((_, index) => `$${index + 1}`).join(',');
720
- const insertQuery = `
721
- INSERT INTO "${tableName}" (${columns.map(col => `"${col}"`).join(',')})
722
- VALUES (${placeholders})
723
- RETURNING *
724
- `;
725
- const values = columns.map(col => data[col]);
726
- const result = await pool.query(insertQuery, values);
727
- console.log(`Successfully inserted 1 record into ${tableName}`);
728
- return result.rows[0];
729
- } catch (error) {
730
- console.error(`Error inserting record into ${tableName}:`, error);
731
- throw error;
732
- } 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')
733
589
  }
590
+ const result = await connection.delete({ table: tableName, where })
591
+ return result && Number.isInteger(result.rowCount) ? result.rowCount : 0
734
592
  }
735
593
 
736
- connection.deleteData = async(tableName, where) => {
737
- try {
738
- // Validasi input
739
- if (!where || typeof where !== 'object') {
740
- throw new Error('Invalid where clause provided for delete');
741
- }
742
-
743
- const whereColumns = Object.keys(where);
744
- if (whereColumns.length === 0) {
745
- throw new Error('No where conditions provided for delete');
746
- }
747
-
748
- const whereConditions = whereColumns.map((col, index) => `"${col}" = $${index + 1}`).join(' AND ');
749
- const deleteQuery = `
750
- DELETE FROM "${tableName}"
751
- WHERE ${whereConditions}
752
- RETURNING *
753
- `;
754
- const values = whereColumns.map(col => where[col]);
755
- const result = await pool.query(deleteQuery, values);
756
- console.log(`Successfully deleted ${result.rowCount} records from ${tableName}`);
757
- return result.rowCount;
758
- } catch (error) {
759
- console.error(`Error deleting records from ${tableName}:`, error);
760
- throw error;
761
- } finally {
762
- }
763
- }
764
-
765
- connection.insertMultipleRecords = async(tableName,records) => {
766
- try {
767
- // Validasi input
768
- if (!records || !Array.isArray(records) || records.length === 0) {
769
- console.warn(`No records to insert into ${tableName}`);
770
- return [];
771
- }
772
-
773
- // Validasi bahwa semua records memiliki struktur yang sama
774
- const firstRecord = records[0];
775
- if (!firstRecord || typeof firstRecord !== 'object') {
776
- throw new Error('First record is invalid or empty');
777
- }
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')
778
599
 
779
- const columns = Object.keys(firstRecord);
780
- if (columns.length === 0) {
781
- throw new Error('No columns found in the first record');
782
- }
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')
783
603
 
784
- // Validasi bahwa semua records memiliki kolom yang sama
785
- for (let i = 1; i < records.length; i++) {
786
- const record = records[i];
787
- if (!record || typeof record !== 'object') {
788
- throw new Error(`Record at index ${i} is invalid`);
789
- }
790
- const recordColumns = Object.keys(record);
791
- if (recordColumns.length !== columns.length || !recordColumns.every(col => columns.includes(col))) {
792
- throw new Error(`Record at index ${i} has different structure than the first record`);
793
- }
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`)
794
609
  }
610
+ })
795
611
 
796
- const placeholders = records.map((_, rowIndex) =>
797
- `(${columns.map((_, colIndex) => `$${rowIndex * columns.length + colIndex + 1}`).join(',')})`
798
- ).join(',');
799
-
800
- // Create the INSERT query
801
- const insertQuery = `
802
- INSERT INTO "${tableName}" (${columns.map(col => `"${col}"`).join(',')})
803
- VALUES ${placeholders}
804
- RETURNING *
805
- `;
806
- const values = records.flatMap(record => columns.map(col => record[col]));
807
- const result = await pool.query(insertQuery, values);
808
- console.log(`Successfully inserted ${records.length} records into ${tableName}`);
809
- return result.rows;
810
- } catch (error) {
811
- console.error(`Error inserting multiple records into ${tableName}:`, error);
812
- throw error;
813
- } finally {
814
- }
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))
815
618
  }
816
619
 
817
- connection.driver = config.driver
620
+ connection.driver = (config && config.parsed && config.parsed.DRIVER) || 'pg'
818
621
  connection.showTables = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'"
819
- connection.showFullFields = (tableRelations) => {
820
- return `SELECT
821
- column_name AS "Field", concat(data_type,'(',character_maximum_length,')') AS "Type" , is_nullable AS "Null"
822
- FROM
823
- information_schema.COLUMNS
824
- WHERE
825
- TABLE_NAME = '${tableRelations}';`
826
- }
827
622
 
828
- connection.describeTable = (table) => {
829
- return connection.showFullFields(table)
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};`
830
629
  }
831
630
 
631
+ connection.describeTable = (table) => connection.showFullFields(table)
632
+
832
633
  connection.showComments = (table) => {
833
- 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"
834
637
  FROM pg_catalog.pg_statio_all_tables as st
835
638
  inner join pg_catalog.pg_description pgd on (pgd.objoid=st.relid)
836
639
  inner join information_schema.columns c on (pgd.objsubid=c.ordinal_position
837
640
  and c.table_schema=st.schemaname and c.table_name=st.relname)
838
- WHERE c.table_name = '${table}' ORDER BY c.column_name`
641
+ WHERE c.table_name = ${name} ORDER BY c.column_name`
839
642
  }
840
643
 
841
644
  connection.showFields = (table) => {
842
- return `
843
- SELECT
844
- tc.table_name AS "TABLE_NAME",
845
- kcu.column_name AS "COLUMN_NAME",
846
- tc.constraint_name AS "CONSTRAINT_NAME",
847
- ccu.table_name AS "REFERENCED_TABLE_NAME",
848
- ccu.column_name AS "REFERENCED_COLUMN_NAME",
849
- tc.table_schema
850
- FROM
851
- 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
852
650
  JOIN information_schema.key_column_usage AS kcu
853
- ON tc.constraint_name = kcu.constraint_name
854
- AND tc.table_schema = kcu.table_schema
651
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
855
652
  JOIN information_schema.constraint_column_usage AS ccu
856
- ON ccu.constraint_name = tc.constraint_name
857
- AND ccu.table_schema = tc.table_schema
858
- 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};`
859
655
  }
860
656
 
861
- //list constraint list
862
657
  connection.constraintList = (table, schema = 'public') => {
863
- return `
864
- SELECT con.*
658
+ const name = schemaSql(table)
659
+ const schemaName = sanitizeTableName(schema)
660
+ if (!name || !schemaName) return ''
661
+ return `SELECT con.*
865
662
  FROM pg_catalog.pg_constraint con
866
- INNER JOIN pg_catalog.pg_class rel
867
- ON rel.oid = con.conrelid
868
- INNER JOIN pg_catalog.pg_namespace nsp
869
- ON nsp.oid = connamespace
870
- 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};`
871
666
  }
872
667
 
873
- //find foreign key
874
668
  connection.foreignKeyList = (table) => {
875
- return `SELECT
876
- tc.table_schema,
877
- tc.constraint_name,
878
- tc.table_name,
879
- kcu.column_name,
880
- ccu.table_schema AS foreign_table_schema,
881
- ccu.table_name AS foreign_table_name,
882
- ccu.column_name AS foreign_column_name
883
- 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
884
674
  JOIN information_schema.key_column_usage AS kcu
885
- ON tc.constraint_name = kcu.constraint_name
886
- AND tc.table_schema = kcu.table_schema
675
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
887
676
  JOIN information_schema.constraint_column_usage AS ccu
888
677
  ON ccu.constraint_name = tc.constraint_name
889
- WHERE tc.constraint_type = 'FOREIGN KEY'
890
- AND tc.table_name='${table}';`
891
- }
892
-
893
- var toNumber = function (num) {
894
- num = num + ''
895
- var t = replaceAll(num, '.', '')
896
- if (t) {
897
- return parseFloat(t)
898
- } else return 0
899
- }
900
-
901
- function replaceAll(str, find, replace) {
902
- return str.replace(new RegExp(find, 'g'), replace)
678
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name=${name};`
903
679
  }
904
680
 
905
681
  module.exports = connection