@sap/cds-dk 10.0.3 → 10.0.4

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 (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
  3. package/node_modules/@cap-js/db-service/LICENSE +201 -0
  4. package/node_modules/@cap-js/db-service/README.md +26 -0
  5. package/node_modules/@cap-js/db-service/index.js +19 -0
  6. package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
  7. package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
  8. package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
  9. package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
  10. package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
  11. package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
  12. package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
  13. package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
  14. package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
  15. package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
  16. package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
  17. package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
  18. package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
  19. package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
  20. package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
  21. package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
  22. package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
  23. package/node_modules/@cap-js/db-service/lib/search.js +220 -0
  24. package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
  25. package/node_modules/@cap-js/db-service/package.json +36 -0
  26. package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
  27. package/node_modules/@cap-js/sqlite/LICENSE +201 -0
  28. package/node_modules/@cap-js/sqlite/README.md +34 -0
  29. package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
  30. package/node_modules/@cap-js/sqlite/index.js +2 -0
  31. package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
  32. package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
  33. package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
  34. package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
  35. package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
  36. package/node_modules/@cap-js/sqlite/package.json +68 -0
  37. package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
  38. package/node_modules/iconv-lite/encodings/utf32.js +10 -3
  39. package/node_modules/iconv-lite/package.json +2 -2
  40. package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
  41. package/package.json +5 -3
@@ -0,0 +1,179 @@
1
+ const SessionContext = require('./session-context')
2
+ const ConnectionPool = require('./generic-pool')
3
+ const infer = require('../infer')
4
+ const cds = require('@sap/cds')
5
+
6
+ /** @typedef {unknown} DatabaseDriver */
7
+
8
+ class DatabaseService extends cds.Service {
9
+
10
+ init() {
11
+ cds.on('shutdown', () => this.disconnect())
12
+ return super.init()
13
+ }
14
+
15
+ /**
16
+ * Dictionary of connection pools per tenant
17
+ */
18
+ pools = Object.setPrototypeOf({}, { _factory: this.factory })
19
+
20
+ /**
21
+ * Return a pool factory + options property as expected by
22
+ * https://github.com/coopernurse/node-pool#createpool.
23
+ * @abstract
24
+ * @type {import('./factory').Factory<DatabaseDriver>}
25
+ */
26
+ get factory() {
27
+ throw '2b overriden in subclass'
28
+ }
29
+
30
+ /**
31
+ * Set one or more session context variables like so:
32
+ *
33
+ * const tx = cds.db.tx()
34
+ * tx.set({ foo: 'bar' })
35
+ *
36
+ * This is used in this.begin() for standard properties
37
+ * like `$user.id` or `$user.locale`.
38
+ */
39
+ // eslint-disable-next-line no-unused-vars
40
+ set(variables) {
41
+ throw '2b overridden by subclass'
42
+ }
43
+
44
+ /**
45
+ * Acquires a pooled connection and starts a session, including setting
46
+ * session context like `$user.id` or `$user.locale`, and starting a
47
+ * transaction with `BEGIN`
48
+ * @returns this
49
+ */
50
+ async begin (min) {
51
+ // We expect tx.begin() being called for an txed db service
52
+ const ctx = this.context
53
+
54
+ // If .begin is called explicitly it starts a new transaction and executes begin
55
+ if (!ctx) return this.tx().begin(min)
56
+
57
+ // REVISIT: can we revisit the below revisit now?
58
+ // REVISIT: tenant should be undefined if !this.isMultitenant
59
+ let isMultitenant = 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
60
+ let tenant = isMultitenant && ctx.tenant
61
+
62
+ // Setting this.pool as used in this.acquire() and this.release()
63
+ this.pool = this.pools[tenant] ??= new ConnectionPool(this.pools._factory, tenant)
64
+
65
+ // Acquire a pooled connection
66
+ this.dbc = await this.acquire()
67
+ this.dbc.destroy = this.destroy.bind(this) // REVISIT: this is bad
68
+
69
+ // Begin a session...
70
+ if (!min) try {
71
+ await this.set(new SessionContext(ctx))
72
+ await this.send('BEGIN')
73
+ } catch (e) {
74
+ this.release()
75
+ throw e
76
+ }
77
+ return this
78
+ }
79
+
80
+ /**
81
+ * Commits a transaction and releases the connection to the pool.
82
+ */
83
+ async commit() {
84
+ if (!this.dbc) return
85
+ await this.send('COMMIT')
86
+ this.release() // only release on successful commit as otherwise released on rollback
87
+ }
88
+
89
+ /**
90
+ * Rolls back a transaction and releases the connection to the pool.
91
+ */
92
+ async rollback() {
93
+ if (!this.dbc) return
94
+ try {
95
+ await this.send('ROLLBACK')
96
+ } finally {
97
+ this.release()
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Acquires a connection from this.pool, stored into this.dbc
103
+ * This is for subclasses to intercept, if required.
104
+ */
105
+ async acquire() {
106
+ return await this.pool.acquire()
107
+ }
108
+
109
+ /**
110
+ * Releases own connection, i.e. tix.dbc, from this.pool
111
+ * This is for subclasses to intercept, if required.
112
+ */
113
+ async release() {
114
+ if (!this.dbc) return
115
+ const dbc = this.dbc
116
+ this.dbc = undefined
117
+ await this.pool.release(dbc)
118
+ }
119
+
120
+ /**
121
+ * Destroys own connection, i.e. tix.dbc, from this.pool
122
+ * This is for subclasses to intercept, if required.
123
+ */
124
+ async destroy() {
125
+ if (!this.dbc) return
126
+ const dbc = this.dbc
127
+ this.dbc = undefined
128
+ await this.pool.destroy(dbc)
129
+ }
130
+
131
+ // REVISIT: should happen automatically after a configurable time
132
+ async disconnect (tenant) {
133
+ const tenants = tenant ? [tenant] : Object.keys(this.pools)
134
+ await Promise.all (tenants.map (async t => {
135
+ const pool = this.pools[t]; if (!pool) return
136
+ delete this.pools[t]
137
+ await pool.drain()
138
+ await pool.clear()
139
+ }))
140
+ }
141
+
142
+ /**
143
+ * Infers the given query with this DatabaseService instance's model.
144
+ * In general `this.model` is the same then `cds.model`
145
+ * @param {CQN} query - the query to infer
146
+ * @returns {CQN} the inferred query
147
+ */
148
+ infer(query) {
149
+ return infer(query, this.model)
150
+ }
151
+
152
+ /**
153
+ * DatabaseServices also support passing native query strings to underlying databases.
154
+ */
155
+ run(query, data, ...etc) {
156
+ // Allow db.run('...',1,2,3,4)
157
+ if (data !== undefined && typeof query === 'string' && typeof data !== 'object') arguments[1] = [data, ...etc]
158
+ return super.run(...arguments) //> important to call like that for tagged template literal args
159
+ }
160
+
161
+ /**
162
+ * @returns {string} A url-like string used to print log output,
163
+ * e.g., in cds.deploy()
164
+ */
165
+ url4(/*tenant*/) {
166
+ return this.options.credentials?.url || this.options.url
167
+ }
168
+ }
169
+
170
+ const _as_array = changes => Object.assign ([],{ affected: changes })
171
+ const _as_number = changes => changes
172
+ DatabaseService.prototype._return_affected = _as_number
173
+ DatabaseService._always_return_arrays = on_off => {
174
+ if (on_off === undefined) return DatabaseService.prototype._return_affected === _as_array
175
+ DatabaseService.prototype._return_affected = on_off ? _as_array : _as_number
176
+ }
177
+ DatabaseService._always_return_arrays (cds.env.features.legacy_db_results === false) // TODO: invert default value
178
+ DatabaseService.prototype.isDatabaseService = true
179
+ module.exports = exports = DatabaseService
@@ -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,310 @@
1
+ const cds = require('@sap/cds')
2
+ const LOG = cds.log('db')
3
+
4
+ const use_new_pool = !cds.env.features.use_generic_pool
5
+ const createPool = use_new_pool ? (...args) => new Pool(...args) : require('generic-pool').createPool
6
+
7
+ function ConnectionPool (factory, tenant) {
8
+ let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
9
+ return createPool(bound_factory, factory.options)
10
+ }
11
+
12
+ function TrackedConnectionPool (factory, tenant) {
13
+ const pool = new ConnectionPool (factory, tenant)
14
+ const { acquire, release } = pool
15
+ return Object.assign(pool, {
16
+ async acquire() {
17
+ const connections = (this._trackedConnections ??= new Set())
18
+ try {
19
+ let dbc = await acquire.call(this)
20
+ connections.add((dbc._beginStack = new Error('begin called from:')))
21
+ return dbc
22
+ } catch (err) {
23
+ // TODO: add acquire timeout error check
24
+ err.stack += `\nActive connections:${connections.size}\n${[...connections].map(e => e.stack).join('\n')}`
25
+ throw err
26
+ }
27
+ },
28
+ release(dbc) {
29
+ this._trackedConnections?.delete(dbc._beginStack)
30
+ return release.call(this, dbc)
31
+ },
32
+ })
33
+ }
34
+
35
+ const DEBUG = /\bpool\b/.test(process.env.DEBUG)
36
+ module.exports = DEBUG && !use_new_pool ? TrackedConnectionPool : ConnectionPool
37
+
38
+ // Drop-in replacement for https://github.com/coopernurse/node-pool
39
+ // TODO: fifo: true? relevant for our use case?
40
+
41
+ const { EventEmitter } = require('events')
42
+
43
+ const ResourceState = Object.freeze({
44
+ ALLOCATED: 'allocated',
45
+ IDLE: 'idle',
46
+ INVALID: 'invalid',
47
+ VALIDATION: 'validation'
48
+ })
49
+
50
+ const RequestState = Object.freeze({
51
+ PENDING: 'pending',
52
+ RESOLVED: 'resolved',
53
+ REJECTED: 'rejected'
54
+ })
55
+
56
+ class Request {
57
+ constructor (ttl) {
58
+ this.state = RequestState.PENDING
59
+ this.promise = new Promise((resolve, reject) => {
60
+ this._resolve = value => {
61
+ clearTimeout(this._timeout)
62
+ this.state = RequestState.RESOLVED
63
+ resolve(value)
64
+ }
65
+ this._reject = reason => {
66
+ clearTimeout(this._timeout)
67
+ this.state = RequestState.REJECTED
68
+ reject(reason)
69
+ }
70
+ if (typeof ttl === 'number' && ttl >= 0) {
71
+ const err = new Error(`Pool resource could not be acquired within ${ttl / 1000}s`)
72
+ this._timeout = setTimeout(() => this._reject(err), ttl).unref()
73
+ }
74
+ })
75
+ }
76
+ resolve (v) { if (this.state === RequestState.PENDING) this._resolve(v) }
77
+ reject (e) { if (this.state === RequestState.PENDING) this._reject(e) }
78
+ }
79
+
80
+ class PooledResource {
81
+ constructor(resource) {
82
+ this.obj = resource
83
+ this.creationTime = Date.now()
84
+ this.idle()
85
+ }
86
+
87
+ update(newState) {
88
+ this.state = newState
89
+ }
90
+ idle() {
91
+ this.state = ResourceState.IDLE
92
+ this.lastIdleTime = Date.now()
93
+ }
94
+ }
95
+
96
+ class Pool extends EventEmitter {
97
+
98
+ constructor (factory, options = {}) {
99
+ super()
100
+ this.factory = factory
101
+
102
+ this.options = Object.assign({
103
+ testOnBorrow: false,
104
+ evictionRunIntervalMillis: 100000,
105
+ numTestsPerEvictionRun: 3,
106
+ softIdleTimeoutMillis: -1,
107
+ idleTimeoutMillis: 30000,
108
+ acquireTimeoutMillis: null,
109
+ destroyTimeoutMillis: null,
110
+ fifo: false,
111
+ min: 0,
112
+ max: 10
113
+ }, options)
114
+
115
+ /** @type {boolean} */
116
+ this._draining = false
117
+
118
+ /** @type {Set<PooledResource>} */
119
+ this._available = new Set()
120
+
121
+ /** @type {Map<any, { pooledResource: PooledResource }>} */
122
+ this._loans = new Map()
123
+
124
+ /** @type {Set<PooledResource>} */
125
+ this._all = new Set()
126
+
127
+ /** @type {Set<Promise<void>>} */
128
+ this._creates = new Set()
129
+
130
+ /** @type {Request[]} */
131
+ this._queue = []
132
+
133
+ this.#scheduleEviction()
134
+
135
+ const initial = this.options.min - this.size
136
+ for (let i = 0; i < initial; i++) this.#createResource()
137
+ }
138
+
139
+ async acquire() {
140
+ if (this._draining) throw new Error('Pool is draining and cannot accept new requests')
141
+ const request = new Request(this.options.acquireTimeoutMillis)
142
+ this._queue.push(request)
143
+ this.#dispense()
144
+ return request.promise
145
+ }
146
+
147
+ async release(resource) {
148
+ if (this._draining) {
149
+ LOG.debug('Pool is already draining. Resource cannot be returned to pool')
150
+ return
151
+ }
152
+ const loan = this._loans.get(resource)
153
+ if (!loan) throw new Error('Resource not currently part of this pool')
154
+ this._loans.delete(resource)
155
+ const pooledResource = loan.pooledResource
156
+ pooledResource.idle()
157
+ this._available.add(pooledResource)
158
+ this.#dispense()
159
+ }
160
+
161
+ async destroy(resource) {
162
+ if (this._draining) {
163
+ LOG.debug('Pool is already draining. Resource will be destroyed anyway')
164
+ return
165
+ }
166
+ const loan = this._loans.get(resource)
167
+ if (!loan) throw new Error('Resource not currently part of this pool')
168
+ this._loans.delete(resource)
169
+ const pooledResource = loan.pooledResource
170
+ await this.#destroy(pooledResource)
171
+ this.#dispense()
172
+ }
173
+
174
+ async drain() {
175
+ this._draining = true
176
+ for (const request of this._queue.splice(0)) {
177
+ if (request.state === RequestState.PENDING) request.reject(new Error('Pool is draining and cannot fulfil request'))
178
+ }
179
+ clearTimeout(this._scheduledEviction)
180
+ }
181
+
182
+ async clear() {
183
+ await Promise.allSettled(Array.from(this._creates))
184
+ await Promise.allSettled(Array.from(this._all).map(resource => this.#destroy(resource)))
185
+ }
186
+
187
+ async #createResource() {
188
+ const createPromise = (async () => {
189
+ try {
190
+ const resource = new PooledResource(await this.factory.create())
191
+ this._all.add(resource)
192
+ this._available.add(resource)
193
+ } catch (err) {
194
+ this._queue.shift()?.reject(err)
195
+ }
196
+ })()
197
+ this._creates.add(createPromise)
198
+ createPromise.finally(() => {
199
+ this._creates.delete(createPromise)
200
+ this.#dispense()
201
+ })
202
+ return createPromise
203
+ }
204
+
205
+ async #dispense() {
206
+ const waiting = this._queue.length
207
+ if (waiting === 0) return
208
+ const capacity = this._available.size + this._creates.size
209
+ const shortfall = waiting - capacity
210
+ if (shortfall > 0 && this.size < this.options.max) {
211
+ const needed = Math.min(shortfall, this.options.max - this.size)
212
+ for (let i = 0; i < needed; i++) this.#createResource()
213
+ }
214
+ const dispense = async resource => {
215
+ const request = this._queue.shift()
216
+ if (!request) {
217
+ resource.idle()
218
+ this._available.add(resource)
219
+ return false
220
+ }
221
+ if (request.state !== RequestState.PENDING) {
222
+ this.#dispense()
223
+ return false
224
+ }
225
+ this._loans.set(resource.obj, { pooledResource: resource })
226
+ resource.update(ResourceState.ALLOCATED)
227
+ request.resolve(resource.obj)
228
+ return true
229
+ }
230
+
231
+ const _dispenses = []
232
+ for (let i = 0; i < Math.min(this._available.size, waiting); i++) {
233
+ const resource = this._available.values().next().value
234
+ this._available.delete(resource)
235
+ if (this.options.testOnBorrow) {
236
+ const validationPromise = (async () => {
237
+ resource.update(ResourceState.VALIDATION)
238
+ try {
239
+ const isValid = await this.factory.validate(resource.obj)
240
+ if (isValid) return dispense(resource)
241
+ } catch {/* marked as invalid below */}
242
+ resource.update(ResourceState.INVALID)
243
+ await this.#destroy(resource)
244
+ this.#dispense()
245
+ return false
246
+ })()
247
+ _dispenses.push(validationPromise)
248
+ } else {
249
+ _dispenses.push(dispense(resource))
250
+ }
251
+ }
252
+ await Promise.all(_dispenses)
253
+ }
254
+
255
+ async #destroy(resource) {
256
+ resource.update(ResourceState.INVALID)
257
+ this._all.delete(resource)
258
+ this._available.delete(resource)
259
+ this._loans.delete(resource.obj)
260
+ try {
261
+ const destroyPromise = Promise.resolve(this.factory.destroy(resource.obj))
262
+ const { destroyTimeoutMillis } = this.options
263
+ if (destroyTimeoutMillis && destroyTimeoutMillis > 0) {
264
+ const timeout = new Promise((_, reject) =>
265
+ setTimeout(() => reject(new Error(`Resource destruction timed out after ${destroyTimeoutMillis}ms`)), destroyTimeoutMillis).unref()
266
+ )
267
+ await Promise.race([destroyPromise, timeout])
268
+ } else {
269
+ await destroyPromise
270
+ }
271
+ } catch (e) {
272
+ LOG.error(e)
273
+ /* FIXME: We have to ignore errors here due to a TypeError in hdb */
274
+ /* This was also a problem with the old (generic-pool) implementation */
275
+ /* Root cause in hdb needs to be fixed */
276
+ } finally {
277
+ if (!this._draining && this.size < this.options.min) this.#createResource()
278
+ }
279
+ }
280
+
281
+ #scheduleEviction() {
282
+ const { evictionRunIntervalMillis, numTestsPerEvictionRun, softIdleTimeoutMillis, min, idleTimeoutMillis } = this.options
283
+ if (evictionRunIntervalMillis <= 0) return
284
+ this._scheduledEviction = setTimeout(async () => {
285
+ try {
286
+ const evictionCandidates = Array.from(this._available).slice(0, numTestsPerEvictionRun)
287
+ const destructionPromises = []
288
+ for (const resource of evictionCandidates) {
289
+ const idleTime = Date.now() - resource.lastIdleTime
290
+ const softEvict = softIdleTimeoutMillis > 0 && softIdleTimeoutMillis < idleTime && this._all.size > min
291
+ const hardEvict = idleTimeoutMillis < idleTime
292
+ if (softEvict || hardEvict) {
293
+ if (this._available.delete(resource)) {
294
+ destructionPromises.push(this.#destroy(resource))
295
+ }
296
+ }
297
+ }
298
+ await Promise.all(destructionPromises)
299
+ } finally {
300
+ this.#scheduleEviction()
301
+ }
302
+ }, evictionRunIntervalMillis).unref()
303
+ }
304
+
305
+ get size() { return this._all.size + this._creates.size }
306
+ get available() { return this._available.size }
307
+ get borrowed() { return this._loans.size }
308
+ get pending() { return this._queue.length }
309
+ get tenant() { return this.options.tenant }
310
+ }
@@ -0,0 +1,46 @@
1
+ const cds = require('@sap/cds')
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(/\.(\d*)(Z?)$/, (_, d, z) => `.${parseInt(d) + 1}${z}`) ??
27
+ (new Date(Date.now() + 1)).toISOString())
28
+ }
29
+ }
30
+
31
+ // Set all getters as enumerable
32
+ const iterate = { enumerable: true }
33
+ const getters = (obj) => {
34
+ const prot = obj.prototype
35
+ const patch = {}
36
+ for (const [key, value] of Object.entries(Object.getOwnPropertyDescriptors(prot))) {
37
+ if (!value.get) continue
38
+ patch[key] = iterate
39
+ }
40
+ Object.defineProperties(prot, patch)
41
+ }
42
+ getters(SessionContext)
43
+ getters(TemporalSessionContext)
44
+
45
+ // REVISIT: only set temporal context if required!
46
+ module.exports = TemporalSessionContext
@@ -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
+ }