@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.
- package/CHANGELOG.md +6 -0
- package/node_modules/@cap-js/db-service/CHANGELOG.md +768 -0
- package/node_modules/@cap-js/db-service/LICENSE +201 -0
- package/node_modules/@cap-js/db-service/README.md +26 -0
- package/node_modules/@cap-js/db-service/index.js +19 -0
- package/node_modules/@cap-js/db-service/lib/InsertResults.js +122 -0
- package/node_modules/@cap-js/db-service/lib/SQLService.js +581 -0
- package/node_modules/@cap-js/db-service/lib/common/DatabaseService.js +179 -0
- package/node_modules/@cap-js/db-service/lib/common/factory.d.ts +5 -0
- package/node_modules/@cap-js/db-service/lib/common/generic-pool.js +310 -0
- package/node_modules/@cap-js/db-service/lib/common/session-context.js +46 -0
- package/node_modules/@cap-js/db-service/lib/converters.d.ts +24 -0
- package/node_modules/@cap-js/db-service/lib/cql-functions.js +464 -0
- package/node_modules/@cap-js/db-service/lib/cqn2pql.js +116 -0
- package/node_modules/@cap-js/db-service/lib/cqn2sql.js +1612 -0
- package/node_modules/@cap-js/db-service/lib/cqn4sql.js +2895 -0
- package/node_modules/@cap-js/db-service/lib/deep-queries.js +290 -0
- package/node_modules/@cap-js/db-service/lib/fill-in-keys.js +75 -0
- package/node_modules/@cap-js/db-service/lib/infer/cqn.d.ts +42 -0
- package/node_modules/@cap-js/db-service/lib/infer/index.js +1290 -0
- package/node_modules/@cap-js/db-service/lib/infer/join-tree.js +278 -0
- package/node_modules/@cap-js/db-service/lib/infer/pseudos.js +24 -0
- package/node_modules/@cap-js/db-service/lib/search.js +220 -0
- package/node_modules/@cap-js/db-service/lib/utils.js +177 -0
- package/node_modules/@cap-js/db-service/package.json +36 -0
- package/node_modules/@cap-js/sqlite/CHANGELOG.md +404 -0
- package/node_modules/@cap-js/sqlite/LICENSE +201 -0
- package/node_modules/@cap-js/sqlite/README.md +34 -0
- package/node_modules/@cap-js/sqlite/cds-plugin.js +0 -0
- package/node_modules/@cap-js/sqlite/index.js +2 -0
- package/node_modules/@cap-js/sqlite/lib/SQLiteService.js +324 -0
- package/node_modules/@cap-js/sqlite/lib/cql-functions.js +160 -0
- package/node_modules/@cap-js/sqlite/lib/node-sqlite.js +36 -0
- package/node_modules/@cap-js/sqlite/lib/session.json +3 -0
- package/node_modules/@cap-js/sqlite/lib/sql.js.js +99 -0
- package/node_modules/@cap-js/sqlite/package.json +68 -0
- package/node_modules/iconv-lite/encodings/sbcs-data.js +2 -0
- package/node_modules/iconv-lite/encodings/utf32.js +10 -3
- package/node_modules/iconv-lite/package.json +2 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +2 -0
- 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,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
|
+
}
|