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