@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.
@@ -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 }