@cap-js/postgres 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 1.2.1 - 2023-09-08
8
+
9
+ ### Changed
10
+
11
+ - Bump minimum required version of `@cap-js/db-service`
12
+
13
+ ## Version 1.2.0 - 2023-09-06
14
+
15
+ ### Added
16
+
17
+ - Reduced the usage of `is not distinct [not] from`. #157
18
+
19
+ ### Fixed
20
+
21
+ - [Reserved words](https://www.postgresql.org/docs/current/sql-keywords-appendix.html) are now used to automatically escape reserved words which are used as identifier. #178
22
+ - Remove column count limitation. #150
23
+
7
24
  ## Version 1.1.0 - 2023-08-01
8
25
 
9
26
  ### Added
@@ -1,7 +1,9 @@
1
1
  const { SQLService } = require('@cap-js/db-service')
2
- const { Client } = require('pg')
2
+ const { Client, Query } = require('pg')
3
3
  const cds = require('@sap/cds/lib')
4
4
  const crypto = require('crypto')
5
+ const { Writable, Readable } = require('stream')
6
+ const sessionVariableMap = require('./session.json')
5
7
 
6
8
  class PostgresService extends SQLService {
7
9
  init() {
@@ -61,18 +63,18 @@ class PostgresService extends SQLService {
61
63
  }
62
64
 
63
65
  async set(variables) {
64
- // REVISIT: remove when all environment variables are aligned
65
66
  // RESTRICTIONS: 'Custom parameter names must be two or more simple identifiers separated by dots.'
66
- const nameMap = {
67
- '$user.id': 'cap.applicationuser',
68
- '$user.locale': 'cap.locale',
69
- '$valid.from': 'cap.valid_from',
70
- '$valid.to': 'cap.valid_to',
71
- }
72
-
73
67
  const env = {}
68
+
69
+ // Check all properties on the variables object
74
70
  for (let name in variables) {
75
- env[nameMap[name]] = variables[name]
71
+ env[sessionVariableMap[name] || name] = variables[name]
72
+ }
73
+
74
+ // Explicitly check for the default session variable properties
75
+ // As they are getters and not own properties of the object
76
+ for (let name in sessionVariableMap) {
77
+ if (variables[name]) env[sessionVariableMap[name]] = variables[name]
76
78
  }
77
79
 
78
80
  return Promise.all([
@@ -106,14 +108,14 @@ class PostgresService extends SQLService {
106
108
  * The group by is done by the key column to make sure that only one collation per key is returned
107
109
  */
108
110
  const cSQL = `
109
- SELECT
111
+ SELECT
110
112
  SUBSTRING(collname, 1, 2) AS K,
111
- MIN(collname) AS V
112
- FROM
113
- pg_collation
114
- WHERE
115
- collprovider = 'c' AND
116
- collname LIKE '__>___' ESCAPE '>'
113
+ MIN(collname) AS V
114
+ FROM
115
+ pg_collation
116
+ WHERE
117
+ collprovider = 'c' AND
118
+ collname LIKE '__>___' ESCAPE '>'
117
119
  GROUP BY k
118
120
  `
119
121
 
@@ -134,6 +136,7 @@ GROUP BY k
134
136
 
135
137
  prepare(sql) {
136
138
  const query = {
139
+ _streams: 0,
137
140
  text: sql,
138
141
  // Track queries name for postgres referencing prepare statements
139
142
  // sha1 as it needs to be less then 63 characters
@@ -142,7 +145,9 @@ GROUP BY k
142
145
  return {
143
146
  run: async values => {
144
147
  // REVISIT: SQLService provides empty values as {} for plain SQL statements - PostgreSQL driver expects array or nothing - see issue #78
145
- const result = await this.dbc.query({ ...query, values: this._getValues(values) })
148
+ let newQuery = this._prepareStreams(query, values)
149
+ if (typeof newQuery.then === 'function') newQuery = await newQuery
150
+ const result = await this.dbc.query(newQuery)
146
151
  return { changes: result.rowCount }
147
152
  },
148
153
  get: async values => {
@@ -159,6 +164,14 @@ GROUP BY k
159
164
  throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
160
165
  }
161
166
  },
167
+ stream: async (values, one) => {
168
+ try {
169
+ const streamQuery = new QueryStream({ ...query, values: this._getValues(values) }, one)
170
+ return await this.dbc.query(streamQuery)
171
+ } catch (e) {
172
+ throw Object.assign(e, { sql: sql + '\n' + new Array(e.position).fill(' ').join('') + '^' })
173
+ }
174
+ },
162
175
  }
163
176
  }
164
177
 
@@ -170,6 +183,60 @@ GROUP BY k
170
183
  return values
171
184
  }
172
185
 
186
+ _prepareStreams(query, values) {
187
+ values = this._getValues(values)
188
+ if (!values) return query
189
+
190
+ const streams = []
191
+ const newValues = []
192
+ let sql = query.text
193
+ if (Array.isArray(values)) {
194
+ values.forEach((value, i) => {
195
+ if (value instanceof Readable) {
196
+ const streamID = query._streams++
197
+ const isBinary = value.type === 'binary'
198
+ const paramStream = new ParameterStream(query.name, streamID)
199
+ if (isBinary) value.setEncoding('base64')
200
+ value.pipe(paramStream)
201
+ value.on('error', err => paramStream.emit('error', err))
202
+ streams[i] = paramStream
203
+ newValues[i] = streamID
204
+ sql = sql.replace(
205
+ new RegExp(`\\$${i + 1}`, 'g'),
206
+ // Don't ask about the dollar signs
207
+ `(SELECT ${isBinary ? `DECODE(PARAM,'base64')` : 'PARAM'} FROM "$$$$PARAMETER_BUFFER$$$$" WHERE NAME='${
208
+ query.name
209
+ }' AND ID=$${i + 1})`,
210
+ )
211
+ return
212
+ }
213
+ newValues[i] = value
214
+ })
215
+ }
216
+
217
+ if (streams.length > 0) {
218
+ return (async () => {
219
+ const newQuery = {
220
+ text: sql,
221
+ // Even with the changed SQL it might be common to call this statement with the same parameters as streams
222
+ // As the streams are selected with their ID as prepared statement parameter, the sql is the same
223
+ name: crypto.createHash('sha1').update(sql).digest('hex'),
224
+ values: newValues,
225
+ }
226
+ await this.dbc.query({
227
+ text: 'CREATE TEMP TABLE IF NOT EXISTS "$$PARAMETER_BUFFER$$" (PARAM TEXT, NAME TEXT, ID INT) ON COMMIT DROP',
228
+ })
229
+ const proms = []
230
+ for (const stream of streams) {
231
+ proms.push(this.dbc.query(stream))
232
+ }
233
+ await Promise.all(proms)
234
+ return newQuery
235
+ })()
236
+ }
237
+ return { ...query, values }
238
+ }
239
+
173
240
  async exec(sql) {
174
241
  return this.dbc.query(sql)
175
242
  }
@@ -249,18 +316,9 @@ GROUP BY k
249
316
  return super.from(from)
250
317
  }
251
318
 
252
- // REVISIT: pg requires alias for {val}
253
- SELECT_columns({ SELECT }) {
254
- // REVISIT: Genres cqn has duplicate ID column
255
- if (!SELECT.columns) return '*'
256
- const unique = {}
257
- return SELECT.columns
258
- .map(x => `${this.column_expr(x)} as ${this.quote(this.column_name(x))}`)
259
- .filter(x => {
260
- if (unique[x]) return false
261
- unique[x] = true
262
- return true
263
- })
319
+ column_alias4(x, q) {
320
+ if (!x.as && 'val' in x) return String(x.val)
321
+ return super.column_alias4(x, q)
264
322
  }
265
323
 
266
324
  SELECT_expand({ SELECT }, sql) {
@@ -268,19 +326,25 @@ GROUP BY k
268
326
  const queryAlias = this.quote(SELECT.from?.as || (SELECT.expand === 'root' && 'root'))
269
327
  const cols = SELECT.columns.map(x => {
270
328
  const name = this.column_name(x)
271
- let col = `${this.string(name)},${this.output_converter4(x.element, queryAlias + '.' + this.quote(name))}`
329
+ const outputConverter = this.output_converter4(x.element, `${queryAlias}.${this.quote(name)}`)
330
+ let col = `${outputConverter} as ${this.doubleQuote(name)}`
272
331
 
273
332
  if (x.SELECT?.count) {
274
333
  // Return both the sub select and the count for @odata.count
275
334
  const qc = cds.ql.clone(x, { columns: [{ func: 'count' }], one: 1, limit: 0, orderBy: 0 })
276
- col += `, '${name}@odata.count',${this.expr(qc)}`
335
+ col += `,${this.expr(qc)} as ${this.doubleQuote(`${name}@odata.count`)}`
277
336
  }
278
337
  return col
279
338
  })
280
- let obj = `json_build_object(${cols})`
339
+ // REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
340
+ let obj = `row_to_json(${queryAlias}.*)`
281
341
  return `SELECT ${
282
342
  SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(json_agg(${obj}),'[]'::json)`
283
- } as _json_ FROM (${sql}) as ${queryAlias}`
343
+ } as _json_ FROM (SELECT ${cols} FROM (${sql}) as ${queryAlias}) as ${queryAlias}`
344
+ }
345
+
346
+ doubleQuote(name) {
347
+ return `"${name.replace(/"/g, '""')}"`
284
348
  }
285
349
 
286
350
  INSERT(q, isUpsert = false) {
@@ -291,20 +355,24 @@ GROUP BY k
291
355
  // Adjusts json path expressions to be postgres specific
292
356
  .replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
293
357
  // Adjusts json function to be postgres specific
294
- .replace('json_each(?)', 'json_array_elements($1)')
358
+ .replace('json_each(?)', 'json_array_elements($1::JSON)')
295
359
  .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
296
360
  }
297
361
 
362
+ param({ ref }) {
363
+ this._paramCount = this._paramCount || 1
364
+ if (ref.length > 1) throw cds.error`Unsupported nested ref parameter: ${ref}`
365
+ return ref[0] === '?' ? `$${this._paramCount++}` : `:${ref}`
366
+ }
367
+
298
368
  val(val) {
299
369
  const ret = super.val(val)
300
370
  return ret === '?' ? `$${this.values.length}` : ret
301
371
  }
302
372
 
303
- operator(x) {
373
+ operator(x, i, xpr) {
304
374
  if (x === 'regexp') return '~'
305
- if (x === '=') return 'is not distinct from'
306
- if (x === '!=') return 'is distinct from'
307
- else return x
375
+ else return super.operator(x, i, xpr)
308
376
  }
309
377
 
310
378
  defaultValue(defaultValue = this.context.timestamp.toISOString()) {
@@ -346,14 +414,14 @@ GROUP BY k
346
414
  // REVISIT: Remove that with upcomming fixes in cds.linked
347
415
  Double: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
348
416
  DecimalFloat: (e, t) => `CAST(${e} as decimal${t.precision && t.scale ? `(${t.precision},${t.scale})` : ''})`,
349
- Binary: e => `CAST(${e} as bytea)`,
350
- LargeBinary: e => `CAST(${e} as bytea)`,
417
+ Binary: e => `DECODE(${e},'base64')`,
418
+ LargeBinary: e => `DECODE(${e},'base64')`,
351
419
  }
352
420
 
353
421
  static OutputConverters = {
354
422
  ...super.OutputConverters,
355
- Binary: e => e,
356
- LargeBinary: e => e,
423
+ Binary: e => `ENCODE(${e},'base64')`,
424
+ LargeBinary: e => `ENCODE(${e},'base64')`,
357
425
  Date: e => `to_char(${e}, 'YYYY-MM-DD')`,
358
426
  Time: e => `to_char(${e}, 'HH24:MI:SS')`,
359
427
  DateTime: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
@@ -403,7 +471,7 @@ GROUP BY k
403
471
  }
404
472
  }
405
473
 
406
- async tenant({ database, tenant }) {
474
+ async tenant({ database, tenant }, clean = false) {
407
475
  const creds = {
408
476
  database: database,
409
477
  usergroup: `${database}_USERS`,
@@ -413,18 +481,20 @@ GROUP BY k
413
481
  creds.password = creds.user
414
482
 
415
483
  try {
416
- await this.tx(async tx => {
417
- // await tx.run(`DROP USER IF EXISTS "${creds.user}"`)
418
- await tx
419
- .run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
420
- .catch(e => {
421
- if (e.code === '42710') return
422
- throw e
423
- })
424
- })
425
- await this.tx(async tx => {
426
- await tx.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
427
- })
484
+ if (!clean) {
485
+ await this.tx(async tx => {
486
+ // await tx.run(`DROP USER IF EXISTS "${creds.user}"`)
487
+ await tx
488
+ .run(`CREATE USER "${creds.user}" IN GROUP "${creds.usergroup}" PASSWORD '${creds.password}'`)
489
+ .catch(e => {
490
+ if (e.code === '42710') return
491
+ throw e
492
+ })
493
+ })
494
+ await this.tx(async tx => {
495
+ await tx.run(`GRANT CREATE, CONNECT ON DATABASE "${creds.database}" TO "${creds.user}";`)
496
+ })
497
+ }
428
498
 
429
499
  // Update credentials to new Schema owner
430
500
  await this.disconnect()
@@ -433,7 +503,7 @@ GROUP BY k
433
503
  // Create new schema using schema owner
434
504
  await this.tx(async tx => {
435
505
  await tx.run(`DROP SCHEMA IF EXISTS "${creds.schema}" CASCADE`)
436
- await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
506
+ if (!clean) await tx.run(`CREATE SCHEMA "${creds.schema}" AUTHORIZATION "${creds.user}"`).catch(() => {})
437
507
  })
438
508
  } finally {
439
509
  await this.disconnect()
@@ -441,4 +511,204 @@ GROUP BY k
441
511
  }
442
512
  }
443
513
 
514
+ class QueryStream extends Query {
515
+ constructor(config, one) {
516
+ // REVISIT: currently when setting the row chunk size
517
+ // it results in an inconsistent connection state
518
+ // if (!one) config.rows = 1000
519
+ super(config)
520
+
521
+ this._one = one || config.one
522
+
523
+ this.stream = new Readable({
524
+ read: this.rows
525
+ ? () => {
526
+ this.stream.pause()
527
+ // Request more rows
528
+ this.connection.execute({
529
+ portal: this.portal,
530
+ rows: this.rows,
531
+ })
532
+ this.connection.flush()
533
+ }
534
+ : () => {},
535
+ })
536
+ this.push = this.stream.push.bind(this.stream)
537
+
538
+ this._prom = new Promise((resolve, reject) => {
539
+ this.once('error', reject)
540
+ this.once('end', () => {
541
+ if (!this._one) this.push(this.constructor.close)
542
+ this.push(null)
543
+ if (this.stream.isPaused()) this.stream.resume()
544
+ resolve(null)
545
+ })
546
+ this.once('row', row => {
547
+ if (row == null) return resolve(null)
548
+ resolve(this.stream)
549
+ })
550
+ })
551
+ }
552
+
553
+ static sep = Buffer.from(',')
554
+ static open = Buffer.from('[')
555
+ static close = Buffer.from(']')
556
+
557
+ // Trigger query initialization
558
+ _getRows(connection) {
559
+ this.connection = connection
560
+ connection.execute({
561
+ portal: this.portal,
562
+ rows: this.rows ? 1 : undefined,
563
+ })
564
+ if (this.rows) {
565
+ connection.flush()
566
+ } else {
567
+ connection.sync()
568
+ }
569
+ }
570
+
571
+ // Delay requesting more rows until next is called
572
+ handlePortalSuspended() {
573
+ this.stream.resume()
574
+ }
575
+
576
+ // Provides metadata information from the database
577
+ handleRowDescription(msg) {
578
+ // Use default parser for binary results
579
+ if (msg.fields.length === 1 && msg.fields[0].dataTypeID === 17) {
580
+ this.handleDataRow = this.handleBinaryRow
581
+ } else {
582
+ this.handleDataRow = msg => {
583
+ const val = msg.fields[0]
584
+ if (!this._one && val !== null) this.push(this.constructor.open)
585
+ this.emit('row', val)
586
+ this.push(val)
587
+ delete this.handleDataRow
588
+ }
589
+ }
590
+ return super.handleRowDescription(msg)
591
+ }
592
+
593
+ // Called when a new row is received
594
+ handleDataRow(msg) {
595
+ this.push(this.constructor.sep)
596
+ this.push(msg.fields[0])
597
+ }
598
+
599
+ // Called when a new binary row is received
600
+ handleBinaryRow(msg) {
601
+ const val = msg.fields[0] === null ? null : this._result._parsers[0](msg.fields[0])
602
+ this.push(val)
603
+ this.emit('row', val)
604
+ }
605
+
606
+ then(resolve, reject) {
607
+ return this._prom.then(resolve, reject)
608
+ }
609
+ }
610
+
611
+ class ParameterStream extends Writable {
612
+ constructor(queryName, id) {
613
+ super({})
614
+ this.queryName = queryName
615
+ this.id = id
616
+ this.text = `COPY "$$PARAMETER_BUFFER$$"(param,name,id) FROM STDIN DELIMITER ',' QUOTE '${this.constructor.sep}' CSV`
617
+ this.lengthBuffer = Buffer.from([0x64, 0, 0, 0, 0])
618
+
619
+ // Flush quote character before input stream
620
+ this.flushChunk = chunk => {
621
+ delete this.flushChunk
622
+
623
+ this.lengthBuffer.writeUInt32BE(chunk.length + 5, 1)
624
+ this.connection.stream.write(this.lengthBuffer)
625
+ this.connection.stream.write(Buffer.from(this.constructor.sep))
626
+ return this.connection.stream.write(chunk)
627
+ }
628
+ }
629
+
630
+ static sep = String.fromCharCode(31) // Separator One
631
+ static done = Buffer.from([0x63, 0, 0, 0, 4])
632
+
633
+ then(resolve, reject) {
634
+ this.on('error', reject)
635
+ this.on('finish', resolve)
636
+ }
637
+
638
+ /**
639
+ * Indicates that the query was started by the connection
640
+ * @param {Object} connection
641
+ */
642
+ submit(connection) {
643
+ this.connection = connection
644
+ // Initialize query to be executed
645
+ connection.query(this.text)
646
+ }
647
+
648
+ // Used by the client to handle timeouts
649
+ callback() {}
650
+
651
+ _write(chunk, enc, cb) {
652
+ return this.flush(chunk, cb)
653
+ }
654
+
655
+ _construct(cb) {
656
+ this.handleCopyInResponse = () => cb()
657
+ }
658
+
659
+ _destroy(err, cb) {
660
+ this.handleError = () => {
661
+ this.callback()
662
+ this.connection = null
663
+ cb(err)
664
+ }
665
+ this.connection.sendCopyFail(err ? err.message : 'ParameterStream early destroy')
666
+ }
667
+
668
+ _final(cb) {
669
+ const sep = this.constructor.sep
670
+ this.flush(Buffer.from(`${sep},${this.queryName},${this.id}`), err => {
671
+ if (err) return cb(err)
672
+ this._finish = () => {
673
+ this.emit('finish')
674
+ cb()
675
+ }
676
+ this._destroy = (err, cb) => cb(err)
677
+ this.connection.stream.write(this.constructor.done)
678
+ })
679
+ }
680
+
681
+ flush(chunk, callback) {
682
+ if (this.flushChunk(chunk)) {
683
+ return callback()
684
+ }
685
+ this.connection.stream.once('drain', callback)
686
+ }
687
+
688
+ flushChunk(chunk) {
689
+ this.lengthBuffer.writeUInt32BE(chunk.length + 4, 1)
690
+ this.connection.stream.write(this.lengthBuffer)
691
+ return this.connection.stream.write(chunk)
692
+ }
693
+
694
+ handleError(e) {
695
+ this.callback()
696
+ this.emit('error', e)
697
+ this.connection = null
698
+ }
699
+
700
+ handleCommandComplete(msg) {
701
+ const match = /COPY (\d+)/.exec((msg || {}).text)
702
+ if (match) {
703
+ this.rowCount = parseInt(match[1], 10)
704
+ }
705
+ }
706
+
707
+ handleReadyForQuery() {
708
+ this.callback()
709
+ this._finish()
710
+ this.connection = null
711
+ }
712
+ }
713
+
444
714
  module.exports = PostgresService
@@ -1,4 +1,102 @@
1
1
  {
2
+ "ALL": 1,
3
+ "ANALYSE": 1,
4
+ "ANALYZE": 1,
5
+ "AND": 1,
6
+ "ANY": 1,
7
+ "ARRAY": 1,
8
+ "AS": 1,
9
+ "ASC": 1,
10
+ "ASYMMETRIC": 1,
11
+ "AUTHORIZATION": 1,
2
12
  "BINARY": 1,
3
- "ARRAY": 1
13
+ "BOTH": 1,
14
+ "CASE": 1,
15
+ "CAST": 1,
16
+ "CHECK": 1,
17
+ "COLLATE": 1,
18
+ "COLLATION": 1,
19
+ "COLUMN": 1,
20
+ "CONCURRENTLY": 1,
21
+ "CONSTRAINT": 1,
22
+ "CREATE": 1,
23
+ "CROSS": 1,
24
+ "CURRENT_CATALOG": 1,
25
+ "CURRENT_DATE": 1,
26
+ "CURRENT_ROLE": 1,
27
+ "CURRENT_SCHEMA": 1,
28
+ "CURRENT_TIME": 1,
29
+ "CURRENT_TIMESTAMP": 1,
30
+ "CURRENT_USER": 1,
31
+ "DEFAULT": 1,
32
+ "DEFERRABLE": 1,
33
+ "DESC": 1,
34
+ "DISTINCT": 1,
35
+ "DO": 1,
36
+ "ELSE": 1,
37
+ "END": 1,
38
+ "EXCEPT": 1,
39
+ "FALSE": 1,
40
+ "FETCH": 1,
41
+ "FOR": 1,
42
+ "FOREIGN": 1,
43
+ "FREEZE": 1,
44
+ "FROM": 1,
45
+ "FULL": 1,
46
+ "GRANT": 1,
47
+ "GROUP": 1,
48
+ "HAVING": 1,
49
+ "ILIKE": 1,
50
+ "IN": 1,
51
+ "INITIALLY": 1,
52
+ "INNER": 1,
53
+ "INTERSECT": 1,
54
+ "INTO": 1,
55
+ "IS": 1,
56
+ "ISNULL": 1,
57
+ "JOIN": 1,
58
+ "LATERAL": 1,
59
+ "LEADING": 1,
60
+ "LEFT": 1,
61
+ "LIKE": 1,
62
+ "LIMIT": 1,
63
+ "LOCALTIME": 1,
64
+ "LOCALTIMESTAMP": 1,
65
+ "NATURAL": 1,
66
+ "NOT": 1,
67
+ "NOTNULL": 1,
68
+ "NULL": 1,
69
+ "OFFSET": 1,
70
+ "ON": 1,
71
+ "ONLY": 1,
72
+ "OR": 1,
73
+ "ORDER": 1,
74
+ "OUTER": 1,
75
+ "OVERLAPS": 1,
76
+ "PLACING": 1,
77
+ "PRIMARY": 1,
78
+ "REFERENCES": 1,
79
+ "RETURNING": 1,
80
+ "RIGHT": 1,
81
+ "SELECT": 1,
82
+ "SESSION_USER": 1,
83
+ "SIMILAR": 1,
84
+ "SOME": 1,
85
+ "SYMMETRIC": 1,
86
+ "TABLE": 1,
87
+ "TABLESAMPLE": 1,
88
+ "THEN": 1,
89
+ "TO": 1,
90
+ "TRAILING": 1,
91
+ "TRUE": 1,
92
+ "UNION": 1,
93
+ "UNIQUE": 1,
94
+ "USER": 1,
95
+ "USING": 1,
96
+ "VARIADIC": 1,
97
+ "VERBOSE": 1,
98
+ "WHEN": 1,
99
+ "WHERE": 1,
100
+ "WINDOW": 1,
101
+ "WITH": 1
4
102
  }
package/lib/func.js CHANGED
@@ -1,4 +1,11 @@
1
+ const session = require('./session.json')
2
+
1
3
  const StandardFunctions = {
4
+ session_context: x => {
5
+ let sql = `current_setting('${ session[x.val] || x.val }')`
6
+ if (x.val === '$now') sql += '::timestamp'
7
+ return sql
8
+ },
2
9
  countdistinct: x => `count(distinct ${x || '*'})`,
3
10
  contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`,
4
11
  indexof: (x, y) => `strpos(${x},${y}) - 1`, // sqlite instr is 1 indexed
@@ -0,0 +1,7 @@
1
+ {
2
+ "$user.id": "cap.applicationuser",
3
+ "$user.locale": "cap.locale",
4
+ "$now": "cap.now",
5
+ "$valid.from": "cap.valid_from",
6
+ "$valid.to": "cap.valid_to"
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/postgres",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "CDS database service for Postgres",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/postgres#cds-database-service-for-postgres",
6
6
  "repository": {
@@ -27,11 +27,11 @@
27
27
  "npm": ">=8"
28
28
  },
29
29
  "scripts": {
30
- "setup": "docker-compose -f pg-stack.yml up -d",
31
- "test": "npm run setup && jest --silent"
30
+ "test": "npm start && jest --silent",
31
+ "start": "docker-compose -f pg-stack.yml up -d"
32
32
  },
33
33
  "dependencies": {
34
- "@cap-js/db-service": "^1.1.0",
34
+ "@cap-js/db-service": "^1.2.1",
35
35
  "pg": "^8"
36
36
  },
37
37
  "peerDependencies": {