zet-lib 6.0.2 → 6.1.0
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 +436 -145
- package/package.json +1 -1
package/lib/connection.js
CHANGED
|
@@ -79,11 +79,62 @@ const sanitizeWhereField = (field) => {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Whitelist operator perbandingan untuk WHERE
|
|
82
|
-
const SAFE_WHERE_OPERATORS = new Set([
|
|
82
|
+
const SAFE_WHERE_OPERATORS = new Set([
|
|
83
|
+
'=', '!=', '<>', '<', '>', '<=', '>=',
|
|
84
|
+
'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE',
|
|
85
|
+
'IS NOT', 'IS', 'IN', 'NOT IN',
|
|
86
|
+
'BETWEEN', 'NOT BETWEEN', '@>',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
const OPERATOR_NAME_MAP = {
|
|
90
|
+
'=': '=',
|
|
91
|
+
EQ: '=',
|
|
92
|
+
'!=': '<>',
|
|
93
|
+
'<>': '<>',
|
|
94
|
+
NE: '<>',
|
|
95
|
+
'<': '<',
|
|
96
|
+
LT: '<',
|
|
97
|
+
'>': '>',
|
|
98
|
+
GT: '>',
|
|
99
|
+
'<=': '<=',
|
|
100
|
+
LTE: '<=',
|
|
101
|
+
'>=': '>=',
|
|
102
|
+
GTE: '>=',
|
|
103
|
+
LIKE: 'LIKE',
|
|
104
|
+
ILIKE: 'ILIKE',
|
|
105
|
+
'NOT LIKE': 'NOT LIKE',
|
|
106
|
+
NOTLIKE: 'NOT LIKE',
|
|
107
|
+
'NOT ILIKE': 'NOT ILIKE',
|
|
108
|
+
NOTILIKE: 'NOT ILIKE',
|
|
109
|
+
IN: 'IN',
|
|
110
|
+
'NOT IN': 'NOT IN',
|
|
111
|
+
NOTIN: 'NOT IN',
|
|
112
|
+
BETWEEN: 'BETWEEN',
|
|
113
|
+
'NOT BETWEEN': 'NOT BETWEEN',
|
|
114
|
+
NOTBETWEEN: 'NOT BETWEEN',
|
|
115
|
+
IS: 'IS',
|
|
116
|
+
'IS NOT': 'IS NOT',
|
|
117
|
+
ISNOT: 'IS NOT',
|
|
118
|
+
'IS NULL': 'IS NULL',
|
|
119
|
+
ISNULL: 'IS NULL',
|
|
120
|
+
'IS NOT NULL': 'IS NOT NULL',
|
|
121
|
+
ISNOTNULL: 'IS NOT NULL',
|
|
122
|
+
'NOT NULL': 'IS NOT NULL',
|
|
123
|
+
NOTNULL: 'IS NOT NULL',
|
|
124
|
+
'@>': '@>',
|
|
125
|
+
CONTAINS: '@>',
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalizeOperator = (op) => {
|
|
129
|
+
if (op == null) return ''
|
|
130
|
+
const key = String(op).trim().replace(/_/g, ' ').replace(/\s+/g, ' ').toUpperCase()
|
|
131
|
+
return OPERATOR_NAME_MAP[key] || ''
|
|
132
|
+
}
|
|
133
|
+
|
|
83
134
|
const sanitizeWhereOption = (option) => {
|
|
84
135
|
if (option == null || typeof option !== 'string') return ''
|
|
85
|
-
const s = String(option).trim().toUpperCase()
|
|
86
|
-
return SAFE_WHERE_OPERATORS.has(s) ? s : ''
|
|
136
|
+
const s = String(option).trim().toUpperCase().replace(/\s+/g, ' ')
|
|
137
|
+
return SAFE_WHERE_OPERATORS.has(s) ? s : (normalizeOperator(s) || '')
|
|
87
138
|
}
|
|
88
139
|
|
|
89
140
|
const sanitizeWhereConnector = (op) => {
|
|
@@ -92,6 +143,233 @@ const sanitizeWhereConnector = (op) => {
|
|
|
92
143
|
return (s === 'OR' ? ' OR ' : ' AND ')
|
|
93
144
|
}
|
|
94
145
|
|
|
146
|
+
const quoteIdent = (name) => sanitizeWhereField(name)
|
|
147
|
+
|
|
148
|
+
const quoteTableName = (table) => {
|
|
149
|
+
const t = sanitizeTableName(table || '')
|
|
150
|
+
if (!t) return ''
|
|
151
|
+
return t.split('.').map((p) => `"${p}"`).join('.')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const META_WHERE_KEYS = new Set(['type', 'cast'])
|
|
155
|
+
const STRING_OPERATOR_RE = /^(>=|<=|<>|!=|=|>|<|NOT\s+ILIKE|NOT\s+LIKE|ILIKE|LIKE|NOT\s+IN|IN|NOT\s+BETWEEN|BETWEEN|IS\s+NOT\s+NULL|IS\s+NULL|IS\s+NOT|IS|@>)\s+([\s\S]+)$/i
|
|
156
|
+
const KEY_OPERATOR_RE = /^(.+?)\s+(>=|<=|<>|!=|=|>|<|NOT\s+ILIKE|NOT\s+LIKE|ILIKE|LIKE|NOT\s+IN|IN|NOT\s+BETWEEN|BETWEEN|IS\s+NOT\s+NULL|IS\s+NULL|IS\s+NOT|IS|@>)$/i
|
|
157
|
+
|
|
158
|
+
const parseListLiteral = (raw) => {
|
|
159
|
+
if (Array.isArray(raw)) return raw
|
|
160
|
+
const s = String(raw == null ? '' : raw).trim().replace(/^\(|\)$/g, '')
|
|
161
|
+
if (!s) return []
|
|
162
|
+
return s.split(',').map((v) => {
|
|
163
|
+
const t = v.trim().replace(/^['"]|['"]$/g, '')
|
|
164
|
+
return /^-?\d+(\.\d+)?$/.test(t) ? Number(t) : t
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const parseRangeLiteral = (raw) => {
|
|
169
|
+
if (Array.isArray(raw)) return raw
|
|
170
|
+
const s = String(raw == null ? '' : raw).trim()
|
|
171
|
+
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
|
+
}
|
|
178
|
+
const byComma = s.split(',')
|
|
179
|
+
if (byComma.length >= 2) {
|
|
180
|
+
return [
|
|
181
|
+
byComma[0].trim().replace(/^['"]|['"]$/g, ''),
|
|
182
|
+
byComma.slice(1).join(',').trim().replace(/^['"]|['"]$/g, ''),
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
return [s, s]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isOperatorMap = (value) => {
|
|
189
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
190
|
+
if (value instanceof Date) return false
|
|
191
|
+
if (value._isAMomentObject) return false
|
|
192
|
+
const keys = Object.keys(value)
|
|
193
|
+
if (!keys.length) return false
|
|
194
|
+
return keys.every((k) => !!normalizeOperator(k) || META_WHERE_KEYS.has(String(k).toLowerCase()))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fieldSqlWithCast = (fieldSql, cast) => {
|
|
198
|
+
if (!cast) return fieldSql
|
|
199
|
+
const c = String(cast).trim().toLowerCase()
|
|
200
|
+
if (!/^[a-z][a-z0-9_]*(\(\d+\))?$/.test(c)) return fieldSql
|
|
201
|
+
return `${fieldSql}::${c}`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const pushWhereCondition = (parts, params, increment, fieldSql, operator, value) => {
|
|
205
|
+
const op = normalizeOperator(operator)
|
|
206
|
+
if (!fieldSql || !op) return increment
|
|
207
|
+
|
|
208
|
+
if (op === 'IS NULL' || op === 'IS NOT NULL') {
|
|
209
|
+
parts.push(`${fieldSql} ${op}`)
|
|
210
|
+
return increment
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (op === 'IS' || op === 'IS NOT') {
|
|
214
|
+
if (value === null || value === undefined || /^null$/i.test(String(value))) {
|
|
215
|
+
parts.push(`${fieldSql} ${op} NULL`)
|
|
216
|
+
return increment
|
|
217
|
+
}
|
|
218
|
+
parts.push(`${fieldSql} ${op} $${increment}`)
|
|
219
|
+
params.push(value)
|
|
220
|
+
return increment + 1
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (op === 'IN' || op === 'NOT IN') {
|
|
224
|
+
const list = Array.isArray(value) ? value : parseListLiteral(value)
|
|
225
|
+
if (!list.length) {
|
|
226
|
+
parts.push(op === 'IN' ? 'FALSE' : 'TRUE')
|
|
227
|
+
return increment
|
|
228
|
+
}
|
|
229
|
+
const placeholders = list.map((_, i) => `$${increment + i}`).join(', ')
|
|
230
|
+
parts.push(`${fieldSql} ${op} (${placeholders})`)
|
|
231
|
+
params.push(...list)
|
|
232
|
+
return increment + list.length
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (op === 'BETWEEN' || op === 'NOT BETWEEN') {
|
|
236
|
+
const range = Array.isArray(value) ? value : parseRangeLiteral(value)
|
|
237
|
+
if (!range || range.length < 2) return increment
|
|
238
|
+
parts.push(`${fieldSql} ${op} $${increment} AND $${increment + 1}`)
|
|
239
|
+
params.push(range[0], range[1])
|
|
240
|
+
return increment + 2
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (op === '@>') {
|
|
244
|
+
parts.push(`${fieldSql}::jsonb @> $${increment}::jsonb`)
|
|
245
|
+
params.push(typeof value === 'string' ? value : JSON.stringify(value))
|
|
246
|
+
return increment + 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
parts.push(`${fieldSql} ${op} $${increment}`)
|
|
250
|
+
params.push(value)
|
|
251
|
+
return increment + 1
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const appendWhereEntry = (parts, params, increment, key, value) => {
|
|
255
|
+
let fieldName = key
|
|
256
|
+
let keyOp = ''
|
|
257
|
+
const keyMatch = KEY_OPERATOR_RE.exec(String(key).trim())
|
|
258
|
+
if (keyMatch) {
|
|
259
|
+
fieldName = keyMatch[1]
|
|
260
|
+
keyOp = normalizeOperator(keyMatch[2])
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const fieldSql = quoteIdent(fieldName)
|
|
264
|
+
if (!fieldSql) return increment
|
|
265
|
+
|
|
266
|
+
if (keyOp) {
|
|
267
|
+
return pushWhereCondition(parts, params, increment, fieldSql, keyOp, value)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (value === null || value === undefined) {
|
|
271
|
+
parts.push(`${fieldSql} IS NULL`)
|
|
272
|
+
return increment
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
if (value.length && typeof value[0] === 'string' && normalizeOperator(value[0])) {
|
|
277
|
+
const op = normalizeOperator(value[0])
|
|
278
|
+
const rest = value.slice(1)
|
|
279
|
+
if (op === 'IN' || op === 'NOT IN') {
|
|
280
|
+
const list = rest.length === 1 && Array.isArray(rest[0]) ? rest[0] : rest
|
|
281
|
+
return pushWhereCondition(parts, params, increment, fieldSql, op, list)
|
|
282
|
+
}
|
|
283
|
+
if (op === 'BETWEEN' || op === 'NOT BETWEEN') {
|
|
284
|
+
const range = rest.length === 1 && Array.isArray(rest[0]) ? rest[0] : rest
|
|
285
|
+
return pushWhereCondition(parts, params, increment, fieldSql, op, range)
|
|
286
|
+
}
|
|
287
|
+
return pushWhereCondition(parts, params, increment, fieldSql, op, rest[0])
|
|
288
|
+
}
|
|
289
|
+
return pushWhereCondition(parts, params, increment, fieldSql, 'IN', value)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (isOperatorMap(value)) {
|
|
293
|
+
const castRaw = value.cast || value.type
|
|
294
|
+
const cast = String(castRaw || '').toLowerCase() === 'date' ? 'date' : castRaw
|
|
295
|
+
const sqlField = fieldSqlWithCast(fieldSql, cast)
|
|
296
|
+
for (const [opKey, opVal] of Object.entries(value)) {
|
|
297
|
+
if (META_WHERE_KEYS.has(String(opKey).toLowerCase())) continue
|
|
298
|
+
increment = pushWhereCondition(parts, params, increment, sqlField, opKey, opVal)
|
|
299
|
+
}
|
|
300
|
+
return increment
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (typeof value === 'string') {
|
|
304
|
+
const matched = STRING_OPERATOR_RE.exec(value.trim())
|
|
305
|
+
if (matched) {
|
|
306
|
+
const op = normalizeOperator(matched[1])
|
|
307
|
+
let raw = matched[2]
|
|
308
|
+
if (op === 'IN' || op === 'NOT IN') raw = parseListLiteral(raw)
|
|
309
|
+
else if (op === 'BETWEEN' || op === 'NOT BETWEEN') raw = parseRangeLiteral(raw)
|
|
310
|
+
else if ((op === 'IS' || op === 'IS NOT') && /^null$/i.test(String(raw))) raw = null
|
|
311
|
+
return pushWhereCondition(parts, params, increment, fieldSql, op, raw)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return pushWhereCondition(parts, params, increment, fieldSql, '=', value)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const formatSelect = (select) => {
|
|
319
|
+
if (Array.isArray(select)) {
|
|
320
|
+
const cols = select.map((item) => {
|
|
321
|
+
if (typeof item !== 'string') return ''
|
|
322
|
+
const trimmed = item.trim()
|
|
323
|
+
if (!trimmed) return ''
|
|
324
|
+
return quoteIdent(trimmed) || trimmed
|
|
325
|
+
}).filter(Boolean)
|
|
326
|
+
return cols.length ? cols.join(', ') : '*'
|
|
327
|
+
}
|
|
328
|
+
if (typeof select === 'string' && select.trim()) return select
|
|
329
|
+
return '*'
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const formatJoins = (joins) => {
|
|
333
|
+
if (!Array.isArray(joins) || !joins.length) return ''
|
|
334
|
+
return joins.map((item) => {
|
|
335
|
+
if (typeof item === 'string') return item
|
|
336
|
+
if (!item || typeof item !== 'object') return ''
|
|
337
|
+
const type = String(item.type || 'LEFT').trim().toUpperCase()
|
|
338
|
+
const allowed = new Set(['LEFT', 'RIGHT', 'INNER', 'FULL', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'CROSS'])
|
|
339
|
+
if (!allowed.has(type)) return ''
|
|
340
|
+
const table = quoteTableName(item.table || '')
|
|
341
|
+
if (!table) return ''
|
|
342
|
+
const alias = item.as ? ` ${quoteIdent(item.as) || String(item.as).trim()}` : ''
|
|
343
|
+
if (type === 'CROSS') return `CROSS JOIN ${table}${alias}`
|
|
344
|
+
const on = String(item.on || '').trim()
|
|
345
|
+
if (!on) return ''
|
|
346
|
+
return `${type} JOIN ${table}${alias} ON ${on}`
|
|
347
|
+
}).filter(Boolean).join(' ')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const groupByFn = (obj) => {
|
|
351
|
+
const g = obj.groupBy || obj.group_by
|
|
352
|
+
if (!g) return ''
|
|
353
|
+
const list = Array.isArray(g) ? g : String(g).split(',')
|
|
354
|
+
const cols = list.map((item) => {
|
|
355
|
+
const trimmed = String(item || '').trim()
|
|
356
|
+
return quoteIdent(trimmed) || sanitizeOrderBySegment(trimmed)
|
|
357
|
+
}).filter(Boolean)
|
|
358
|
+
return cols.length ? ` GROUP BY ${cols.join(', ')} ` : ''
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const havingFn = (obj, increment, arr) => {
|
|
362
|
+
const having = obj.having || {}
|
|
363
|
+
if (!having || typeof having !== 'object' || Array.isArray(having)) {
|
|
364
|
+
return { sql: '', increment }
|
|
365
|
+
}
|
|
366
|
+
const parts = []
|
|
367
|
+
for (const key in having) {
|
|
368
|
+
increment = appendWhereEntry(parts, arr, increment, key, having[key])
|
|
369
|
+
}
|
|
370
|
+
return { sql: parts.length ? ` HAVING ${parts.join(' AND ')} ` : '', increment }
|
|
371
|
+
}
|
|
372
|
+
|
|
95
373
|
const connection = {}
|
|
96
374
|
|
|
97
375
|
connection.query = async (string, arr) => {
|
|
@@ -134,25 +412,51 @@ const orderByFn = (obj) => {
|
|
|
134
412
|
return orderBy
|
|
135
413
|
}
|
|
136
414
|
|
|
137
|
-
const whereFn = (obj) => {
|
|
415
|
+
const whereFn = (obj, startIncrement = 1) => {
|
|
138
416
|
const where = obj.where || {}
|
|
139
|
-
|
|
417
|
+
const orWhere = obj.orWhere || obj.or_where || {}
|
|
418
|
+
// [{field:"join_date",option:">=",value:"2025-02-12",operator:"AND",type:"text,json,date,inline"}]
|
|
419
|
+
// atau bentuk singkat: ["join_date", ">=", "2025-02-12"]
|
|
140
420
|
const whereArray = obj.whereArray || []
|
|
141
|
-
let increment =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
421
|
+
let increment = startIncrement
|
|
422
|
+
const arr = []
|
|
423
|
+
const parts = []
|
|
424
|
+
|
|
425
|
+
if (where && typeof where === 'object' && !Array.isArray(where)) {
|
|
426
|
+
for (const key in where) {
|
|
427
|
+
increment = appendWhereEntry(parts, arr, increment, key, where[key])
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const orParts = []
|
|
432
|
+
if (orWhere && typeof orWhere === 'object' && !Array.isArray(orWhere)) {
|
|
433
|
+
for (const key in orWhere) {
|
|
434
|
+
increment = appendWhereEntry(orParts, arr, increment, key, orWhere[key])
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (orParts.length) {
|
|
438
|
+
parts.push(`(${orParts.join(' OR ')})`)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let wherequery = parts.join(' AND ')
|
|
442
|
+
let hasWhere = parts.length > 0
|
|
443
|
+
|
|
152
444
|
if (whereArray.length) {
|
|
153
445
|
let andOr = wherequery ? ' AND ' : ''
|
|
154
|
-
whereArray.
|
|
155
|
-
let
|
|
446
|
+
whereArray.forEach((rawItem, index) => {
|
|
447
|
+
let item = rawItem
|
|
448
|
+
if (Array.isArray(item)) {
|
|
449
|
+
item = {
|
|
450
|
+
field: item[0],
|
|
451
|
+
option: item[1],
|
|
452
|
+
value: item[2],
|
|
453
|
+
operator: item[3] || 'AND',
|
|
454
|
+
type: item[4],
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!item || typeof item !== 'object') return
|
|
458
|
+
|
|
459
|
+
const type = !item.type ? 'text' : item.type
|
|
156
460
|
if (index > 0) {
|
|
157
461
|
andOr = ''
|
|
158
462
|
}
|
|
@@ -160,74 +464,100 @@ const whereFn = (obj) => {
|
|
|
160
464
|
if (index == whereArray.length - 1) {
|
|
161
465
|
operator = ''
|
|
162
466
|
}
|
|
163
|
-
|
|
467
|
+
|
|
468
|
+
const quotedField = item.field ? quoteIdent(item.field) : ''
|
|
469
|
+
let field = ''
|
|
164
470
|
if (type == 'date') {
|
|
165
|
-
field =
|
|
471
|
+
field = quotedField ? `${quotedField}::text ` : ''
|
|
166
472
|
} else {
|
|
167
|
-
field =
|
|
473
|
+
field = quotedField || ''
|
|
168
474
|
}
|
|
169
|
-
|
|
170
|
-
if (item.isJSON) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
475
|
+
|
|
476
|
+
if (item.isJSON || type == 'json' || type == 'jsonb') {
|
|
477
|
+
if (quotedField) {
|
|
478
|
+
wherequery += andOr + ` (${quotedField})::jsonb @> $${increment}::jsonb ${operator}`
|
|
479
|
+
arr.push(typeof item.value === 'string' ? item.value : JSON.stringify(item.value))
|
|
480
|
+
increment++
|
|
481
|
+
hasWhere = true
|
|
482
|
+
}
|
|
483
|
+
} else if (type == 'inline') {
|
|
484
|
+
if (field) {
|
|
485
|
+
wherequery += andOr + ` ${field} ${item.value} ${operator}`
|
|
486
|
+
} else {
|
|
487
|
+
wherequery += andOr + ` ${item.value} ${operator}`
|
|
488
|
+
}
|
|
174
489
|
hasWhere = true
|
|
175
490
|
} else {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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}`
|
|
179
495
|
increment++
|
|
496
|
+
arr.push(item.value)
|
|
180
497
|
hasWhere = true
|
|
181
|
-
} else if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
} else {
|
|
186
|
-
wherequery += andOr + ` ${item.value} ${operator}`
|
|
187
|
-
}
|
|
498
|
+
} else if (rawOption.includes('$')) {
|
|
499
|
+
wherequery += `${andOr} ${field} ${rawOption} ${operator}`
|
|
500
|
+
increment++
|
|
501
|
+
arr.push(item.value)
|
|
188
502
|
hasWhere = true
|
|
189
503
|
} else {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
531
|
} else {
|
|
198
|
-
wherequery += `${andOr} ${field} ${
|
|
532
|
+
wherequery += `${andOr} ${field} ${option} $${increment} ${operator}`
|
|
199
533
|
increment++
|
|
534
|
+
let itemValue = item.value
|
|
535
|
+
if (option === '=') {
|
|
536
|
+
itemValue = Util.replaceAll(itemValue + '', '%', '')
|
|
537
|
+
}
|
|
538
|
+
arr.push(itemValue)
|
|
539
|
+
hasWhere = true
|
|
200
540
|
}
|
|
201
|
-
let itemValue = item.value
|
|
202
|
-
if (item.option == '=') {
|
|
203
|
-
itemValue = Util.replaceAll(itemValue + '', '%', '')
|
|
204
|
-
}
|
|
205
|
-
arr.push(itemValue)
|
|
206
541
|
}
|
|
207
542
|
}
|
|
208
543
|
})
|
|
209
|
-
//console.log(arr)
|
|
210
544
|
}
|
|
545
|
+
|
|
211
546
|
if (arr.length > 0) {
|
|
212
547
|
hasWhere = true
|
|
213
548
|
}
|
|
214
|
-
let wheres = ''
|
|
215
|
-
if (hasWhere) {
|
|
216
|
-
wheres = `WHERE ${wherequery}`
|
|
217
|
-
}
|
|
218
549
|
|
|
219
|
-
|
|
220
|
-
where:
|
|
221
|
-
arr
|
|
222
|
-
increment
|
|
550
|
+
return {
|
|
551
|
+
where: hasWhere && wherequery ? `WHERE ${wherequery}` : '',
|
|
552
|
+
arr,
|
|
553
|
+
increment,
|
|
223
554
|
}
|
|
224
|
-
//console.log(obj)
|
|
225
|
-
return objAll
|
|
226
555
|
}
|
|
227
556
|
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const select = obj.select
|
|
557
|
+
const buildSelectQuery = (obj) => {
|
|
558
|
+
const tableSql = obj.table ? quoteTableName(obj.table) : '""'
|
|
559
|
+
const select = formatSelect(obj.select)
|
|
560
|
+
const distinct = obj.distinct ? 'DISTINCT ' : ''
|
|
231
561
|
const statement = obj.statement || ''
|
|
232
562
|
const limitVal = toSafeNonNegativeInt(obj.limit, null)
|
|
233
563
|
const offsetVal = obj.hasOwnProperty('offset') ? toSafeNonNegativeInt(obj.offset, 0) : (obj.limit ? 0 : null)
|
|
@@ -235,18 +565,18 @@ connection.results = async (obj) => {
|
|
|
235
565
|
const offset = offsetVal !== null ? ` OFFSET ${offsetVal} ` : ''
|
|
236
566
|
const orderBy = orderByFn(obj)
|
|
237
567
|
const values = obj.values || []
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
if (objJoin.length) {
|
|
241
|
-
join = objJoin.join(' ')
|
|
242
|
-
}
|
|
568
|
+
const join = formatJoins(obj.joins || obj.join || [])
|
|
569
|
+
const groupBy = groupByFn(obj)
|
|
243
570
|
const whereObj = whereFn(obj)
|
|
244
|
-
const
|
|
571
|
+
const havingObj = havingFn(obj, whereObj.increment, whereObj.arr)
|
|
245
572
|
const arr = whereObj.arr
|
|
573
|
+
const sql = `SELECT ${distinct}${select} FROM ${tableSql} ${join} ${whereObj.where} ${groupBy} ${havingObj.sql} ${statement} ${orderBy} ${limit} ${offset}`.replace(/\s+/g, ' ').trim()
|
|
574
|
+
return { sql, arr, values }
|
|
575
|
+
}
|
|
246
576
|
|
|
247
|
-
|
|
577
|
+
connection.results = async (obj) => {
|
|
578
|
+
const { sql, arr, values } = buildSelectQuery(obj)
|
|
248
579
|
try {
|
|
249
|
-
const start = Date.now()
|
|
250
580
|
const result = await pool.query(sql, arr.length ? arr : values.length ? values : null)
|
|
251
581
|
return !result.rows ? [] : result.rows
|
|
252
582
|
} catch (e) {
|
|
@@ -258,24 +588,8 @@ connection.results = async (obj) => {
|
|
|
258
588
|
}
|
|
259
589
|
|
|
260
590
|
connection.sql = async (obj) => {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
//[{field:"your_field",option:"=>",value:"12",operator:"AND",type:"text,json,date"}]
|
|
264
|
-
const statement = obj.statement || ''
|
|
265
|
-
const limit = obj.limit ? ` LIMIT ${obj.limit} ` : ''
|
|
266
|
-
const offset = obj.hasOwnProperty('offset') ? ` OFFSET ${obj.offset} ` : obj.limit ? 'OFFSET 0' : ''
|
|
267
|
-
const orderBy = orderByFn(obj)
|
|
268
|
-
const values = obj.values || []
|
|
269
|
-
const objJoin = obj.joins || []
|
|
270
|
-
let join = ''
|
|
271
|
-
if (objJoin.length) {
|
|
272
|
-
join = objJoin.join(' ')
|
|
273
|
-
}
|
|
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 }
|
|
591
|
+
const { sql, arr } = buildSelectQuery(obj)
|
|
592
|
+
return { sql, arr }
|
|
279
593
|
}
|
|
280
594
|
|
|
281
595
|
connection.result = async (obj) => {
|
|
@@ -288,8 +602,8 @@ connection.result = async (obj) => {
|
|
|
288
602
|
}
|
|
289
603
|
|
|
290
604
|
connection.insert = async (obj) => {
|
|
291
|
-
const
|
|
292
|
-
if (!
|
|
605
|
+
const tableSql = quoteTableName(obj.table || '')
|
|
606
|
+
if (!tableSql) throw new Error('Table name is required for insert')
|
|
293
607
|
const data = { ...obj.data }
|
|
294
608
|
const returning = sanitizeReturning(data.returning || 'RETURNING *')
|
|
295
609
|
delete data.returning
|
|
@@ -307,7 +621,7 @@ connection.insert = async (obj) => {
|
|
|
307
621
|
}
|
|
308
622
|
}
|
|
309
623
|
if (datas.length === 0) throw new Error('At least one valid column is required for insert')
|
|
310
|
-
const sql = `INSERT INTO
|
|
624
|
+
const sql = `INSERT INTO ${tableSql} ("${datas.join('","')}") VALUES (${values.join(',')}) ${returning}`
|
|
311
625
|
|
|
312
626
|
try {
|
|
313
627
|
const results = await pool.query(sql, arr)
|
|
@@ -321,17 +635,14 @@ connection.insert = async (obj) => {
|
|
|
321
635
|
}
|
|
322
636
|
|
|
323
637
|
connection.update = async (obj) => {
|
|
324
|
-
const
|
|
325
|
-
if (!
|
|
638
|
+
const tableSql = quoteTableName(obj.table || '')
|
|
639
|
+
if (!tableSql) throw new Error('Table name is required for update')
|
|
326
640
|
const data = { ...obj.data }
|
|
327
641
|
const returning = sanitizeReturning(data.returning || 'RETURNING *')
|
|
328
642
|
delete data.returning
|
|
329
643
|
|
|
330
|
-
const where = obj.where || {}
|
|
331
|
-
const whereArray = obj.whereArray || []
|
|
332
644
|
const arr = []
|
|
333
645
|
const dataArr = []
|
|
334
|
-
let wherequery = []
|
|
335
646
|
let increment = 1
|
|
336
647
|
|
|
337
648
|
for (const key in data) {
|
|
@@ -344,35 +655,10 @@ connection.update = async (obj) => {
|
|
|
344
655
|
}
|
|
345
656
|
if (dataArr.length === 0) throw new Error('At least one valid column is required for update')
|
|
346
657
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
arr.push(where[key])
|
|
352
|
-
increment++
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
wherequery = wherequery.length ? wherequery.join(' AND ') : ''
|
|
356
|
-
|
|
357
|
-
if (whereArray.length) {
|
|
358
|
-
let andOr = wherequery ? ' AND ' : ''
|
|
359
|
-
whereArray.forEach((item, index) => {
|
|
360
|
-
if (index > 0) andOr = ''
|
|
361
|
-
const field = sanitizeWhereField(item.field)
|
|
362
|
-
const option = sanitizeWhereOption(item.option)
|
|
363
|
-
const connector = sanitizeWhereConnector(item.operator)
|
|
364
|
-
if (field && option) {
|
|
365
|
-
wherequery += `${andOr} ${field} ${option} $${increment} ${connector}`
|
|
366
|
-
arr.push(item.value)
|
|
367
|
-
increment++
|
|
368
|
-
}
|
|
369
|
-
})
|
|
370
|
-
if (wherequery.endsWith(' AND ')) wherequery = wherequery.slice(0, -5)
|
|
371
|
-
else if (wherequery.endsWith(' OR ')) wherequery = wherequery.slice(0, -4)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const wheres = wherequery ? ' WHERE ' + wherequery : ''
|
|
375
|
-
const sql = `UPDATE "${table}" SET ${dataArr.join(', ')}${wheres} ${returning}`
|
|
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}`
|
|
376
662
|
|
|
377
663
|
try {
|
|
378
664
|
const result = await pool.query(sql, arr)
|
|
@@ -386,33 +672,38 @@ connection.update = async (obj) => {
|
|
|
386
672
|
}
|
|
387
673
|
|
|
388
674
|
connection.delete = async (obj) => {
|
|
389
|
-
const
|
|
390
|
-
if (!
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
let increment = 1
|
|
395
|
-
for (const key in where) {
|
|
396
|
-
const col = sanitizeColumnName(key)
|
|
397
|
-
if (col) {
|
|
398
|
-
wherequery.push(`"${col}" = $${increment}`)
|
|
399
|
-
arr.push(where[key])
|
|
400
|
-
increment++
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const whereClause = wherequery.length ? wherequery.join(' AND ') : ''
|
|
404
|
-
const wheres = whereClause ? ' WHERE ' + whereClause : ''
|
|
405
|
-
const sql = `DELETE FROM "${table}"${wheres} RETURNING *`
|
|
675
|
+
const tableSql = quoteTableName(obj.table || '')
|
|
676
|
+
if (!tableSql) throw new Error('Table name is required for delete')
|
|
677
|
+
const whereObj = whereFn(obj, 1)
|
|
678
|
+
const wheres = whereObj.where ? ` ${whereObj.where}` : ''
|
|
679
|
+
const sql = `DELETE FROM ${tableSql}${wheres} RETURNING *`
|
|
406
680
|
try {
|
|
407
|
-
return await pool.query(sql, arr)
|
|
681
|
+
return await pool.query(sql, whereObj.arr)
|
|
408
682
|
} catch (e) {
|
|
409
683
|
console.log(sql)
|
|
410
|
-
console.log(arr)
|
|
684
|
+
console.log(whereObj.arr)
|
|
411
685
|
console.log(e.toString())
|
|
412
686
|
throw e
|
|
413
687
|
}
|
|
414
688
|
}
|
|
415
689
|
|
|
690
|
+
connection.count = async (obj) => {
|
|
691
|
+
const countSelect = obj.select && /count\s*\(/i.test(String(obj.select))
|
|
692
|
+
? obj.select
|
|
693
|
+
: `COUNT(${obj.countField ? (quoteIdent(obj.countField) || '*') : '*'}) AS count`
|
|
694
|
+
const row = await connection.result({
|
|
695
|
+
...obj,
|
|
696
|
+
select: countSelect,
|
|
697
|
+
limit: undefined,
|
|
698
|
+
offset: undefined,
|
|
699
|
+
orderBy: undefined,
|
|
700
|
+
order_by: undefined,
|
|
701
|
+
distinct: undefined,
|
|
702
|
+
})
|
|
703
|
+
if (!row || Array.isArray(row)) return 0
|
|
704
|
+
return Number(row.count || 0)
|
|
705
|
+
}
|
|
706
|
+
|
|
416
707
|
connection.insertData = async(tableName, data) => {
|
|
417
708
|
try {
|
|
418
709
|
// Validasi input
|