@cap-js/sqlite 0.1.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,167 @@
1
+ 'use strict';
2
+
3
+ class Node {
4
+ constructor($refLink, parent, where = null) {
5
+ this.$refLink = $refLink;
6
+ this.parent = parent;
7
+ this.where = where;
8
+ this.children = new Map();
9
+ }
10
+ }
11
+
12
+ class Root {
13
+ constructor(querySource) {
14
+ const [ alias, queryArtifact ] = querySource;
15
+ this.queryArtifact = queryArtifact;
16
+ this.alias = alias;
17
+ this.parent = null;
18
+ this.children = new Map();
19
+ }
20
+ }
21
+
22
+ class JoinTree {
23
+ constructor(sources) {
24
+ this._roots = new Map();
25
+ this.isInitial = true;
26
+ /**
27
+ * A map that holds query aliases which are used during the
28
+ * association to join translation. It is also considered during the
29
+ * where exists expansion.
30
+ *
31
+ * The table aliases are treated case insensitive. The index of each
32
+ * table alias entry, is the capitalized version of the alias.
33
+ */
34
+ this._queryAliases = new Map();
35
+ Object.entries(sources).forEach( (entry) => {
36
+ const alias = this.addNextAvailableTableAlias(entry[0]);
37
+ this._roots.set(alias, new Root(entry));
38
+ if (entry[1].sources) // respect outer aliases
39
+ this.addAliasesOfSubqueryInFrom(entry[1].sources);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Recursively drills into subqueries in a query source and
45
+ * adds the aliases of those subqueries to the alias map.
46
+ *
47
+ * @param {object} sources of inferred subquery in from
48
+ */
49
+ addAliasesOfSubqueryInFrom(sources) {
50
+ Object.entries(sources).forEach( (e) => {
51
+ this.addNextAvailableTableAlias(e[0]);
52
+ if (e[1].sources) // recurse
53
+ this.addAliasesOfSubqueryInFrom(e[1].sources);
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Calculate the next available table alias and add it to
59
+ * the alias map. Returns the alias in original case, appended
60
+ * by an integer if necessary.
61
+ *
62
+ * @param {string} alias
63
+ * @returns the next un-ambigous table alias
64
+ */
65
+ addNextAvailableTableAlias(alias) {
66
+ const upperAlias = alias.toUpperCase();
67
+ if (this._queryAliases.get(upperAlias)) {
68
+ let j = 2;
69
+ while (this._queryAliases.get(upperAlias + j))
70
+ j += 1;
71
+ alias += j;
72
+ }
73
+ this._queryAliases.set(alias.toUpperCase(), alias);
74
+ return alias;
75
+ }
76
+
77
+ /**
78
+ * Merge a column into the join tree.
79
+ *
80
+ * First, the source of the column is inferred, i.e. the table alias in which the `col` is resolvable.
81
+ * The table alias is the root of this column. Each of the following steps represents a `node` in the join tree.
82
+ * If a `node` is already present in the tree, the current step is replaced by the already merged `node`.
83
+ * This makes sure all references which follow the same path will have the same table alias in the end.
84
+ *
85
+ * @param {object} col the column which shall be merged into the existing join tree
86
+ * @returns {true}
87
+ */
88
+ mergeColumn(col) {
89
+ if (this.isInitial)
90
+ this.isInitial = false;
91
+ const head = col.$refLinks[0];
92
+ let node = this._roots.get(head.alias);
93
+ let i = 0;
94
+ if (!node) {
95
+ this._roots.forEach( (r) => { // find the correct query source
96
+ if (
97
+ r.queryArtifact === head.target ||
98
+ r.queryArtifact === head.target.target /** might as well be a query for order by */
99
+ )
100
+ node = r;
101
+ });
102
+ }
103
+ else {
104
+ i += 1; // skip first step which is table alias
105
+ }
106
+
107
+ while (i < col.ref.length) {
108
+ const step = col.ref[i];
109
+ const { where } = step;
110
+ const id = where ? step.id + JSON.stringify(where) : step;
111
+ const next = node.children.get(id);
112
+ const $refLink = col.$refLinks[i];
113
+ if (next) { // step already seen before
114
+ node = next;
115
+ col.$refLinks[i] = node.$refLink; // re-set $refLink to point to already merged $refLink
116
+ }
117
+ else {
118
+ if (col.expand && !col.ref[i + 1]) {
119
+ node.$refLink.onlyForeignKeyAccess = false;
120
+ return true;
121
+ }
122
+ const child = new Node($refLink, node, where);
123
+ if (child.$refLink.definition.isAssociation) {
124
+ if (child.where) { // always join relevant
125
+ child.$refLink.onlyForeignKeyAccess = false;
126
+ }
127
+ else {
128
+ child.$refLink.onlyForeignKeyAccess = true;
129
+ }
130
+ child.$refLink.alias = this.addNextAvailableTableAlias($refLink.alias);
131
+ }
132
+
133
+ const foreignKeys = node.$refLink?.definition.foreignKeys;
134
+ if (node.$refLink && (!foreignKeys || !(child.$refLink.alias in foreignKeys)) ) // foreign key access
135
+ node.$refLink.onlyForeignKeyAccess = false;
136
+
137
+ node.children.set(id, child);
138
+ node = child;
139
+ }
140
+ i += 1;
141
+ }
142
+
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Search depth-first for the next association in the children's of the given `node` which
148
+ * does not only access foreign keys.
149
+ *
150
+ * @param {Node} node the node from which to search for the next association
151
+ * @returns {Node|null} the node which represents an association. Or null if none was found.
152
+ */
153
+ findNextAssoc(node) {
154
+ if (node.$refLink.definition.isAssociation && !node.$refLink.onlyForeignKeyAccess)
155
+ return node;
156
+ // recurse on each child node
157
+ for (const child of node.children.values()) {
158
+ const grandChild = this.findNextAssoc(child);
159
+ if (grandChild)
160
+ return grandChild;
161
+ }
162
+
163
+ return null;
164
+ }
165
+ }
166
+
167
+ 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,52 @@
1
+ {
2
+ "name": "@cap-js/sqlite",
3
+ "version": "0.1.0",
4
+ "description": "CDS database service for SQLite",
5
+ "homepage": "https://cap.cloud.sap/",
6
+ "keywords": [
7
+ "CAP",
8
+ "CDS",
9
+ "SQLite"
10
+ ],
11
+ "author": "SAP SE (https://www.sap.com)",
12
+ "license": "SEE LICENSE",
13
+ "homepage": "https://cap.cloud.sap/",
14
+ "main": "index.js",
15
+ "files": [
16
+ "cds.js",
17
+ "lib/db/DatabaseService.js",
18
+ "lib/db/sqlite",
19
+ "lib/db/sql",
20
+ "lib/ql",
21
+ "LICENSE",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=14",
26
+ "npm": ">=8"
27
+ },
28
+ "scripts": {
29
+ "//pg:up": "use only for dev time, CI is handled separately",
30
+ "pg:up": "docker-compose -f etc/pg-stack.yml up -d",
31
+ "prettier": "npx prettier --write .",
32
+ "test": "jest --silent",
33
+ "lint": "npx eslint . && npx prettier --check ."
34
+ },
35
+ "dependencies": {
36
+ "better-sqlite3": "^8"
37
+ },
38
+ "peerDependencies": {
39
+ "@sap/cds": "*"
40
+ },
41
+ "devDependencies": {
42
+ "@capire/sflight": "sap-samples/cap-sflight",
43
+ "@cap-js/sqlite": ".",
44
+ "axios": ">=1.3",
45
+ "chai": "^4.3.7",
46
+ "chai-as-promised": "^7.1.1",
47
+ "chai-subset": "^1.6.0",
48
+ "express": "^4",
49
+ "pg": "^8",
50
+ "jest": "^29"
51
+ }
52
+ }