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.
Files changed (2) hide show
  1. package/lib/connection.js +436 -145
  2. 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(['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'ILIKE', 'IS NOT', 'IS'])
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
- //[{field:"your_field",option:"=>",value:"12",operator:"AND",type:"text,json,date, inline"}]
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 = 1
142
- let arr = [],
143
- wherequery = []
144
- for (const key in where) {
145
- wherequery.push(key.indexOf('.') > -1 ? ` ${key} = $${increment} ` : ` "${key}" = $${increment}`)
146
- arr.push(where[key])
147
- increment++
148
- }
149
- //console.log(whereArray);
150
- let hasWhere = false
151
- wherequery = arr.length ? wherequery.join(' AND ') : ''
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.map((item, index) => {
155
- let type = !item.type ? 'text' : item.type
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
- let fields = ''
467
+
468
+ const quotedField = item.field ? quoteIdent(item.field) : ''
469
+ let field = ''
164
470
  if (type == 'date') {
165
- field = item.field.indexOf('.') > -1 ? item.field + '::text ' : ` ${item.field}::text `
471
+ field = quotedField ? `${quotedField}::text ` : ''
166
472
  } else {
167
- field = item.field.indexOf('.') > -1 ? item.field : item.field ? ` "${item.field}" ` : ''
473
+ field = quotedField || ''
168
474
  }
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++
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
- if (type == 'json') {
177
- wherequery += andOr + ` (${String(field).trim()})::jsonb @> $${increment}::jsonb ${operator}`
178
- arr.push(JSON.stringify(item.value))
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 (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
- }
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
- 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++
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} ${item.option} $${increment} ${operator}`
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
- let objAll = {
220
- where: wheres,
221
- arr: arr,
222
- increment: 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
- connection.results = async (obj) => {
229
- const table = sanitizeTableName(obj.table || '')
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 objJoin = obj.joins || []
239
- let join = ''
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 wheres = whereObj.where
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
- const sql = `SELECT ${select} FROM "${table}" ${join} ${wheres} ${statement} ${orderBy} ${limit} ${offset}`.replace(/\s+/g, ' ').trim()
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 select = obj.select || '*'
262
- const table = obj.table || ''
263
- //[{field:"your_field",option:"=>",value:"12",operator:"AND",type:"text,json,date"}]
264
- const statement = obj.statement || ''
265
- const limit = obj.limit ? ` LIMIT ${obj.limit} ` : ''
266
- const offset = obj.hasOwnProperty('offset') ? ` OFFSET ${obj.offset} ` : obj.limit ? 'OFFSET 0' : ''
267
- const orderBy = orderByFn(obj)
268
- const values = obj.values || []
269
- const objJoin = obj.joins || []
270
- let join = ''
271
- if (objJoin.length) {
272
- join = objJoin.join(' ')
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 table = sanitizeTableName(obj.table || '')
292
- if (!table) throw new Error('Table name is required for insert')
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 "${table}" ("${datas.join('","')}") VALUES (${values.join(',')}) ${returning}`
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 table = sanitizeTableName(obj.table || '')
325
- if (!table) throw new Error('Table name is required for update')
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
- for (const key in where) {
348
- const col = sanitizeColumnName(key)
349
- if (col) {
350
- wherequery.push(`"${col}" = $${increment}`)
351
- arr.push(where[key])
352
- increment++
353
- }
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 table = sanitizeTableName(obj.table || '')
390
- if (!table) throw new Error('Table name is required for delete')
391
- const where = obj.where || {}
392
- const arr = []
393
- const wherequery = []
394
- let increment = 1
395
- for (const key in where) {
396
- const col = sanitizeColumnName(key)
397
- if (col) {
398
- wherequery.push(`"${col}" = $${increment}`)
399
- arr.push(where[key])
400
- increment++
401
- }
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zet-lib",
3
- "version": "6.0.2",
3
+ "version": "6.1.0",
4
4
  "description": "zet is a library that part of zet generator.",
5
5
  "engines": {
6
6
  "node": ">=18"