@cap-js/sqlite 0.1.0 → 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 CHANGED
@@ -4,6 +4,15 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+
8
+ ## Version 1.0.0 - 2023-06-23
9
+
10
+ - First official release
11
+
12
+ ## Version 0.2.0 - 2023-05-03
13
+
14
+ - Continuous improvements
15
+
7
16
  ## Version 0.1.0 - 2023-04-04
8
17
 
9
18
  - Initial release
package/README.md CHANGED
@@ -2,188 +2,5 @@
2
2
 
3
3
  Welcome to the new SQLite database service for [SAP Cloud Application Programming Model](https://cap.cloud.sap) Node.js, based on new, streamlined database architecture and [*better-sqlite* driver](https://www.npmjs.com/package/better-sqlite3) .
4
4
 
5
- > ⚠️ _**WARNING:** This package is in a beta state._ ⚠️
6
- >
5
+ Find documentation at https://cap.cloud.sap/docs/guides/databases-sqlite.
7
6
 
8
-
9
- ## Installing
10
-
11
- ### Add `@cap-js/sqlite` as package dependency
12
-
13
- As SQLite is commonly used during development or tests only, add it as a dev dependency like so:
14
-
15
- ```sh
16
- npm add @cap-js/sqlite -D
17
- ```
18
-
19
- > **Note:** There's no need to add the [*better-sqlite*](https://www.npmjs.com/package/better-sqlite3) driver, and it's also not recommended anymore to do so, as that is done transiently.
20
-
21
-
22
- ### Remove `sqlite3` package dependency
23
-
24
- If migrating an existing project you may want to remove the old [*sqlite3* driver](https://www.npmjs.com/package/sqlite3) :
25
-
26
- ```sh
27
- npm rm sqlite3
28
- ```
29
-
30
-
31
- ## Usage
32
-
33
-
34
- ### Via `@cap-js/sqlite` in package dependencies
35
-
36
- Adding `@cap-js/sqlite` to your package dependencies as described above is all you need to do. We'll automatically use the new service if that dependency is there in
37
- `dependencies` or in `devDependencies`. Your configuration can stay as is, e.g.:
38
-
39
- ```jsonc
40
- { ...,
41
- "cds": {
42
- "requires": {
43
- "db": "sql"
44
- }
45
- }
46
- }
47
- ```
48
-
49
- > **Note:** This automatically also enables the new, lean draft implementation that's required for the new database services, i.e., `cds.fiori.lean_draft` will be automatically set to `true`.
50
-
51
-
52
- ### Via `better-sqlite` config profile
53
-
54
- Alternatively, if you don't have `@cap-js/sqlite` in your package dependencies, but installed in an outer monorepo like in *[cap/samples](https://github.com/sap-samples/cloud-cap-samples)*, you can occasionally run or test your apps with the `better-sqlite` profile using one of these options to specify the profile:
55
-
56
- ```sh
57
- cds watch bookshop --profile better-sqlite
58
- ```
59
- ```sh
60
- CDS_ENV=better-sqlite cds watch bookshop
61
- ```
62
- ```sh
63
- CDS_ENV=better-sqlite jest --silent
64
- ```
65
-
66
-
67
-
68
- ## New Features & Improvements
69
-
70
- ### Full Support for Path Expressions
71
-
72
- The new SQLite service provides full support for all kinds of [path expressions](https://cap.cloud.sap/docs/cds/cql#path-expressions), including [infix filters](https://cap.cloud.sap/docs/cds/cql#with-infix-filters), and [exists predicates](https://cap.cloud.sap/docs/cds/cql#exists-predicate). For example, you can try this out with *[cap/samples](https://github.com/sap-samples/cloud-cap-samples)* as follows:
73
-
74
- ```sh
75
- cds repl --profile better-sqlite
76
- var { server } = await cds.test('bookshop')
77
- var { Books, Authors } = cds.entities
78
- await INSERT.into (Books) .entries ({ title: 'Unwritten Book' })
79
- await INSERT.into (Authors) .entries ({ name: 'Upcomming Author' })
80
- await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre }`
81
- await SELECT `from ${Authors} { books.title as book, name as author, books.genre.name as genre }`
82
- await SELECT `from ${Books} { title as book, author[ID<170].name as author, genre.name as genre }`
83
- await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre }` .where ({'author.name':{like:'Ed%'},or:{'author.ID':170}})
84
- await SELECT `from ${Books} { title as book, author.name as author, genre.name as genre } where author.name like 'Ed%' or author.ID=170`
85
- await SELECT `from ${Books}:author[name like 'Ed%' or ID=170] { books.title as book, name as author, books.genre.name as genre }`
86
- await SELECT `from ${Books}:author[150] { books.title as book, name as author, books.genre.name as genre }`
87
- await SELECT `from ${Authors} { ID, name, books { ID, title }}`
88
- await SELECT `from ${Authors} { ID, name, books { ID, title, genre { ID, name }}}`
89
- await SELECT `from ${Authors} { ID, name, books.genre { ID, name }}`
90
- await SELECT `from ${Authors} { ID, name, books as some_books { ID, title, genre.name as genre }}`
91
- await SELECT `from ${Authors} { ID, name, books[genre.ID=11] as dramatic_books { ID, title, genre.name as genre }}`
92
- await SELECT `from ${Authors} { ID, name, books.genre[name!='Drama'] as no_drama_books_count { count(*) as sum }}`
93
- await SELECT `from ${Authors} { books.genre.ID }`
94
- await SELECT `from ${Authors} { books.genre }`
95
- await SELECT `from ${Authors} { books.genre.name }`
96
-
97
- ```
98
-
99
-
100
-
101
- ### Specified Standard Functions
102
-
103
- A specified set of standard functions is now supported and translated to database-specific variants. These functions are by and large the same as specified in OData:
104
-
105
- * `concat`, `indexof`, `length`
106
- * `contains`, `startswith`, `endswith`, `substring`, `matchesPattern`
107
- * `tolower`, `toupper`
108
- * `ceiling`
109
- * `year`, `month`, `day`, `hour`, `minute`, `second`
110
-
111
- The db service implementation translates these to the best-possible native SQL functions, thus enhancing the extend of **portable** queries.
112
-
113
- > **Note** that usage is **case-sensitive**, which means you have to write these functions exactly as given above; all-uppercase usages are not supported.
114
-
115
- ### Support for Common HANA Functions
116
-
117
- In addition to the standard functions, which all new database services will support, the new SQLite service also supports these common HANA functions, to further increase the scope for portable testing:
118
-
119
- - `years_between`
120
- - `months_between`
121
- - `days_between`
122
- - `seconds_between`
123
- - `nano100_between`
124
-
125
- > Both usages are allowed here: all-lowercase as given above, as well as all-uppercase.
126
-
127
- ### Support for Session Context Variables
128
-
129
- The new SQLite service can leverage [*better-sqlite*](https://www.npmjs.com/package/better-sqlite3)'s user-defined functions to support *session context* variables. In particular, the pseudo variables `$user.id`, `$user.locale`, `$valid.from`, and `$valid.to` are available in native SQL queries like so:
130
-
131
- ```sql
132
- SELECT session_context('$user.id')
133
- SELECT session_context('$user.locale')
134
- SELECT session_context('$valid.from')
135
- SELECT session_context('$valid.to')
136
- ```
137
-
138
- Amongst other, this allows us to get rid of static helper views for localized data like `localized_de_sap_capire_Books`.
139
-
140
- ### Deep Reads via Single Queries
141
-
142
- The old database service implementation(s) translated deep reads, i.e., SELECTs with expands, into several database queries and collected the individual results into deep result structures. The new service uses `json_object` functions and alike to instead do that in one single query, with sub selects, which greatly improves performance.
143
-
144
- ### New `SELECT.localized` Queries
145
-
146
- With the old implementation when running queries like `SELECT.from(Books)` would always return localized data, without being able to easily read the non-localized data. The new service does only what you asked for, offering new `SELECT.localized` options:
147
-
148
- ```js
149
- let books = await SELECT.from(Books) //> non-localized data
150
- let lbooks = await SELECT.localized(Books) //> localized data
151
- ```
152
-
153
- Usage variants include:
154
-
155
- ```js
156
- SELECT.localized(Books)
157
- SELECT.from.localized(Books)
158
- SELECT.one.localized(Books)
159
- ```
160
-
161
- > **Note:** Queries executed through generic application service handlers continue to serve localized data as before.
162
-
163
- ### Using Lean Draft Implementation
164
-
165
- The old implementation was overly polluted with draft handling. But as draft is actually a Fiori UI concept, that should not be the case. Hence, we eliminated all draft handling from the new database service implementations, and implemented draft in a modular, non-intrusive way — called *'Lean Draft'*. The most important change is that we don't do expensive UNIONs anymore but work with single cheap selects.
166
-
167
- When using the new SQLite service the new `cds.fiori.lean_draft` mode is automatically switched on. You may additionally switch on `cds.fiori_draft_compat` in case you run into problems.
168
-
169
- More detailed documentation for that will follow soon.
170
-
171
- ### Performance Improvements
172
-
173
- The combination of the above-mentioned improvements commonly leads to significant performance improvements. For example displaying the list page of Travels in [cap/sflight](https://github.com/SAP-samples/cap-sflight) took **>250ms** in the past, and **~15ms** now.
174
-
175
- ## Known Limitations & Changes
176
-
177
- - Node v14 is no longer supported → will be dropped anyways with upcomming cds7.
178
- - JOINs and UNIONs by CQN are no longer supported → use plain SQL instead.
179
- * CQNs with subqueries require table aliases to refer to elements of outer queries.
180
- * CQNs with an empty columns array now throws an error.
181
- * Search: only single values are allowed as search expression.
182
- * CSV input: column names like `author.ID` are disallowed → use `author_ID` instead.
183
- * No `default` values are returned anymore for `virtual` elements.
184
- * `SELECT.from(...)` queries on database level don't return localized data anymore → use `SELECT.localized(...)`
185
- * Standard functions in CQN are case-sensitive → don't uppercase them.
186
- * For `@cds.on.insert/update`annotations only `$now` and `$user.id` are supported.
187
- * The `cds/db.stream()` methods are not implemented yet → will come soon.
188
-
189
- _The list of important changes is not final and will be constantly updated._
package/cds-plugin.js ADDED
@@ -0,0 +1,5 @@
1
+ const cds = require('@sap/cds/lib')
2
+
3
+ if (!cds.env.fiori.lean_draft) {
4
+ throw new Error('"@cap-js/sqlite" only works if cds.fiori.lean_draft is enabled. Please adapt your configuration.')
5
+ }
package/index.js CHANGED
@@ -1 +1 @@
1
- module.exports = require('./lib/db/sqlite/SQLiteService.js')
1
+ module.exports = require('./lib/SQLiteService.js')
@@ -0,0 +1,149 @@
1
+ {
2
+ "ABORT": 1,
3
+ "ACTION": 1,
4
+ "ADD": 1,
5
+ "AFTER": 1,
6
+ "ALL": 1,
7
+ "ALTER": 1,
8
+ "ALWAYS": 1,
9
+ "ANALYZE": 1,
10
+ "AND": 1,
11
+ "AS": 1,
12
+ "ASC": 1,
13
+ "ATTACH": 1,
14
+ "AUTOINCREMENT": 1,
15
+ "BEFORE": 1,
16
+ "BEGIN": 1,
17
+ "BETWEEN": 1,
18
+ "BY": 1,
19
+ "CASCADE": 1,
20
+ "CASE": 1,
21
+ "CAST": 1,
22
+ "CHECK": 1,
23
+ "COLLATE": 1,
24
+ "COLUMN": 1,
25
+ "COMMIT": 1,
26
+ "CONFLICT": 1,
27
+ "CONSTRAINT": 1,
28
+ "CREATE": 1,
29
+ "CROSS": 1,
30
+ "CURRENT": 1,
31
+ "CURRENT_DATE": 1,
32
+ "CURRENT_TIME": 1,
33
+ "CURRENT_TIMESTAMP": 1,
34
+ "DATABASE": 1,
35
+ "DEFAULT": 1,
36
+ "DEFERRABLE": 1,
37
+ "DEFERRED": 1,
38
+ "DELETE": 1,
39
+ "DESC": 1,
40
+ "DETACH": 1,
41
+ "DISTINCT": 1,
42
+ "DO": 1,
43
+ "DROP": 1,
44
+ "EACH": 1,
45
+ "ELSE": 1,
46
+ "END": 1,
47
+ "ESCAPE": 1,
48
+ "EXCEPT": 1,
49
+ "EXCLUDE": 1,
50
+ "EXCLUSIVE": 1,
51
+ "EXISTS": 1,
52
+ "EXPLAIN": 1,
53
+ "FAIL": 1,
54
+ "FILTER": 1,
55
+ "FIRST": 1,
56
+ "FOLLOWING": 1,
57
+ "FOR": 1,
58
+ "FOREIGN": 1,
59
+ "FROM": 1,
60
+ "FULL": 1,
61
+ "GENERATED": 1,
62
+ "GLOB": 1,
63
+ "GROUP": 1,
64
+ "GROUPS": 1,
65
+ "HAVING": 1,
66
+ "IF": 1,
67
+ "IGNORE": 1,
68
+ "IMMEDIATE": 1,
69
+ "IN": 1,
70
+ "INDEX": 1,
71
+ "INDEXED": 1,
72
+ "INITIALLY": 1,
73
+ "INNER": 1,
74
+ "INSERT": 1,
75
+ "INSTEAD": 1,
76
+ "INTERSECT": 1,
77
+ "INTO": 1,
78
+ "IS": 1,
79
+ "ISNULL": 1,
80
+ "JOIN": 1,
81
+ "KEY": 1,
82
+ "LAST": 1,
83
+ "LEFT": 1,
84
+ "LIKE": 1,
85
+ "LIMIT": 1,
86
+ "MATCH": 1,
87
+ "MATERIALIZED": 1,
88
+ "NATURAL": 1,
89
+ "NO": 1,
90
+ "NOT": 1,
91
+ "NOTHING": 1,
92
+ "NOTNULL": 1,
93
+ "NULL": 1,
94
+ "NULLS": 1,
95
+ "OF": 1,
96
+ "OFFSET": 1,
97
+ "ON": 1,
98
+ "OR": 1,
99
+ "ORDER": 1,
100
+ "OTHERS": 1,
101
+ "OUTER": 1,
102
+ "OVER": 1,
103
+ "PARTITION": 1,
104
+ "PLAN": 1,
105
+ "PRAGMA": 1,
106
+ "PRECEDING": 1,
107
+ "PRIMARY": 1,
108
+ "QUERY": 1,
109
+ "RAISE": 1,
110
+ "RANGE": 1,
111
+ "RECURSIVE": 1,
112
+ "REFERENCES": 1,
113
+ "REGEXP": 1,
114
+ "REINDEX": 1,
115
+ "RELEASE": 1,
116
+ "RENAME": 1,
117
+ "REPLACE": 1,
118
+ "RESTRICT": 1,
119
+ "RETURNING": 1,
120
+ "RIGHT": 1,
121
+ "ROLLBACK": 1,
122
+ "ROW": 1,
123
+ "ROWS": 1,
124
+ "SAVEPOINT": 1,
125
+ "SELECT": 1,
126
+ "SET": 1,
127
+ "TABLE": 1,
128
+ "TEMP": 1,
129
+ "TEMPORARY": 1,
130
+ "THEN": 1,
131
+ "TIES": 1,
132
+ "TO": 1,
133
+ "TRANSACTION": 1,
134
+ "TRIGGER": 1,
135
+ "UNBOUNDED": 1,
136
+ "UNION": 1,
137
+ "UNIQUE": 1,
138
+ "UPDATE": 1,
139
+ "USING": 1,
140
+ "VACUUM": 1,
141
+ "VALUES": 1,
142
+ "VIEW": 1,
143
+ "VIRTUAL": 1,
144
+ "WHEN": 1,
145
+ "WHERE": 1,
146
+ "WINDOW": 1,
147
+ "WITH": 1,
148
+ "WITHOUT": 1
149
+ }
@@ -0,0 +1,215 @@
1
+ const { SQLService } = require('@cap-js/db-service')
2
+ const { Readable } = require('stream')
3
+ const cds = require('@sap/cds/lib')
4
+ const sqlite = require('better-sqlite3')
5
+ const $session = Symbol('dbc.session')
6
+ const convStrm = require('stream/consumers')
7
+
8
+ class SQLiteService extends SQLService {
9
+ get factory() {
10
+ return {
11
+ options: { max: 1, ...this.options.pool },
12
+ create: tenant => {
13
+ const database = this.url4(tenant)
14
+ const dbc = new sqlite(database)
15
+ dbc.function('SESSION_CONTEXT', key => dbc[$session][key])
16
+ dbc.function('REGEXP', { deterministic: true }, (re, x) => (RegExp(re).test(x) ? 1 : 0))
17
+ if (!dbc.memory) dbc.pragma('journal_mode = WAL')
18
+ return dbc
19
+ },
20
+ destroy: dbc => dbc.close(),
21
+ validate: dbc => dbc.open,
22
+ }
23
+ }
24
+
25
+ url4(tenant) {
26
+ let { url, database: db = url } = this.options.credentials || this.options || {}
27
+ if (!db || db === ':memory:') return ':memory:'
28
+ if (tenant) db = db.replace(/\.(db|sqlite)$/, `-${tenant}.$1`)
29
+ return cds.utils.path.resolve(cds.root, db)
30
+ }
31
+
32
+ set(variables) {
33
+ const dbc = this.dbc || cds.error('Cannot set session context: No database connection')
34
+ if (!dbc[$session]) {
35
+ dbc[$session] = variables // initial call from within this.begin()
36
+ const $super = this._release
37
+ this._release = function (dbc) {
38
+ // reset session on release
39
+ delete dbc[$session]
40
+ return $super.call(this, dbc)
41
+ }
42
+ } else Object.assign(dbc[$session], variables) // subsequent uses from custom code
43
+ }
44
+
45
+ prepare(sql) {
46
+ try {
47
+ return this.dbc.prepare(sql)
48
+ } catch (e) {
49
+ e.message += ' in:\n' + (e.sql = sql)
50
+ throw e
51
+ }
52
+ }
53
+
54
+ exec(sql) {
55
+ return this.dbc.exec(sql)
56
+ }
57
+
58
+ static CQN2SQL = class CQN2SQLite extends SQLService.CQN2SQL {
59
+ SELECT_columns({ SELECT }) {
60
+ if (!SELECT.columns) return '*'
61
+ const { orderBy } = SELECT
62
+ const orderByMap = {}
63
+ // Collect all orderBy columns that should be taken from the SELECT.columns
64
+ if (Array.isArray(orderBy))
65
+ orderBy?.forEach(o => {
66
+ if (o.ref?.length === 1) {
67
+ orderByMap[o.ref[0]] = true
68
+ }
69
+ })
70
+ return SELECT.columns.map(x => {
71
+ const alias = this.column_name(x)
72
+ // Check whether the column alias should be added
73
+ const xpr = this.column_expr(x)
74
+ const needsAlias = (typeof x.as === 'string' && x.as) || orderByMap[alias]
75
+ return `${xpr}${needsAlias ? ` as ${this.quote(alias)}` : ''}`
76
+ })
77
+ }
78
+
79
+ operator(x, i, xpr) {
80
+ if (x === '=' && xpr[i + 1]?.val === null) return 'is'
81
+ if (x === '!=') return 'is not'
82
+ else return x
83
+ }
84
+
85
+ // Used for INSERT statements
86
+ static InputConverters = {
87
+ ...super.InputConverters,
88
+ Date: e => `strftime('%Y-%m-%d',${e})`,
89
+ Time: e => `strftime('%H:%M:%S',${e})`,
90
+ DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
91
+ Timestamp: e => `strftime('%Y-%m-%dT%H:%M:%fZ',${fixTimeZone(e)})`,
92
+ }
93
+
94
+ static OutputConverters = {
95
+ ...super.OutputConverters,
96
+ boolean: expr => `CASE ${expr} when 1 then 'true' when 0 then 'false' END ->'$'`, // REVIEW: ist that correct?
97
+ Int64: expr => `CAST(${expr} as TEXT)`, // REVISIT: As discussed: please put that on a list of things to revisit later on
98
+ Decimal: expr => `nullif(quote(${expr}),'NULL')->'$'`, // REVISIT: what is that ->'$' doing?
99
+ Float: expr => `nullif(quote(${expr}),'NULL')->'$'`,
100
+ Double: expr => `nullif(quote(${expr}),'NULL')->'$'`,
101
+ struct: expr => `${expr}->'$'`, // Association + Composition inherits from struct
102
+ array: expr => `${expr}->'$'`,
103
+ // REVISIT: Timestamp should not loos precision
104
+ Date: e => `strftime('%Y-%m-%d',${e})`,
105
+ Time: e => `strftime('%H:%M:%S',${e})`,
106
+ DateTime: e => `strftime('%Y-%m-%dT%H:%M:%SZ',${fixTimeZone(e)})`,
107
+ Timestamp: e => `strftime('%Y-%m-%dT%H:%M:%fZ',${fixTimeZone(e)})`,
108
+ }
109
+
110
+ // Used for SQL function expressions
111
+ static Functions = { ...super.Functions }
112
+
113
+ // Used for CREATE TABLE statements
114
+ static TypeMap = {
115
+ ...super.TypeMap,
116
+ Binary: e => `BINARY_BLOB(${e.length || 5000})`,
117
+ Date: () => 'DATE_TEXT',
118
+ Time: () => 'TIME_TEXT',
119
+ DateTime: () => 'TIMESTAMP_TEXT',
120
+ Timestamp: () => 'TIMESTAMP_TEXT',
121
+ }
122
+
123
+ static ReservedWords = { ...super.ReservedWords, ...require('./ReservedWords.json') }
124
+ }
125
+
126
+ // REALLY REVISIT: Here we are doing error handling which we probably never should have started.
127
+ // And worst of all, we handed out this as APIs without documenting it, so stakeholder tests rely
128
+ // on that? -> we urgently need to review these stakeholder tests.
129
+ // And we'd also need this to be implemented by each db service, and therefore documented, correct?
130
+ async onINSERT(req) {
131
+ try {
132
+ return await super.onINSERT(req)
133
+ } catch (err) {
134
+ throw _not_unique(err, 'ENTITY_ALREADY_EXISTS') || err
135
+ }
136
+ }
137
+
138
+ async onUPDATE(req) {
139
+ try {
140
+ return await super.onUPDATE(req)
141
+ } catch (err) {
142
+ throw _not_unique(err, 'UNIQUE_CONSTRAINT_VIOLATION') || err
143
+ }
144
+ }
145
+
146
+ // overrides generic onSTREAM
147
+ // SQLite doesn't support streaming, the whole data is read from/written into the database
148
+ async onSTREAM(req) {
149
+ const { sql, values, entries } = this.cqn2sql(req.query)
150
+ // writing stream
151
+ if (req.query.STREAM.into) {
152
+ const stream = entries[0]
153
+ stream.on('error', () => stream.removeAllListeners('error'))
154
+ values.unshift((await convStrm.buffer(stream)).toString('base64'))
155
+ const ps = await this.prepare(sql)
156
+ return (await ps.run(values)).changes
157
+ }
158
+ // reading stream
159
+ const ps = await this.prepare(sql)
160
+ let result = await ps.all(values)
161
+ if (result.length === 0) return
162
+
163
+ const val = Object.values(result[0])[0]
164
+ if (val === null) return val
165
+ const stream_ = new Readable()
166
+ stream_.push(Buffer.from(val, 'base64'))
167
+ stream_.push(null)
168
+ return stream_
169
+ }
170
+ }
171
+
172
+ // function _not_null (err) {
173
+ // if (err.code === "SQLITE_CONSTRAINT_NOTNULL") return Object.assign ({
174
+ // code: 'MUST_NOT_BE_NULL',
175
+ // target: /\.(.*?)$/.exec(err.message)[1], // here we are even constructing OData responses, with .target
176
+ // message: 'Value is required',
177
+ // })
178
+ // }
179
+
180
+ function _not_unique(err, code) {
181
+ if (err.message.match(/unique constraint/i))
182
+ return Object.assign({
183
+ originalMessage: err.message, // FIXME: required because of next line
184
+ message: code, // FIXME: misusing message as code
185
+ code: 400, // FIXME: misusing code as (http) status
186
+ })
187
+ }
188
+
189
+ /**
190
+ * Generates SQL statement that allows SQLite to support most of the ISO 8601 timezone syntaxes
191
+ * @example
192
+ * '1970-01-01T00:00:00+0200' -> '1970-01-01T00:00:00+02:00'
193
+ * @example
194
+ * '1970-01-01T00:00:00-02' -> '1970-01-01T00:00:00-02:00'
195
+ * @example
196
+ * '1970-01-01T00:00:00Z' -> '1970-01-01T00:00:00Z'
197
+ * @param {String} e value SQL expression
198
+ * @returns {String} SQL statement that ensures that the value has the valid ISO timezone for SQLite
199
+ */
200
+ const fixTimeZone = e =>
201
+ `(
202
+ SELECT
203
+ CASE
204
+ WHEN substr(T,length(T),1) = 'Z' THEN
205
+ T
206
+ WHEN substr(T,length(T) - 4,1) = '-' OR substr(T,length(T) - 4,1) = '+' THEN
207
+ substr(T,0,length(T) - 1) || ':' || substr(T,length(T) - 1)
208
+ WHEN substr(T,length(T) - 2,1) = '-' OR substr(T,length(T) - 2,1) = '+' THEN
209
+ T || ':' || '00'
210
+ ELSE T
211
+ END AS T
212
+ FROM (SELECT (${e}) AS T)
213
+ )`.replace(/\s*\n\s*/g, ' ')
214
+
215
+ module.exports = SQLiteService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/sqlite",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "CDS database service for SQLite",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -9,44 +9,40 @@
9
9
  "SQLite"
10
10
  ],
11
11
  "author": "SAP SE (https://www.sap.com)",
12
- "license": "SEE LICENSE",
13
- "homepage": "https://cap.cloud.sap/",
14
12
  "main": "index.js",
15
13
  "files": [
16
- "cds.js",
17
- "lib/db/DatabaseService.js",
18
- "lib/db/sqlite",
19
- "lib/db/sql",
20
- "lib/ql",
21
- "LICENSE",
14
+ "cds-plugin.js",
15
+ "lib",
22
16
  "CHANGELOG.md"
23
17
  ],
24
18
  "engines": {
25
- "node": ">=14",
19
+ "node": ">=16",
26
20
  "npm": ">=8"
27
21
  },
28
22
  "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 ."
23
+ "test": "jest --silent"
34
24
  },
35
25
  "dependencies": {
26
+ "@cap-js/db-service": "^1",
36
27
  "better-sqlite3": "^8"
37
28
  },
38
29
  "peerDependencies": {
39
- "@sap/cds": "*"
30
+ "@sap/cds": ">=7"
31
+ },
32
+ "cds": {
33
+ "requires": {
34
+ "kinds": {
35
+ "sql": {
36
+ "[development]": {
37
+ "kind": "sqlite"
38
+ }
39
+ },
40
+ "sqlite": {
41
+ "impl": "@cap-js/sqlite"
42
+ }
43
+ },
44
+ "db": "sql"
45
+ }
40
46
  },
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
- }
47
+ "license": "SEE LICENSE"
52
48
  }