@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,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
+ }