@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,188 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A class representing a Node in the join tree.
|
|
5
|
+
*
|
|
6
|
+
* @property {$refLink} - A reference link to this node.
|
|
7
|
+
* @property {parent} - The parent Node of this node.
|
|
8
|
+
* @property {where} - An optional condition to be applied to this node.
|
|
9
|
+
* @property {children} - A Map of children nodes belonging to this node.
|
|
10
|
+
*/
|
|
11
|
+
class Node {
|
|
12
|
+
constructor($refLink, parent, where = null) {
|
|
13
|
+
this.$refLink = $refLink
|
|
14
|
+
this.parent = parent
|
|
15
|
+
this.where = where
|
|
16
|
+
this.children = new Map()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A class representing the root of the join tree.
|
|
22
|
+
*
|
|
23
|
+
* @property {queryArtifact} - The artifact used to make the query.
|
|
24
|
+
* @property {alias} - The alias of the artifact.
|
|
25
|
+
* @property {parent} - The parent Node of this root, null for the root Node.
|
|
26
|
+
* @property {children} - A Map of children nodes belonging to this root.
|
|
27
|
+
*/
|
|
28
|
+
class Root {
|
|
29
|
+
constructor(querySource) {
|
|
30
|
+
const [alias, queryArtifact] = querySource
|
|
31
|
+
this.queryArtifact = queryArtifact
|
|
32
|
+
this.alias = alias
|
|
33
|
+
this.parent = null
|
|
34
|
+
this.children = new Map()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A class representing a Join Tree.
|
|
40
|
+
*
|
|
41
|
+
* @property {_roots} - A Map of root nodes.
|
|
42
|
+
* @property {isInitial} - A boolean indicating if the join tree is in its initial state.
|
|
43
|
+
* @property {_queryAliases} - A Map of query aliases, which is used during the association to join translation.
|
|
44
|
+
*/
|
|
45
|
+
class JoinTree {
|
|
46
|
+
constructor(sources) {
|
|
47
|
+
this._roots = new Map()
|
|
48
|
+
this.isInitial = true
|
|
49
|
+
/**
|
|
50
|
+
* A map that holds query aliases which are used during the
|
|
51
|
+
* association to join translation. It is also considered during the
|
|
52
|
+
* where exists expansion.
|
|
53
|
+
*
|
|
54
|
+
* The table aliases are treated case insensitive. The index of each
|
|
55
|
+
* table alias entry, is the capitalized version of the alias.
|
|
56
|
+
*/
|
|
57
|
+
this._queryAliases = new Map()
|
|
58
|
+
Object.entries(sources).forEach(entry => {
|
|
59
|
+
const alias = this.addNextAvailableTableAlias(entry[0])
|
|
60
|
+
this._roots.set(alias, new Root(entry))
|
|
61
|
+
if (entry[1].sources)
|
|
62
|
+
// respect outer aliases
|
|
63
|
+
this.addAliasesOfSubqueryInFrom(entry[1].sources)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recursively adds aliases of subqueries from a given query source to the alias map.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} sources - The sources of the inferred subquery in a FROM clause.
|
|
71
|
+
*/
|
|
72
|
+
addAliasesOfSubqueryInFrom(sources) {
|
|
73
|
+
Object.entries(sources).forEach(e => {
|
|
74
|
+
this.addNextAvailableTableAlias(e[0])
|
|
75
|
+
if (e[1].sources)
|
|
76
|
+
// recurse
|
|
77
|
+
this.addAliasesOfSubqueryInFrom(e[1].sources)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculates and adds the next available table alias to the alias map.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} alias - The original alias name.
|
|
85
|
+
* @returns {string} - The next unambiguous table alias.
|
|
86
|
+
*/
|
|
87
|
+
addNextAvailableTableAlias(alias, outerQueries) {
|
|
88
|
+
const upperAlias = alias.toUpperCase()
|
|
89
|
+
if (this._queryAliases.get(upperAlias) || outerQueries?.some(outer => outerHasAlias(outer))) {
|
|
90
|
+
let j = 2
|
|
91
|
+
while (this._queryAliases.get(upperAlias + j) || outerQueries?.some(outer => outerHasAlias(outer, j))) j += 1
|
|
92
|
+
alias += j
|
|
93
|
+
}
|
|
94
|
+
this._queryAliases.set(alias.toUpperCase(), alias)
|
|
95
|
+
return alias
|
|
96
|
+
|
|
97
|
+
function outerHasAlias(outer, number) {
|
|
98
|
+
return outer.joinTree._queryAliases.get(number ? upperAlias + number : upperAlias)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merges a column into the join tree.
|
|
104
|
+
*
|
|
105
|
+
* It begins by inferring the source of the given column, which is the table alias where the column is resolvable.
|
|
106
|
+
* Each step during this process represents a node in the join tree. If a node already exists in the tree, the current step is replaced by the already merged node.
|
|
107
|
+
* For each step, it checks whether it has been seen before. If so, it resets the $refLink to point to the already merged $refLink.
|
|
108
|
+
* If not, it creates a new Node and ensures proper aliasing and foreign key access.
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} col - The column object to be merged into the existing join tree. This object should have the properties $refLinks and ref.
|
|
111
|
+
* @returns {boolean} - Always returns true, indicating the column has been successfully merged into the join tree.
|
|
112
|
+
*/
|
|
113
|
+
mergeColumn(col) {
|
|
114
|
+
if (this.isInitial) this.isInitial = false
|
|
115
|
+
const head = col.$refLinks[0]
|
|
116
|
+
let node = this._roots.get(head.alias)
|
|
117
|
+
let i = 0
|
|
118
|
+
if (!node) {
|
|
119
|
+
this._roots.forEach(r => {
|
|
120
|
+
// find the correct query source
|
|
121
|
+
if (
|
|
122
|
+
r.queryArtifact === head.target ||
|
|
123
|
+
r.queryArtifact === head.target.target /** might as well be a query for order by */
|
|
124
|
+
)
|
|
125
|
+
node = r
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
i += 1 // skip first step which is table alias
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (i < col.ref.length) {
|
|
132
|
+
const step = col.ref[i]
|
|
133
|
+
const { where } = step
|
|
134
|
+
const id = where ? step.id + JSON.stringify(where) : step
|
|
135
|
+
const next = node.children.get(id)
|
|
136
|
+
const $refLink = col.$refLinks[i]
|
|
137
|
+
if (next) {
|
|
138
|
+
// step already seen before
|
|
139
|
+
node = next
|
|
140
|
+
col.$refLinks[i] = node.$refLink // re-set $refLink to point to already merged $refLink
|
|
141
|
+
} else {
|
|
142
|
+
if (col.expand && !col.ref[i + 1]) {
|
|
143
|
+
node.$refLink.onlyForeignKeyAccess = false
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
const child = new Node($refLink, node, where)
|
|
147
|
+
if (child.$refLink.definition.isAssociation) {
|
|
148
|
+
if (child.where) {
|
|
149
|
+
// always join relevant
|
|
150
|
+
child.$refLink.onlyForeignKeyAccess = false
|
|
151
|
+
} else {
|
|
152
|
+
child.$refLink.onlyForeignKeyAccess = true
|
|
153
|
+
}
|
|
154
|
+
child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const foreignKeys = node.$refLink?.definition.foreignKeys
|
|
158
|
+
if (node.$refLink && (!foreignKeys || !(child.$refLink.alias in foreignKeys)))
|
|
159
|
+
// foreign key access
|
|
160
|
+
node.$refLink.onlyForeignKeyAccess = false
|
|
161
|
+
|
|
162
|
+
node.children.set(id, child)
|
|
163
|
+
node = child
|
|
164
|
+
}
|
|
165
|
+
i += 1
|
|
166
|
+
}
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Performs a depth-first search for the next association in the children of the given node which does not only access foreign keys.
|
|
172
|
+
*
|
|
173
|
+
* @param {Node} node - The node from which to search for the next association.
|
|
174
|
+
* @returns {Node|null} - Returns the node which represents an association or null if none was found.
|
|
175
|
+
*/
|
|
176
|
+
findNextAssoc(node) {
|
|
177
|
+
if (node.$refLink.definition.isAssociation && !node.$refLink.onlyForeignKeyAccess) return node
|
|
178
|
+
// recurse on each child node
|
|
179
|
+
for (const child of node.children.values()) {
|
|
180
|
+
const grandChild = this.findNextAssoc(child)
|
|
181
|
+
if (grandChild) return grandChild
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = JoinTree
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// REVISIT: we should always return cds.linked elements
|
|
4
|
+
// > e.g. cds.linked({definitions:{pseudos}})
|
|
5
|
+
const pseudos = {
|
|
6
|
+
elements: {
|
|
7
|
+
$user: {
|
|
8
|
+
elements: {
|
|
9
|
+
id: { type: 'cds.String' },
|
|
10
|
+
locale: { type: 'cds.String' }, // deprecated
|
|
11
|
+
tenant: { type: 'cds.String' }, // deprecated
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
$now: { type: 'cds.Timestamp' },
|
|
15
|
+
$at: { type: 'cds.Timestamp' },
|
|
16
|
+
$from: { type: 'cds.Timestamp' },
|
|
17
|
+
$to: { type: 'cds.Timestamp' },
|
|
18
|
+
$locale: { type: 'cds.String' },
|
|
19
|
+
$tenant: { type: 'cds.String' },
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { pseudos }
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cap-js/db-service",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CDS base database service",
|
|
5
|
+
"homepage": "https://cap.cloud.sap/",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"CAP",
|
|
8
|
+
"CDS"
|
|
9
|
+
],
|
|
10
|
+
"author": "SAP SE (https://www.sap.com)",
|
|
11
|
+
"main": "index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=16",
|
|
18
|
+
"npm": ">=8"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prettier": "npx prettier --write .",
|
|
22
|
+
"test": "npx jest --silent",
|
|
23
|
+
"lint": "npx eslint . && npx prettier --check . "
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@sap/cds": ">=7"
|
|
28
|
+
},
|
|
29
|
+
"license": "SEE LICENSE"
|
|
30
|
+
}
|