@cap-js/db-service 1.0.1 → 1.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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
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.1.0 - 2023-08-01
8
+
9
+ ### Fixed
10
+
11
+ - `UPDATE` with path expressions do not end up in a dump anymore. Instead, a proper error message is emitted.
12
+ - `UPDATE` is only noop if it does not include an element annotated with `@cds.on.update`.
13
+ - `SELECT` with `'*'` that is not expanded creates now a clearer error when the column name is required.
14
+ - `SELECT` with plain SQL statements will return correct result regardless of casing.
15
+ - View resolving for streams.
16
+
7
17
  ## Version 1.0.1 - 2023-07-03
8
18
 
9
19
  ### Fixed
@@ -12,7 +22,6 @@
12
22
  are now correctly substituted.
13
23
  - Mapping for OData `average` function to ANSI SQL compliant `avg` function.
14
24
 
15
-
16
25
  ## Version 1.0.0 - 2023-06-23
17
26
 
18
- - Initial Release
27
+ - Initial Release
package/index.js CHANGED
@@ -1,4 +1,18 @@
1
+ const DatabaseService = require('./lib/common/DatabaseService')
2
+ const SQLService = require('./lib/SQLService')
3
+ const CQN2SQL = require('./lib/cqn2sql').classDefinition
4
+
5
+ /**
6
+ * @template T
7
+ * @typedef {import('./lib/common/factory').Factory<T>} Factory
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('./lib/SQLService').prototype.PreparedStatement} PreparedStatement
12
+ */
13
+
1
14
  module.exports = {
2
- DatabaseService: require('./lib/common/DatabaseService'),
3
- SQLService: require('./lib/SQLService'),
15
+ DatabaseService,
16
+ SQLService,
17
+ CQN2SQL,
4
18
  }
@@ -12,12 +12,16 @@ const USAGE_SAMPLE = async () => {
12
12
  }
13
13
 
14
14
  module.exports = class InsertResult {
15
+ /**
16
+ * @param {import('@sap/cds/apis/cqn').INSERT} query
17
+ * @param {unknown[]} results
18
+ */
15
19
  constructor(query, results) {
16
20
  this.query = query
17
21
  this.results = results
18
22
  }
19
23
 
20
- /*
24
+ /**
21
25
  * Lazy access to auto-generated keys.
22
26
  */
23
27
  get [iterator]() {
@@ -70,8 +74,9 @@ module.exports = class InsertResult {
70
74
  })
71
75
  }
72
76
 
73
- /*
77
+ /**
74
78
  * the number of inserted (root) entries or the number of affectedRows in case of INSERT into SELECT
79
+ * @return {number}
75
80
  */
