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 +404 -628
- package/lib/generatorApi.js +578 -1129
- package/lib/generatorApp.js +468 -1264
- package/lib/generatorModel.js +418 -728
- package/package.json +1 -1
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
|
|
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,
|
|
12
|
-
idleTimeoutMillis: 1000,
|
|
13
|
-
connectionTimeoutMillis: 10000,
|
|
14
|
-
maxUses: 7500,
|
|
15
|
-
poolSize: 50,
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
return n
|
|
76
|
+
return Number.isInteger(n) && n >= 0 ? n : fallback
|
|
41
77
|
}
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
return
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
90
|
-
'
|
|
91
|
-
|
|
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
|
-
|
|
143
|
-
return (s === 'OR' ? ' OR ' : ' AND ')
|
|
127
|
+
return String(op).trim().toUpperCase() === 'OR' ? ' OR ' : ' AND '
|
|
144
128
|
}
|
|
145
129
|
|
|
146
|
-
const
|
|
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
|
|
149
|
-
|
|
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
|
-
|
|
145
|
+
const name = t.includes('.') ? t.split('.').pop() : t
|
|
146
|
+
return `'${escapeLiteral(name)}'`
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
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((
|
|
163
|
-
const t =
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
237
|
-
if (
|
|
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(
|
|
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(
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
return pushWhereCondition(parts, params, increment, fieldSql, op,
|
|
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
|
|
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
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
|
417
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
559
|
-
const
|
|
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
|
|
573
|
-
const
|
|
574
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
const 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
|
-
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
const 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
|
|
645
|
-
const
|
|
646
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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.
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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.
|
|
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.
|
|
829
|
-
|
|
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
|
-
|
|
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 =
|
|
641
|
+
WHERE c.table_name = ${name} ORDER BY c.column_name`
|
|
839
642
|
}
|
|
840
643
|
|
|
841
644
|
connection.showFields = (table) => {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
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
|
-
|
|
864
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|