@cap-js/db-service 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,30 @@
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
+ ### Fixed
10
+
11
+ - Association expansion in infix filters. #213
12
+
13
+ ## Version 1.2.0 - 2023-09-06
14
+
15
+ ### Added
16
+
17
+ - support for calculated elements on read. #113 #123
18
+ - support for managed associations with default values. #193
19
+ - introduced new operator `==` which translates to `IS NOT DISTINCT FROM`. #164
20
+
21
+ ### Fixed
22
+
23
+ - resolved a type error which occured in some cases for deeply nested `expand`s. #173
24
+ - path expression traversing non-foreign-key fields within infix filters are now properly rejected for `exists` predicates. #181
25
+ - CQL functions: In the `args` of the `concat` function an `xpr` is now wrapped in parentheses. #196
26
+ - Make `UPDATE` and `ofarray` typed column compatible. #184
27
+ - Ensure that `INSERT` with `rows` always inserts into the correct column. #193
28
+ - Allow `DateTime` columns to compare against their returned value. #206
29
+ - Deep Insert using backlink associations as key #199.
30
+
7
31
  ## Version 1.1.0 - 2023-08-01
8
32
 
9
33
  ### Fixed
@@ -17,7 +17,8 @@ module.exports = class InsertResult {
17
17
  * @param {unknown[]} results
18
18
  */
19
19
  constructor(query, results) {
20
- this.query = query
20
+ // Storing query as non-enumerable property to avoid polluting trace output
21
+ Object.defineProperty(this, 'query', { value: query })
21
22
  this.results = results
22
23
  }
23
24
 
package/lib/SQLService.js CHANGED
@@ -18,6 +18,7 @@ const cqn4sql = require('./cqn4sql')
18
18
  */
19
19
 
20
20
  class SQLService extends DatabaseService {
21
+
21
22
  init() {
22
23
  this.on(['SELECT'], this.transformStreamFromCQN)
23
24
  this.on(['UPDATE'], this.transformStreamIntoCQN)
@@ -82,7 +83,7 @@ class SQLService extends DatabaseService {
82
83
  if (rows.length)
83
84
  if (cqn.SELECT.expand) rows = rows.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))
84
85
  if (cqn.SELECT.count) rows.$count = await this.count(query, rows)
85
- return cqn.SELECT.one || query.SELECT.from.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
86
+ return cqn.SELECT.one || query.SELECT.from?.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
86
87
  }
87
88
 
88
89
  /**
@@ -91,7 +92,7 @@ class SQLService extends DatabaseService {
91
92
  */
92
93
  async onINSERT({ query, data }) {
93
94
  const { sql, entries, cqn } = this.cqn2sql(query, data)
94
- if (!sql) return // Do nothing when there is nothing to be done
95
+ if (!sql) return // Do nothing when there is nothing to be done // REVISIT: fix within mtxs
95
96
  const ps = await this.prepare(sql)
96
97
  const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
97
98
  return new this.class.InsertResults(cqn, results)
@@ -103,10 +104,11 @@ class SQLService extends DatabaseService {
103
104
  */
104
105
  async onUPSERT({ query, data }) {
105
106
  const { sql, entries } = this.cqn2sql(query, data)
106
- if (!sql) return // Do nothing when there is nothing to be done
107
+ if (!sql) return // Do nothing when there is nothing to be done // REVISIT: When does this happen?
107
108
  const ps = await this.prepare(sql)
108
109
  const results = entries ? await Promise.all(entries.map(e => ps.run(e))) : await ps.run()
109
- return results.reduce((lastValue, currentValue) => (lastValue += currentValue.changes), 0)
110
+ // REVISIT: results isn't an array, when no entries -> how could that work? when do we have no entries?
111
+ return results.reduce((total, affectedRows) => (total += affectedRows.changes), 0)
110
112
  }
111
113
 
112
114
  /**
@@ -129,21 +131,15 @@ class SQLService extends DatabaseService {
129
131
  * @type {Handler}
130
132
  */
131
133
  async onSTREAM(req) {
132
- const { sql, values, entries } = this.cqn2sql(req.query)
134
+ const { one, sql, values } = this.cqn2sql(req.query)
133
135
  // writing stream
134
136
  if (req.query.STREAM.into) {
135
- const stream = entries[0]
136
- stream.on('error', () => stream.removeAllListeners('error'))
137
- values.unshift(stream)
138
137
  const ps = await this.prepare(sql)
139
138
  return (await ps.run(values)).changes
140
139
  }
141
140
  // reading stream
142
141
  const ps = await this.prepare(sql)
143
- let result = await ps.all(values)
144
- if (result.length === 0) return
145
-
146
- return Object.values(result[0])[0]
142
+ return ps.stream(values, one)
147
143
  }
148
144
 
149
145
  /**
@@ -171,7 +167,12 @@ class SQLService extends DatabaseService {
171
167
  */
172
168
  async onPlainSQL({ query, data }, next) {
173
169
  if (typeof query === 'string') {
174
- DEBUG?.(query)
170
+ // REVISIT: this is a hack the target of $now might not be a timestamp or date time
171
+ // Add input converter to CURRENT_TIMESTAMP inside views using $now
172
+ if(/^CREATE VIEW.* CURRENT_TIMESTAMP[( ]/is.test(query)) {
173
+ query = query.replace(/CURRENT_TIMESTAMP/gi, 'ISO(CURRENT_TIMESTAMP)')
174
+ }
175
+ DEBUG?.(query, data)
175
176
  const ps = await this.prepare(query)
176
177
  const exec = this.hasResults(query) ? d => ps.all(d) : d => ps.run(d)
177
178
  if (Array.isArray(data) && typeof data[0] === 'object') return await Promise.all(data.map(exec))
@@ -200,8 +201,9 @@ class SQLService extends DatabaseService {
200
201
  const [max, offset = 0] = one ? [1] : _ ? [_.rows?.val, _.offset?.val] : []
201
202
  if (max === undefined || (n < max && (n || !offset))) return n + offset
202
203
  }
204
+ // REVISIT: made uppercase count because of HANA reserved word quoting
203
205
  const cq = cds.ql.clone(query, {
204
- columns: [{ func: 'count' }],
206
+ columns: [{ func: 'count', as: 'COUNT' }],
205
207
  localized: false,
206
208
  expand: false,
207
209
  limit: 0,
@@ -209,10 +211,14 @@ class SQLService extends DatabaseService {
209
211
  })
210
212
  const { sql, values } = this.cqn2sql(cq)
211
213
  const ps = await this.prepare(sql)
212
- const { count } = await ps.get(values)
213
- return count
214
+ const { count, COUNT } = await ps.get(values)
215
+ return count ?? COUNT
214
216
  }
215
217
 
218
+ /**
219
+ * Helper class for results of INSERTs.
220
+ * Subclasses may override this.
221
+ */
216
222
  static InsertResults = require('./InsertResults')
217
223
 
218
224
  /**
@@ -229,28 +235,22 @@ class SQLService extends DatabaseService {
229
235
  }
230
236
 
231
237
  /**
232
- * @param {import('@sap/cds/apis/cqn').Query} q
238
+ * @param {import('@sap/cds/apis/cqn').Query} query
233
239
  * @param {unknown} values
234
240
  * @returns {typeof SQLService.CQN2SQL}
235
241
  */
236
- cqn2sql(q, values) {
237
- const cqn = this.cqn4sql(q)
238
-
239
- // REVISIT: disable this for queries like (SELECT 1)
240
- // Will return multiple rows with objects inside
241
- // Only enable expand when the query is inferred
242
- if (cqn.SELECT && cqn.elements) cqn.SELECT.expand = cqn.SELECT.expand ?? 'root'
243
-
244
- const cmd = cqn.cmd || Object.keys(cqn)[0]
245
- if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || cqn.STREAM?.into) {
246
- let resolvedCqn = resolveView(cqn, this.model, this)
247
- if (resolvedCqn && resolvedCqn[cmd]._transitions?.[0].target) {
248
- resolvedCqn = resolvedCqn || cqn
249
- resolvedCqn.target = resolvedCqn?.[cmd]._transitions[0].target || cqn.target
250
- }
251
- return new this.class.CQN2SQL(this.context).render(resolvedCqn, values)
242
+ cqn2sql(query, values) {
243
+ let q = this.cqn4sql(query)
244
+ if (q.SELECT && q.elements) q.SELECT.expand = q.SELECT.expand ?? 'root'
245
+
246
+ let cmd = q.cmd || Object.keys(q)[0]
247
+ if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || q.STREAM?.into) {
248
+ q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
249
+ let target = q[cmd]._transitions?.[0].target
250
+ if (target) q.target = target // REVISIT: Why isn't that done in resolveView?
252
251
  }
253
- return new this.class.CQN2SQL(this.context).render(cqn, values)
252
+ let cqn2sql = new this.class.CQN2SQL(this)
253
+ return cqn2sql.render(q, values)
254
254
  }
255
255
 
256
256
  /**
@@ -258,18 +258,19 @@ class SQLService extends DatabaseService {
258
258
  * @returns {import('./infer/cqn').Query}
259
259
  */
260
260
  cqn4sql(q) {
261
- // REVISIT: move this check to cqn4sql?
262
- if (!q.SELECT?.from?.join && !this.model?.definitions[_target_name4(q)]) return _unquirked(q)
261
+ if (!q.SELECT?.from?.join && !q.SELECT?.from?.SELECT && !this.model?.definitions[_target_name4(q)]) return _unquirked(q)
263
262
  return cqn4sql(q, this.model)
264
263
  }
265
264
 
266
265
  /**
267
266
  * Returns a Promise which resolves to a prepared statement object with
268
267
  * `{run,get,all}` signature as specified in {@link PreparedStatement}.
268
+ * @abstract
269
+ * @param {string} sql The SQL String to be prepared
269
270
  * @returns {PreparedStatement}
270
271
  */
271
- // eslint-disable-next-line no-unused-vars
272
- async prepare(/*sql*/) {
272
+ async prepare(sql) {
273
+ sql
273
274
  throw '2b overridden by subclass'
274
275
  }
275
276
 
@@ -290,8 +291,10 @@ class SQLService extends DatabaseService {
290
291
  * @interface
291
292
  */
292
293
  class PreparedStatement {
294
+
293
295
  /**
294
296
  * Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
297
+ * @abstract
295
298
  * @param {unknown|unknown[]} binding_params
296
299
  */
297
300
  async run(binding_params) {
@@ -300,6 +303,7 @@ class PreparedStatement {
300
303
  }
301
304
  /**
302
305
  * Executes a prepared SELECT query and returns a single/first row only
306
+ * @abstract
303
307
  * @param {unknown|unknown[]} binding_params
304
308
  * @returns {Promise<unknown>}
305
309
  */
@@ -309,6 +313,7 @@ class PreparedStatement {
309
313
  }
310
314
  /**
311
315
  * Executes a prepared SELECT query and returns an array of all rows
316
+ * @abstract
312
317
  * @param {unknown|unknown[]} binding_params
313
318
  * @returns {Promise<unknown[]>}
314
319
  */
@@ -316,6 +321,15 @@ class PreparedStatement {
316
321
  binding_params
317
322
  return [{}]
318
323
  }
324
+ /**
325
+ * Executes a prepared SELECT query and returns a stream of the result
326
+ * @abstract
327
+ * @param {unknown|unknown[]} binding_params
328
+ * @returns {ReadableStream<string|Buffer>} A stream of the result
329
+ */
330
+ async stream(binding_params) {
331
+ binding_params
332
+ }
319
333
  }
320
334
  SQLService.prototype.PreparedStatement = PreparedStatement
321
335
 
@@ -329,29 +343,26 @@ const _target_name4 = q => {
329
343
  q.CREATE?.entity ||
330
344
  q.DROP?.entity ||
331
345
  q.STREAM?.from ||
332
- q.STREAM?.into ||
333
- undefined
334
- if (target?.SET?.op === 'union') throw new cds.error('”UNION” based queries are not supported')
346
+ q.STREAM?.into
347
+ if (target?.SET?.op === 'union') throw new cds.error('UNION-based queries are not supported')
335
348
  if (!target?.ref) return target
336
349
  const [first] = target.ref
337
350
  return first.id || first
338
351
  }
339
352
 
340
353
  const _unquirked = q => {
341
- if (typeof q.INSERT?.into === 'string') q.INSERT.into = { ref: [q.INSERT.into] }
342
- if (typeof q.UPSERT?.into === 'string') q.UPSERT.into = { ref: [q.UPSERT.into] }
343
- if (typeof q.UPDATE?.entity === 'string') q.UPDATE.entity = { ref: [q.UPDATE.entity] }
344
- if (typeof q.DELETE?.from === 'string') q.DELETE.from = { ref: [q.DELETE.from] }
345
- if (typeof q.CREATE?.entity === 'string') q.CREATE.entity = { ref: [q.CREATE.entity] }
346
- if (typeof q.DROP?.entity === 'string') q.DROP.entity = { ref: [q.DROP.entity] }
354
+ if (!q) return q
355
+ else if (typeof q.SELECT?.from === 'string') q.SELECT.from = { ref: [q.SELECT.from] }
356
+ else if (typeof q.INSERT?.into === 'string') q.INSERT.into = { ref: [q.INSERT.into] }
357
+ else if (typeof q.UPSERT?.into === 'string') q.UPSERT.into = { ref: [q.UPSERT.into] }
358
+ else if (typeof q.UPDATE?.entity === 'string') q.UPDATE.entity = { ref: [q.UPDATE.entity] }
359
+ else if (typeof q.DELETE?.from === 'string') q.DELETE.from = { ref: [q.DELETE.from] }
360
+ else if (typeof q.CREATE?.entity === 'string') q.CREATE.entity = { ref: [q.CREATE.entity] }
361
+ else if (typeof q.DROP?.entity === 'string') q.DROP.entity = { ref: [q.DROP.entity] }
347
362
  return q
348
363
  }
349
364
 
350
- const sqls = new (class extends SQLService {
351
- get factory() {
352
- return null
353
- }
354
- })()
365
+ const sqls = new class extends SQLService { get factory() { return null } }
355
366
  cds.extend(cds.ql.Query).with(
356
367
  class {
357
368
  forSQL() {
@@ -359,6 +370,7 @@ cds.extend(cds.ql.Query).with(
359
370
  return this.flat(cqn)
360
371
  }
361
372
  toSQL() {
373
+ if (this.SELECT) this.SELECT.expand = 'root' // Enforces using json functions always for top-level SELECTS
362
374
  let { sql, values } = (cds.db || sqls).cqn2sql(this)
363
375
  return { sql, values } // skipping .cqn property
364
376
  }
@@ -1,16 +1,16 @@
1
+ const SessionContext = require('./session-context')
2
+ const ConnectionPool = require('./generic-pool')
1
3
  const infer = require('../infer')
2
- const cds = require('@sap/cds')
3
-
4
- function Pool(factory, tenant) {
5
- const pool = createPool({ __proto__: factory, create: factory.create.bind(undefined, tenant) }, factory.options)
6
- pool._trackedConnections = []
7
- return pool
8
- }
9
- const { createPool } = require('@sap/cds-foss').pool
4
+ const cds = require('@sap/cds/lib')
10
5
 
11
6
  /** @typedef {unknown} DatabaseDriver */
12
7
 
13
8
  class DatabaseService extends cds.Service {
9
+ /**
10
+ * Dictionary of connection pools per tenant
11
+ */
12
+ pools = { _factory: this.factory }
13
+
14
14
  /**
15
15
  * Return a pool factory + options property as expected by
16
16
  * https://github.com/coopernurse/node-pool#createpool.
@@ -20,120 +20,89 @@ class DatabaseService extends cds.Service {
20
20
  get factory() {
21
21
  throw '2b overriden in subclass'
22
22
  }
23
- pools = { _factory: this.factory }
24
-
25
- /**
26
- * @returns {boolean} whether this service is multi tenant enabled
27
- */
28
- get isMultitenant() {
29
- return 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
30
- }
31
-
32
- /**
33
- * @typedef {Object} DefaultSessionVariables
34
- * @property {string} '$user.id'
35
- * @property {string} '$user.locale'
36
- * @property {string} '$valid.from'
37
- * @property {string} '$valid.to'
38
- */
39
23
 
40
24
  /**
41
- * Set one or more session context variables
42
- * @example
43
- * ```js
44
- * const tx = cds.db.tx()
45
- * tx.set({
46
- * '$user.name': 'Alice',
47
- * '$user.role': 'admin'
48
- * })
49
- * ```
50
- * @param {unknown|DefaultSessionVariables} variables
25
+ * Set one or more session context variables like so:
26
+ *
27
+ * const tx = cds.db.tx()
28
+ * tx.set({ foo: 'bar' })
29
+ *
30
+ * This is used in this.begin() for standard properties
31
+ * like `$user.id` or `$user.locale`.
51
32
  */
33
+ // eslint-disable-next-line no-unused-vars
52
34
  set(variables) {
53
- variables
54
35
  throw '2b overridden by subclass'
55
36
  }
56
37
 
57
38
  /**
58
- * @param {import('@sap/cds/apis/cqn').Query} q
59
- * @param {import('@sap/cds/apis/csn').CSN} m
60
- * @returns {import('../infer/cqn').Query}
61
- */
62
- infer(q, m = this.model) {
63
- return infer(q, m)
64
- }
65
-
66
- /**
67
- * @returns {Promise<DatabaseService>}
39
+ * Acquires a pooled connection and starts a session, including setting
40
+ * session context like `$user.id` or `$user.locale`, and starting a
41
+ * transaction with `BEGIN`
42
+ * @returns this
68
43
  */
69
44
  async begin() {
45
+ // We expect tx.begin() being called for an txed db service
70
46
  const ctx = this.context
71
- if (!ctx) return this.tx().begin()
72
- const tenant = this.isMultitenant && ctx.tenant
73
- const pool = (this.pools[tenant] ??= new Pool(this.pools._factory, tenant))
74
- const connections = pool._trackedConnections
75
- let dbc
76
- try {
77
- /** @type {DatabaseDriver} */
78
- dbc = this.dbc = await pool.acquire()
79
- } catch (err) {
80
- // TODO: add acquire timeout error check
81
- err.stack += `\nActive connections:${connections.length}\n${connections.map(c => c._beginStack.stack).join('\n')}`
82
- throw err
83
- }
84
- this._beginStack = new Error('begin called from:')
85
- connections.push(this)
86
- /**
87
- * @param {DatabaseDriver} dbc
88
- */
89
- this._release = async dbc => {
90
- await pool.release(dbc)
91
- connections.splice(connections.indexOf(this), 1)
92
- }
47
+ if (!ctx) return this.tx().begin() // REVISIT: Is this correct? When does this happen?
48
+
49
+ // REVISIT: tenant should be undefined if !this.isMultitenant
50
+ let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
51
+ let tenant = isMultitenant && ctx.tenant
52
+
53
+ // Setting this.pool as used in this.acquire() and this.release()
54
+ this.pool = this.pools[tenant] ??= new ConnectionPool(this.pools._factory, tenant)
55
+
56
+ // Acquire a pooled connection
57
+ this.dbc = await this.acquire()
58
+
59
+ // Begin a session...
93
60
  try {
94
- // Setting session context variables
95
- await this.set({
96
- get '$user.id'() {
97
- return _set(this, '$user.id', ctx.user?.id || 'anonymous')
98
- },
99
- get '$user.locale'() {
100
- return _set(this, '$user.locale', ctx.locale || cds.env.i18n.default_language)
101
- },
102
- get '$valid.from'() {
103
- return _set(this, '$valid.from', ctx._?.['VALID-FROM'] ?? ctx._?.['VALID-AT'] ?? '1970-01-01T00:00:00.000Z')
104
- },
105
- get '$valid.to'() {
106
- return _set(
107
- this,
108
- '$valid.to',
109
- ctx._?.['VALID-TO'] ?? _validTo4(ctx._?.['VALID-AT']) ?? '9999-11-11T22:22:22.000Z',
110
- )
111
- },
112
- })
113
- // Run BEGIN
61
+ await this.set(new SessionContext(ctx))
114
62
  await this.send('BEGIN')
115
63
  } catch (e) {
116
- this._release(dbc)
64
+ this.release()
117
65
  throw e
118
66
  }
119
67
  return this
120
68
  }
121
69
 
70
+ /**
71
+ * Commits a transaction and releases the connection to the pool.
72
+ */
122
73
  async commit() {
123
- const dbc = this.dbc
124
- if (!dbc) return
74
+ if (!this.dbc) return
125
75
  await this.send('COMMIT')
126
- this._release(dbc) // only release on successful commit as otherwise released on rollback
76
+ this.release() // only release on successful commit as otherwise released on rollback
127
77
  }
128
78
 
79
+ /**
80
+ * Rolls back a transaction and releases the connection to the pool.
81
+ */
129
82
  async rollback() {
130
- const dbc = this.dbc
131
- if (!dbc) return
132
- try {
133
- await this.send('ROLLBACK')
134
- } finally {
135
- this._release(dbc)
136
- }
83
+ if (!this.dbc) return
84
+ else
85
+ try {
86
+ await this.send('ROLLBACK')
87
+ } finally {
88
+ this.release()
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Acquires a connection from this.pool, stored into this.dbc
94
+ * This is for subclasses to intercept, if required.
95
+ */
96
+ async acquire() {
97
+ return await this.pool.acquire()
98
+ }
99
+
100
+ /**
101
+ * Releases own connection, i.e. tix.dbc, from this.pool
102
+ * This is for subclasses to intercept, if required.
103
+ */
104
+ async release() {
105
+ return this.pool.release(this.dbc)
137
106
  }
138
107
 
139
108
  // REVISIT: should happen automatically after a configurable time
@@ -142,18 +111,24 @@ class DatabaseService extends cds.Service {
142
111
  */
143
112
  async disconnect(tenant) {
144
113
  const pool = this.pools[tenant]
145
- if (pool) delete this.pools[tenant]
146
- else return
114
+ if (!pool) return
147
115
  await pool.drain()
148
116
  await pool.clear()
117
+ delete this.pools[tenant]
149
118
  }
150
119
 
151
120
  /**
152
- * Runs a Query on the database service
153
- * @param {import("@sap/cds/apis/cqn").Query} query
154
- * @param {unknown} data
155
- * @param {...unknown} etc
156
- * @returns {Promise<unknown>}
121
+ * Infers the given query with this DatabaseService instance's model.
122
+ * In general `this.model` is the same then `cds.model`
123
+ * @param {CQN} query - the query to infer
124
+ * @returns {CQN} the inferred query
125
+ */
126
+ infer(query) {
127
+ return infer(query, this.model)
128
+ }
129
+
130
+ /**
131
+ * DatabaseServices also support passing native query strings to underlying databases.
157
132
  */
158
133
  run(query, data, ...etc) {
159
134
  // Allow db.run('...',1,2,3,4)
@@ -162,33 +137,12 @@ class DatabaseService extends cds.Service {
162
137
  }
163
138
 
164
139
  /**
165
- * Generated the database url for the given tenant
166
- * @param {string} tenant
167
- * @returns {string}
140
+ * @returns {string} A url-like string used to print log output,
141
+ * e.g., in cds.deploy()
168
142
  */
169
- url4(tenant) {
170
- tenant
171
- let { url } = this.options?.credentials || this.options || {}
172
- return url
143
+ url4(/*tenant*/) {
144
+ return this.options.credentials?.url || this.options.url
173
145
  }
174
-
175
- /**
176
- * Old name of url4
177
- * @deprecated
178
- * @param {string} tenant
179
- * @returns {string}
180
- */
181
- getDbUrl(tenant) {
182
- return this.url4(tenant)
183
- } // REVISIT: Remove after cds v6.7
184
- }
185
-
186
- const _set = (context, variable, value) => {
187
- Object.defineProperty(context, variable, { value, configurable: true })
188
- return value
189
- }
190
- const _validTo4 = validAt => {
191
- return validAt?.replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || '')
192
146
  }
193
147
 
194
148
  DatabaseService.prototype.isDatabaseService = true
@@ -0,0 +1,34 @@
1
+ const { createPool } = require('@sap/cds-foss').pool
2
+
3
+ class ConnectionPool {
4
+ constructor(factory, tenant) {
5
+ let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
6
+ return _track_connections4(createPool(bound_factory, factory.options))
7
+ }
8
+ }
9
+
10
+ // REVISIT: Is that really neccessary ?!
11
+ function _track_connections4(pool) {
12
+ const { acquire, release } = pool
13
+ return Object.assign(pool, {
14
+ async acquire() {
15
+ const connections = (this._trackedConnections ??= new Set())
16
+ try {
17
+ let dbc = await acquire.call(this)
18
+ connections.add((dbc._beginStack = new Error('begin called from:')))
19
+ return dbc
20
+ } catch (err) {
21
+ // TODO: add acquire timeout error check
22
+ err.stack += `\nActive connections:${connections.size}\n${[...connections].map(e => e.stack).join('\n')}`
23
+ throw err
24
+ }
25
+ },
26
+
27
+ release(dbc) {
28
+ this._trackedConnections?.delete(dbc._beginStack)
29
+ return release.call(this, dbc)
30
+ },
31
+ })
32
+ }
33
+
34
+ module.exports = ConnectionPool
@@ -0,0 +1,32 @@
1
+ const cds = require('@sap/cds/lib')
2
+
3
+ class SessionContext {
4
+ constructor(ctx) {
5
+ Object.defineProperty(this, 'ctx', { value: ctx })
6
+ }
7
+ get '$user.id'() {
8
+ return (super['$user.id'] = this.ctx.user?.id || 'anonymous')
9
+ }
10
+ get '$user.locale'() {
11
+ return (super['$user.locale'] = this.ctx.locale || cds.env.i18n.default_language)
12
+ }
13
+ // REVISIT: should be decided in spec meeting for definitive name
14
+ get $now() {
15
+ return (super.$now = (this.ctx.timestamp || new Date()).toISOString())
16
+ }
17
+ }
18
+
19
+ class TemporalSessionContext extends SessionContext {
20
+ get '$valid.from'() {
21
+ return (super['$valid.from'] = this.ctx._?.['VALID-FROM'] ?? this.ctx._?.['VALID-AT'] ?? new Date().toISOString())
22
+ }
23
+ get '$valid.to'() {
24
+ return (super['$valid.to'] =
25
+ this.ctx._?.['VALID-TO'] ??
26
+ this.ctx._?.['VALID-AT']?.replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || '') ??
27
+ new Date().toISOString().replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || ''))
28
+ }
29
+ }
30
+
31
+ // REVISIT: only set temporal context if required!
32
+ module.exports = TemporalSessionContext
@@ -31,7 +31,8 @@ const StandardFunctions = {
31
31
  * @param {...string} args
32
32
  * @returns {string}
33
33
  */
34
- concat: (...args) => args.join('||'),
34
+ concat: (...args) => args.map(a => a.xpr ? `(${a})` : a).join(' || '),
35
+
35
36
  /**
36
37
  * Generates SQL statement that produces a boolean value indicating whether the first string contains the second string
37
38
  * @param {...string} args
@@ -139,6 +140,11 @@ const StandardFunctions = {
139
140
  round: (x, p) => `round(${x}${p ? `,${p}` : ''})`,
140
141
 
141
142
  // Date and Time Functions
143
+
144
+ current_date: p => p ? `current_date(${p})`: 'current_date',
145
+ current_time: p => p ? `current_time(${p})`: 'current_time',
146
+ current_timestamp: p => p ? `current_timestamp(${p})`: 'current_timestamp',
147
+
142
148
  /**
143
149
  * Generates SQL statement that produces the year of a given timestamp
144
150
  * @param {string} x
@@ -241,6 +247,13 @@ const StandardFunctions = {
241
247
  )
242
248
  ) * 86400
243
249
  )`,
250
+
251
+ /**
252
+ * Generates SQL statement that calls the session_context function with the given parameter
253
+ * @param {string} x session variable name or SQL expression
254
+ * @returns {string}
255
+ */
256
+ session_context: x => `session_context('${x.val}')`,
244
257
  }
245
258
 
246
259
  const HANAFunctions = {