@cap-js/db-service 1.0.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 +9 -0
- package/LICENSE +201 -0
- package/README.md +5 -0
- package/index.js +4 -0
- package/lib/InsertResults.js +95 -0
- package/lib/SQLService.js +288 -0
- package/lib/common/DatabaseService.js +144 -0
- package/lib/cql-functions.js +148 -0
- package/lib/cqn2sql.js +605 -0
- package/lib/cqn4sql.js +1846 -0
- package/lib/deep-queries.js +244 -0
- package/lib/fill-in-keys.js +65 -0
- package/lib/infer/index.js +922 -0
- package/lib/infer/join-tree.js +188 -0
- package/lib/infer/pseudos.js +23 -0
- package/package.json +30 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
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
|
|
10
|
+
|
|
11
|
+
class DatabaseService extends cds.Service {
|
|
12
|
+
/**
|
|
13
|
+
* Return a pool factory + options property as expected by
|
|
14
|
+
* https://github.com/coopernurse/node-pool#createpool.
|
|
15
|
+
*/
|
|
16
|
+
get factory() {
|
|
17
|
+
throw '2b overriden in subclass'
|
|
18
|
+
}
|
|
19
|
+
pools = { _factory: this.factory }
|
|
20
|
+
|
|
21
|
+
get isMultitenant() {
|
|
22
|
+
return 'multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set one or more session context variables like so:
|
|
27
|
+
* ```js
|
|
28
|
+
* const tx = cds.db.tx()
|
|
29
|
+
* tx.set({
|
|
30
|
+
* '$user.name': 'Alice',
|
|
31
|
+
* '$user.role': 'admin'
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
// eslint-disable-next-line no-unused-vars
|
|
36
|
+
set(variables) {
|
|
37
|
+
throw '2b overridden by subclass'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
infer(q, m = this.model) {
|
|
41
|
+
return infer(q, m)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async begin() {
|
|
45
|
+
const ctx = this.context
|
|
46
|
+
if (!ctx) return this.tx().begin()
|
|
47
|
+
const tenant = this.isMultitenant && ctx.tenant
|
|
48
|
+
const pool = (this.pools[tenant] ??= new Pool(this.pools._factory, tenant))
|
|
49
|
+
const connections = pool._trackedConnections
|
|
50
|
+
let dbc
|
|
51
|
+
try {
|
|
52
|
+
dbc = this.dbc = await pool.acquire()
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// TODO: add acquire timeout error check
|
|
55
|
+
err.stack += `\nActive connections:${connections.length}\n${connections.map(c => c._beginStack.stack).join('\n')}`
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
this._beginStack = new Error('begin called from:')
|
|
59
|
+
connections.push(this)
|
|
60
|
+
this._release = async dbc => {
|
|
61
|
+
await pool.release(dbc)
|
|
62
|
+
connections.splice(connections.indexOf(this), 1)
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// Setting session context variables
|
|
66
|
+
await this.set({
|
|
67
|
+
get '$user.id'() {
|
|
68
|
+
return _set(this, '$user.id', ctx.user?.id || 'anonymous')
|
|
69
|
+
},
|
|
70
|
+
get '$user.locale'() {
|
|
71
|
+
return _set(this, '$user.locale', ctx.locale || cds.env.i18n.default_language)
|
|
72
|
+
},
|
|
73
|
+
get '$valid.from'() {
|
|
74
|
+
return _set(this, '$valid.from', ctx._?.['VALID-FROM'] ?? ctx._?.['VALID-AT'] ?? '1970-01-01T00:00:00.000Z')
|
|
75
|
+
},
|
|
76
|
+
get '$valid.to'() {
|
|
77
|
+
return _set(
|
|
78
|
+
this,
|
|
79
|
+
'$valid.to',
|
|
80
|
+
ctx._?.['VALID-TO'] ?? _validTo4(ctx._?.['VALID-AT']) ?? '9999-11-11T22:22:22.000Z',
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
// Run BEGIN
|
|
85
|
+
await this.send('BEGIN')
|
|
86
|
+
} catch (e) {
|
|
87
|
+
this._release(dbc)
|
|
88
|
+
throw e
|
|
89
|
+
}
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async commit() {
|
|
94
|
+
const dbc = this.dbc
|
|
95
|
+
if (!dbc) return
|
|
96
|
+
await this.send('COMMIT')
|
|
97
|
+
this._release(dbc) // only release on successful commit as otherwise released on rollback
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async rollback() {
|
|
101
|
+
const dbc = this.dbc
|
|
102
|
+
if (!dbc) return
|
|
103
|
+
try {
|
|
104
|
+
await this.send('ROLLBACK')
|
|
105
|
+
} finally {
|
|
106
|
+
this._release(dbc)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// REVISIT: should happen automatically after a configurable time
|
|
111
|
+
async disconnect(tenant) {
|
|
112
|
+
const pool = this.pools[tenant]
|
|
113
|
+
if (pool) delete this.pools[tenant]
|
|
114
|
+
else return
|
|
115
|
+
await pool.drain()
|
|
116
|
+
await pool.clear()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
run(query, data, ...etc) {
|
|
120
|
+
// Allow db.run('...',1,2,3,4)
|
|
121
|
+
if (data !== undefined && typeof query === 'string' && typeof data !== 'object') data = [data, ...etc]
|
|
122
|
+
return super.run(query, data)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
url4(/*tenant*/) {
|
|
126
|
+
// eslint-disable-line no-unused-vars
|
|
127
|
+
let { url } = this.options?.credentials || this.options || {}
|
|
128
|
+
return url
|
|
129
|
+
}
|
|
130
|
+
getDbUrl(tenant) {
|
|
131
|
+
return this.url4(tenant)
|
|
132
|
+
} // REVISIT: Remove after cds v6.7
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const _set = (context, variable, value) => {
|
|
136
|
+
Object.defineProperty(context, variable, { value, configurable: true })
|
|
137
|
+
return value
|
|
138
|
+
}
|
|
139
|
+
const _validTo4 = validAt => {
|
|
140
|
+
return validAt?.replace(/(\dZ?)$/, d => parseInt(d[0]) + 1 + d[1] || '')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
DatabaseService.prototype.isDatabaseService = true
|
|
144
|
+
module.exports = DatabaseService
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const StandardFunctions = {
|
|
2
|
+
// OData: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_CanonicalFunctions
|
|
3
|
+
|
|
4
|
+
// String and Collection Functions
|
|
5
|
+
// length : (x) => `length(${x})`,
|
|
6
|
+
search: function (ref, arg) {
|
|
7
|
+
if (!('val' in arg)) throw `SQLite only supports single value arguments for $search`
|
|
8
|
+
const refs = ref.list || [ref],
|
|
9
|
+
{ toString } = ref
|
|
10
|
+
return '(' + refs.map(ref2 => this.contains(this.tolower(toString(ref2)), this.tolower(arg))).join(' or ') + ')'
|
|
11
|
+
},
|
|
12
|
+
concat: (...args) => args.join('||'),
|
|
13
|
+
contains: (...args) => `ifnull(instr(${args}),0)`,
|
|
14
|
+
count: x => `count(${x || '*'})`,
|
|
15
|
+
countdistinct: x => `count(distinct ${x || '*'})`,
|
|
16
|
+
indexof: (x, y) => `instr(${x},${y}) - 1`, // sqlite instr is 1 indexed
|
|
17
|
+
startswith: (x, y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed
|
|
18
|
+
// takes the end of the string of the size of the target and compares it with the target
|
|
19
|
+
endswith: (x, y) => `substr(${x}, length(${x}) + 1 - length(${y})) = ${y}`,
|
|
20
|
+
substring: (x, y, z) =>
|
|
21
|
+
z
|
|
22
|
+
? `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end, ${z} )`
|
|
23
|
+
: `substr( ${x}, case when ${y} < 0 then length(${x}) + ${y} + 1 else ${y} + 1 end )`,
|
|
24
|
+
|
|
25
|
+
// String Functions
|
|
26
|
+
matchesPattern: (x, y) => `${x} regexp ${y})`,
|
|
27
|
+
tolower: x => `lower(${x})`,
|
|
28
|
+
toupper: x => `upper(${x})`,
|
|
29
|
+
// trim : (x) => `trim(${x})`,
|
|
30
|
+
|
|
31
|
+
// Arithmetic Functions
|
|
32
|
+
ceiling: x => `ceil(${x})`,
|
|
33
|
+
// floor : (x) => `floor(${x})`,
|
|
34
|
+
// round : (x) => `round(${x})`,
|
|
35
|
+
|
|
36
|
+
// Date and Time Functions
|
|
37
|
+
year: x => `cast( strftime('%Y',${x}) as Integer )`,
|
|
38
|
+
month: x => `cast( strftime('%m',${x}) as Integer )`,
|
|
39
|
+
day: x => `cast( strftime('%d',${x}) as Integer )`,
|
|
40
|
+
hour: x => `cast( strftime('%H',${x}) as Integer )`,
|
|
41
|
+
minute: x => `cast( strftime('%M',${x}) as Integer )`,
|
|
42
|
+
second: x => `cast( strftime('%S',${x}) as Integer )`,
|
|
43
|
+
|
|
44
|
+
fractionalseconds: x => `cast( strftime('%f0000',${x}) as Integer )`,
|
|
45
|
+
|
|
46
|
+
maxdatetime: () => '9999-12-31 23:59:59.999',
|
|
47
|
+
mindatetime: () => '0001-01-01 00:00:00.000',
|
|
48
|
+
|
|
49
|
+
// odata spec defines the date time offset type as a normal ISO time stamp
|
|
50
|
+
// Where the timezone can either be 'Z' (for UTC) or [+|-]xx:xx for the time offset
|
|
51
|
+
// sqlite understands this so by splitting the timezone from the actual date
|
|
52
|
+
// prefixing it with 1970 it allows sqlite to give back the number of seconds
|
|
53
|
+
// which can be divided by 60 back to minutes
|
|
54
|
+
totaloffsetminutes: x => `case
|
|
55
|
+
when substr(${x}, length(${x})) = 'z' then 0
|
|
56
|
+
else strftime('%s', '1970-01-01T00:00:00' || substr(${x}, length(${x}) - 5)) / 60
|
|
57
|
+
end`,
|
|
58
|
+
|
|
59
|
+
// odata spec defines the value format for totalseconds as a duration like: P12DT23H59M59.999999999999S
|
|
60
|
+
// P -> duration indicator
|
|
61
|
+
// D -> days, T -> Time seperator, H -> hours, M -> minutes, S -> fractional seconds
|
|
62
|
+
// By splitting the DT and calculating the seconds of the time separate from the day
|
|
63
|
+
// it possible to determine the full amount of seconds by adding them together as fractionals and multiplying
|
|
64
|
+
// the number of seconds in a day
|
|
65
|
+
// As sqlite is most accurate with juliandays it is better to do then then using actual second function
|
|
66
|
+
// while the odata specification states that the seconds has to be fractional which only julianday allows
|
|
67
|
+
totalseconds: x => `(
|
|
68
|
+
(
|
|
69
|
+
(
|
|
70
|
+
cast(substr(${x},2,instr(${x},'DT') - 2) as Integer)
|
|
71
|
+
) + (
|
|
72
|
+
julianday(
|
|
73
|
+
'-4713-11-25T' ||
|
|
74
|
+
replace(
|
|
75
|
+
replace(
|
|
76
|
+
replace(
|
|
77
|
+
substr(${x},instr(${x},'DT') + 2),
|
|
78
|
+
'H',':'
|
|
79
|
+
),'M',':'
|
|
80
|
+
),'S','Z'
|
|
81
|
+
)
|
|
82
|
+
) - 0.5
|
|
83
|
+
)
|
|
84
|
+
) * 86400
|
|
85
|
+
)`,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const HANAFunctions = {
|
|
89
|
+
// https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/f12b86a6284c4aeeb449e57eb5dd3ebd.html
|
|
90
|
+
|
|
91
|
+
// Time functions
|
|
92
|
+
nano100_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 864000000000`,
|
|
93
|
+
seconds_between: (x, y) => `(julianday(${y}) - julianday(${x})) * 86400`,
|
|
94
|
+
// Calculates the difference in full days using julian day
|
|
95
|
+
// Using the exact time of the day to determine whether 24 hours have passed or not to add the final day
|
|
96
|
+
// When just comparing the julianday values with each other there are leap seconds included
|
|
97
|
+
// Which on the day resolution are included as the individual days therefor ignoring them to match HANA
|
|
98
|
+
days_between: (x, y) => `(
|
|
99
|
+
cast ( julianday(${y}) as Integer ) - cast ( julianday(${x}) as Integer )
|
|
100
|
+
) + (
|
|
101
|
+
case
|
|
102
|
+
when ( julianday(${y}) < julianday(${x}) ) then
|
|
103
|
+
(cast( strftime('%H%M%S%f0000', ${y}) as Integer ) < cast( strftime('%H%M%S%f0000', ${x}) as Integer ))
|
|
104
|
+
else
|
|
105
|
+
(cast( strftime('%H%M%S%f0000', ${y}) as Integer ) > cast( strftime('%H%M%S%f0000', ${x}) as Integer )) * -1
|
|
106
|
+
end
|
|
107
|
+
)`,
|
|
108
|
+
|
|
109
|
+
// (y1 - y0) * 12 + (m1 - m0) + (t1 < t0) * -1
|
|
110
|
+
/* '%d%H%M%S%f' returns as a number like which results in an equal check to:
|
|
111
|
+
(
|
|
112
|
+
d1 < d0 ||
|
|
113
|
+
(d1 = d0 && h1 < h0) ||
|
|
114
|
+
(d1 = d0 && h1 = h0 && m1 < m0) ||
|
|
115
|
+
(d1 = d0 && h1 = h0 && m1 = m0 && s1 < s0) ||
|
|
116
|
+
(d1 = d0 && h1 = h0 && m1 = m0 && s1 = s0 && ms1 < ms0)
|
|
117
|
+
)
|
|
118
|
+
Which will remove the current month if the time of the month is below the time of the month of the start date
|
|
119
|
+
It should not matter that the number of days in the month is different as for a month to have passed
|
|
120
|
+
the time of the month would have to be higher then the time of the month of the start date
|
|
121
|
+
|
|
122
|
+
Also check whether the result will be positive or negative to make sure to not subtract an extra month
|
|
123
|
+
*/
|
|
124
|
+
months_between: (x, y) => `
|
|
125
|
+
(
|
|
126
|
+
(
|
|
127
|
+
( cast( strftime('%Y', ${y}) as Integer ) - cast( strftime('%Y', ${x}) as Integer ) ) * 12
|
|
128
|
+
) + (
|
|
129
|
+
cast( strftime('%m', ${y}) as Integer ) - cast( strftime('%m', ${x}) as Integer )
|
|
130
|
+
) + (
|
|
131
|
+
(
|
|
132
|
+
case
|
|
133
|
+
when ( cast( strftime('%Y%m', ${y}) as Integer ) < cast( strftime('%Y%m', ${x}) as Integer ) ) then
|
|
134
|
+
(cast( strftime('%d%H%M%S%f0000', ${y}) as Integer ) > cast( strftime('%d%H%M%S%f0000', ${x}) as Integer ))
|
|
135
|
+
else
|
|
136
|
+
(cast( strftime('%d%H%M%S%f0000', ${y}) as Integer ) < cast( strftime('%d%H%M%S%f0000', ${x}) as Integer )) * -1
|
|
137
|
+
end
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
)`,
|
|
141
|
+
years_between(x, y) {
|
|
142
|
+
return `floor(${this.months_between(x, y)} / 12)`
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each]
|
|
147
|
+
|
|
148
|
+
module.exports = { ...StandardFunctions, ...HANAFunctions }
|