76
81
  get affectedRows() {
77
82
  const { INSERT: _ } = this.query
@@ -79,16 +84,28 @@ module.exports = class InsertResult {
79
84
  else return (super.affectedRows = _.entries?.length || _.rows?.length || this.results.length || 1)
80
85
  }
81
86
 
82
- /*
87
+ /**
83
88
  * for checks such as res > 2
89
+ * @return {number}
84
90
  */
85
91
  valueOf() {
86
92
  return this.affectedRows
87
93
  }
88
94
 
95
+ /**
96
+ * The last id of the auto incremented key column
97
+ * @param {unknown[]} result
98
+ * @returns {number}
99
+ */
89
100
  insertedRowId4(result) {
90
101
  return result.lastID
91
102
  }
103
+
104
+ /**
105
+ * Number of affected rows
106
+ * @param {unknown[]} result
107
+ * @returns {number}
108
+ */
92
109
  affectedRows4(result) {
93
110
  return result.changes
94
111
  }
package/lib/SQLService.js CHANGED
@@ -4,6 +4,19 @@ const { resolveView } = require('@sap/cds/libx/_runtime/common/utils/resolveView
4
4
  const DatabaseService = require('./common/DatabaseService')
5
5
  const cqn4sql = require('./cqn4sql')
6
6
 
7
+ /**
8
+ * @callback next
9
+ * @param {Error} param0
10
+ * @returns {Promise<unknown>}
11
+ */
12
+
13
+ /**
14
+ * @callback Handler
15
+ * @param {import('@sap/cds/apis/services').Request} param0
16
+ * @param {next} param1
17
+ * @returns {Promise<unknown>}
18
+ */
19
+
7
20
  class SQLService extends DatabaseService {
8
21
  init() {
9
22
  this.on(['SELECT'], this.transformStreamFromCQN)
@@ -21,6 +34,7 @@ class SQLService extends DatabaseService {
21
34
  return super.init()
22
35
  }
23
36
 
37
+ /** @type {Handler} */
24
38
  async transformStreamFromCQN({ query }, next) {
25
39
  if (!query._streaming) return next()
26
40
  const cqn = STREAM.from(query.SELECT.from).column(query.SELECT.columns[0].ref[0])
@@ -29,6 +43,7 @@ class SQLService extends DatabaseService {
29
43
  return stream && { value: stream }
30
44
  }
31
45
 
46
+ /** @type {Handler} */
32
47
  async transformStreamIntoCQN({ query, data, target }, next) {
33
48
  let col, type, etag
34
49
  const elements = query._target?.elements || target?.elements
@@ -56,11 +71,11 @@ class SQLService extends DatabaseService {
56
71
  return result
57
72
  }
58
73
 
59
- /** Handler for SELECT */
74
+ /**
75
+ * Handler for SELECT
76
+ * @type {Handler}
77
+ */
60
78
  async onSELECT({ query, data }) {
61
- // REVISIT: disable this for queries like (SELECT 1)
62
- // Will return multiple rows with objects inside
63
- query.SELECT.expand = 'root'
64
79
  const { sql, values, cqn } = this.cqn2sql(query, data)
65
80
  let ps = await this.prepare(sql)
66
81
  let rows = await ps.all(values)
@@ -70,6 +85,10 @@ class SQLService extends DatabaseService {
70
85
  return cqn.SELECT.one || query.SELECT.from.ref?.[0].cardinality?.max === 1 ? rows[0] : rows
71
86
  }
72
87
 
88
+ /**
89
+ * Handler for INSERT
90
+ * @type {Handler}
91
+ */
73
92
  async onINSERT({ query, data }) {
74
93
  const { sql, entries, cqn } = this.cqn2sql(query, data)
75
94
  if (!sql) return // Do nothing when there is nothing to be done
@@ -78,6 +97,10 @@ class SQLService extends DatabaseService {
78
97
  return new this.class.InsertResults(cqn, results)
79
98
  }
80
99
 
100
+ /**
101
+ * Handler for UPSERT
102
+ * @type {Handler}
103
+ */
81
104
  async onUPSERT({ query, data }) {
82
105
  const { sql, entries } = this.cqn2sql(query, data)
83
106
  if (!sql) return // Do nothing when there is nothing to be done
@@ -86,13 +109,25 @@ class SQLService extends DatabaseService {
86
109
  return results.reduce((lastValue, currentValue) => (lastValue += currentValue.changes), 0)
87
110
  }
88
111
 
89
- /** Handler for UPDATE */
112
+ /**
113
+ * Handler for UPDATE
114
+ * @type {Handler}
115
+ */
90
116
  async onUPDATE(req) {
91
- if (!req.query.UPDATE.data && !req.query.UPDATE.with) return 0
117
+ // noop if not a touch for @cds.on.update
118
+ if (
119
+ !req.query.UPDATE.data &&
120
+ !req.query.UPDATE.with &&
121
+ !Object.values(req.target?.elements || {}).some(e => e['@cds.on.update'])
122
+ )
123
+ return 0
92
124
  return this.onSIMPLE(req)
93
125
  }
94
126
 
95
- /** Handler for Stream */
127
+ /**
128
+ * Handler for Stream
129
+ * @type {Handler}
130
+ */
96
131
  async onSTREAM(req) {
97
132
  const { sql, values, entries } = this.cqn2sql(req.query)
98
133
  // writing stream
@@ -111,20 +146,29 @@ class SQLService extends DatabaseService {
111
146
  return Object.values(result[0])[0]
112
147
  }
113
148
 
114
- /** Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN */
149
+ /**
150
+ * Handler for CREATE, DROP, UPDATE, DELETE, with simple CQN
151
+ * @type {Handler}
152
+ */
115
153
  async onSIMPLE({ query, data }) {
116
154
  const { sql, values } = this.cqn2sql(query, data)
117
155
  let ps = await this.prepare(sql)
118
156
  return (await ps.run(values)).changes
119
157
  }
120
158
 
121
- /** Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN */
159
+ /**
160
+ * Handler for BEGIN, COMMIT, ROLLBACK, which don't have any CQN
161
+ * @type {Handler}
162
+ */
122
163
  async onEVENT({ event }) {
123
164
  DEBUG?.(event) // in the other cases above DEBUG happens in cqn2sql
124
165
  return await this.exec(event)
125
166
  }
126
167
 
127
- /** Handler for SQL statements which don't have any CQN */
168
+ /**
169
+ * Handler for SQL statements which don't have any CQN
170
+ * @type {Handler}
171
+ */
128
172
  async onPlainSQL({ query, data }, next) {
129
173
  if (typeof query === 'string') {
130
174
  DEBUG?.(query)
@@ -135,12 +179,20 @@ class SQLService extends DatabaseService {
135
179
  } else return next()
136
180
  }
137
181
 
138
- /** Override in subclasses to detect more statements to be called with ps.all() */
182
+ /**
183
+ * Override in subclasses to detect more statements to be called with ps.all()
184
+ * @param {string} sql
185
+ */
139
186
  hasResults(sql) {
140
- return /^(SELECT|WITH|CALL|PRAGMA table_info)/.test(sql)
187
+ return /^(SELECT|WITH|CALL|PRAGMA table_info)/i.test(sql)
141
188
  }
142
189
 
143
- /** Derives and executes a query to fill in `$count` for given query */
190
+ /**
191
+ * Derives and executes a query to fill in `$count` for given query
192
+ * @param {import('@sap/cds/apis/cqn').SELECT} query - SELECT CQN
193
+ * @param {unknown[]} ret - Results of the original query
194
+ * @returns {Promise<number>}
195
+ */
144
196
  async count(query, ret) {
145
197
  if (ret) {
146
198
  const { one, limit: _ } = query.SELECT,
@@ -168,15 +220,29 @@ class SQLService extends DatabaseService {
168
220
  * Subclasses commonly override this.
169
221
  */
170
222
  static CQN2SQL = require('./cqn2sql').class
171
- constructor() {
172
- super(...arguments)
223
+
224
+ /** @param {unknown[]} args */
225
+ constructor(...args) {
226
+ super(...args)
227
+ /** @type {unknown} */
173
228
  this.class = new.target // for IntelliSense
174
229
  }
230
+
231
+ /**
232
+ * @param {import('@sap/cds/apis/cqn').Query} q
233
+ * @param {unknown} values
234
+ * @returns {typeof SQLService.CQN2SQL}
235
+ */
175
236
  cqn2sql(q, values) {
176
237
  const cqn = this.cqn4sql(q)
177
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
+
178
244
  const cmd = cqn.cmd || Object.keys(cqn)[0]
179
- if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 }) {
245
+ if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || cqn.STREAM?.into) {
180
246
  let resolvedCqn = resolveView(cqn, this.model, this)
181
247
  if (resolvedCqn && resolvedCqn[cmd]._transitions?.[0].target) {
182
248
  resolvedCqn = resolvedCqn || cqn
@@ -186,6 +252,11 @@ class SQLService extends DatabaseService {
186
252
  }
187
253
  return new this.class.CQN2SQL(this.context).render(cqn, values)
188
254
  }
255
+
256
+ /**
257
+ * @param {import('@sap/cds/apis/cqn').Query} q
258
+ * @returns {import('./infer/cqn').Query}
259
+ */
189
260
  cqn4sql(q) {
190
261
  // REVISIT: move this check to cqn4sql?
191
262
  if (!q.SELECT?.from?.join && !this.model?.definitions[_target_name4(q)]) return _unquirked(q)
@@ -204,35 +275,47 @@ class SQLService extends DatabaseService {
204
275
 
205
276
  /**
206
277
  * Used to execute simple SQL statement like BEGIN, COMMIT, ROLLBACK
278
+ * @param {string} sql
279
+ * @returns {Promise<unknown>} The result of the query
207
280
  */
208
- // eslint-disable-next-line no-unused-vars
209
281
  async exec(sql) {
282
+ sql
210
283
  throw '2b overridden by subclass'
211
284
  }
212
285
  }
213
286
 
214
- /** Interface of prepared statement objects as returned by {@link SQLService#prepare} */
287
+ /**
288
+ * Interface of prepared statement objects as returned by {@link SQLService#prepare}
289
+ * @class
290
+ * @interface
291
+ */
215
292
  class PreparedStatement {
216
- // eslint-disable-line no-unused-vars
217
293
  /**
218
294
  * Executes a prepared DML query, i.e., INSERT, UPDATE, DELETE, CREATE, DROP
219
- * @param {[]|{}} binding_params
295
+ * @param {unknown|unknown[]} binding_params
220
296
  */
221
- async run(/*binding_params*/) {} // eslint-disable-line no-unused-vars
297
+ async run(binding_params) {
298
+ binding_params
299
+ return 0
300
+ }
222
301
  /**
223
302
  * Executes a prepared SELECT query and returns a single/first row only
224
- * @param {[]|{}} binding_params
303
+ * @param {unknown|unknown[]} binding_params
304
+ * @returns {Promise<unknown>}
225
305
  */
226
- async get(/*binding_params*/) {
306
+ async get(binding_params) {
307
+ binding_params
227
308
  return {}
228
- } // eslint-disable-line no-unused-vars
309
+ }
229
310
  /**
230
311
  * Executes a prepared SELECT query and returns an array of all rows
231
- * @param {[]|{}} binding_params
312
+ * @param {unknown|unknown[]} binding_params
313
+ * @returns {Promise<unknown[]>}
232
314
  */
233
- async all(/*binding_params*/) {
315
+ async all(binding_params) {
316
+ binding_params
234
317
  return [{}]
235
- } // eslint-disable-line no-unused-vars
318
+ }
236
319
  }
237
320
  SQLService.prototype.PreparedStatement = PreparedStatement
238
321
 
@@ -285,4 +368,5 @@ cds.extend(cds.ql.Query).with(
285
368
  },
286
369
  )
287
370
 
288
- module.exports = Object.assign(SQLService, { _target_name4 })
371
+ Object.assign(SQLService, { _target_name4 })
372
+ module.exports = SQLService
@@ -8,22 +8,38 @@ function Pool(factory, tenant) {
8
8
  }
9
9
  const { createPool } = require('@sap/cds-foss').pool
10
10
 
11
+ /** @typedef {unknown} DatabaseDriver */
12
+
11
13
  class DatabaseService extends cds.Service {
12
14
  /**
13
15
  * Return a pool factory + options property as expected by
14
16
  * https://github.com/coopernurse/node-pool#createpool.
17
+ * @abstract
18
+ * @type {import('./factory').Factory<DatabaseDriver>}
15
19
  */
16
20
  get factory() {
17
21
  throw '2b overriden in subclass'
18
22
  }
19
23
  pools = { _factory: this.factory }
20
24
 
25
+ /**
26
+ * @returns {boolean} whether this service is multi tenant enabled
27
+ */
21
28
  get isMultitenant() {
22
29
  return 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
23
30
  }
24
31
 
25
32
  /**
26
- * Set one or more session context variables like so:
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
+
40
+ /**
41
+ * Set one or more session context variables
42
+ * @example
27
43
  * ```js
28
44
  * const tx = cds.db.tx()
29
45
  * tx.set({
@@ -31,16 +47,25 @@ class DatabaseService extends cds.Service {
31
47
  * '$user.role': 'admin'
32
48
  * })
33
49
  * ```
50
+ * @param {unknown|DefaultSessionVariables} variables
34
51
  */
35
- // eslint-disable-next-line no-unused-vars
36
52
  set(variables) {
53
+ variables
37
54
  throw '2b overridden by subclass'
38
55
  }
39
56
 
57
+ /**
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
+ */
40
62
  infer(q, m = this.model) {
41
63
  return infer(q, m)
42
64
  }
43
65
 
66
+ /**
67
+ * @returns {Promise<DatabaseService>}
68
+ */
44
69
  async begin() {
45
70
  const ctx = this.context
46
71
  if (!ctx) return this.tx().begin()
@@ -49,6 +74,7 @@ class DatabaseService extends cds.Service {
49
74
  const connections = pool._trackedConnections
50
75
  let dbc
51
76
  try {
77
+ /** @type {DatabaseDriver} */
52
78
  dbc = this.dbc = await pool.acquire()
53
79
  } catch (err) {
54
80
  // TODO: add acquire timeout error check
@@ -57,6 +83,9 @@ class DatabaseService extends cds.Service {
57
83
  }
58
84
  this._beginStack = new Error('begin called from:')
59
85
  connections.push(this)
86
+ /**
87
+ * @param {DatabaseDriver} dbc
88
+ */
60
89
  this._release = async dbc => {
61
90
  await pool.release(dbc)
62
91
  connections.splice(connections.indexOf(this), 1)
@@ -108,6 +137,9 @@ class DatabaseService extends cds.Service {
108
137
  }
109
138
 
110
139
  // REVISIT: should happen automatically after a configurable time
140
+ /**
141
+ * @param {string} tenant
142
+ */
111
143
  async disconnect(tenant) {
112
144
  const pool = this.pools[tenant]
113
145
  if (pool) delete this.pools[tenant]
@@ -116,17 +148,36 @@ class DatabaseService extends cds.Service {
116
148
  await pool.clear()
117
149
  }
118
150
 
151
+ /**
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>}
157
+ */
119
158
  run(query, data, ...etc) {
120
159
  // Allow db.run('...',1,2,3,4)
121
160
  if (data !== undefined && typeof query === 'string' && typeof data !== 'object') data = [data, ...etc]
122
161
  return super.run(query, data)
123
162
  }
124
163
 
125
- url4(/*tenant*/) {
126
- // eslint-disable-line no-unused-vars
164
+ /**
165
+ * Generated the database url for the given tenant
166
+ * @param {string} tenant
167
+ * @returns {string}
168
+ */
169
+ url4(tenant) {
170
+ tenant
127
171
  let { url } = this.options?.credentials || this.options || {}
128
172
  return url
129
173
  }
174
+
175
+ /**
176
+ * Old name of url4
177
+ * @deprecated
178
+ * @param {string} tenant
179
+ * @returns {string}
180
+ */
130
181
  getDbUrl(tenant) {
131
182
  return this.url4(tenant)
132
183
  } // REVISIT: Remove after cds v6.7
@@ -0,0 +1,5 @@
1
+ import { Factory as GenericFactory, Options } from 'generic-pool'
2
+
3
+ export interface Factory<T> extends GenericFactory<T> {
4
+ options: Options
5
+ }
@@ -0,0 +1,24 @@
1
+ declare function ConverterFunction(expression: string): string
2
+ export type Converter = typeof ConverterFunction
3
+
4
+ export type Converters = {
5
+ UUID: Converter
6
+ String: Converter
7
+ LargeString: Converter
8
+ Binary: Converter
9
+ LargeBinary: Converter
10
+ Boolean: Converter
11
+ Integer: Converter
12
+ UInt8: Converter
13
+ Int16: Converter
14
+ Int32: Converter
15
+ Int64: Converter
16
+ Float: Converter
17
+ Double: Converter
18
+ Decimal: Converter
19
+ DecimalFloat: Converter
20
+ Date: Converter
21
+ Time: Converter
22
+ DateTime: Converter
23
+ Timestamp: Converter
24
+ }