@sap/cds 8.5.0 → 8.6.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +54 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +2 -2
  5. package/lib/compile/for/java.js +15 -3
  6. package/lib/compile/for/lean_drafts.js +44 -34
  7. package/lib/compile/for/nodejs.js +19 -10
  8. package/lib/compile/minify.js +2 -4
  9. package/lib/compile/parse.js +106 -72
  10. package/lib/compile/to/edm.js +19 -9
  11. package/lib/compile/to/hana.js +25 -21
  12. package/lib/compile/to/sql.js +15 -8
  13. package/lib/core/linked-csn.js +10 -4
  14. package/lib/dbs/cds-deploy.js +2 -2
  15. package/lib/env/cds-env.js +76 -66
  16. package/lib/env/defaults.js +1 -0
  17. package/lib/i18n/bundles.js +2 -1
  18. package/lib/i18n/localize.js +2 -2
  19. package/lib/index.js +24 -18
  20. package/lib/ql/CREATE.js +11 -6
  21. package/lib/ql/DELETE.js +12 -9
  22. package/lib/ql/DROP.js +15 -8
  23. package/lib/ql/INSERT.js +19 -14
  24. package/lib/ql/SELECT.js +95 -168
  25. package/lib/ql/UPDATE.js +23 -14
  26. package/lib/ql/UPSERT.js +15 -2
  27. package/lib/ql/Whereable.js +44 -118
  28. package/lib/ql/cds-ql.js +222 -28
  29. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  30. package/lib/ql/cds.ql-predicates.js +133 -0
  31. package/lib/ql/cds.ql-projections.js +111 -0
  32. package/lib/ql/cqn.d.ts +146 -0
  33. package/lib/srv/cds-connect.js +3 -3
  34. package/lib/srv/cds-serve.js +2 -2
  35. package/lib/srv/cds.Service.js +132 -0
  36. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  37. package/lib/srv/cds.ServiceProvider.js +20 -0
  38. package/lib/srv/factory.js +20 -8
  39. package/lib/srv/protocols/hcql.js +2 -3
  40. package/lib/srv/protocols/index.js +3 -3
  41. package/lib/srv/srv-dispatch.js +7 -6
  42. package/lib/srv/srv-handlers.js +103 -113
  43. package/lib/srv/srv-methods.js +14 -14
  44. package/lib/srv/srv-tx.js +5 -3
  45. package/lib/utils/cds-utils.js +2 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  47. package/libx/_runtime/cds.js +2 -1
  48. package/libx/_runtime/common/aspects/service.js +25 -0
  49. package/libx/_runtime/common/generic/auth/index.js +5 -0
  50. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  51. package/libx/_runtime/common/generic/auth/service.js +24 -0
  52. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  53. package/libx/_runtime/common/generic/etag.js +1 -1
  54. package/libx/_runtime/common/utils/cqn.js +1 -2
  55. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  56. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  57. package/libx/_runtime/common/utils/postProcess.js +4 -1
  58. package/libx/_runtime/common/utils/restrictions.js +1 -0
  59. package/libx/_runtime/fiori/lean-draft.js +53 -42
  60. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  61. package/libx/_runtime/remote/Service.js +2 -0
  62. package/libx/_runtime/remote/utils/client.js +12 -0
  63. package/libx/odata/ODataAdapter.js +2 -1
  64. package/libx/odata/index.js +5 -3
  65. package/libx/odata/middleware/batch.js +4 -0
  66. package/libx/odata/middleware/create.js +2 -2
  67. package/libx/odata/middleware/delete.js +2 -2
  68. package/libx/odata/middleware/operation.js +2 -2
  69. package/libx/odata/middleware/read.js +14 -12
  70. package/libx/odata/middleware/service-document.js +16 -8
  71. package/libx/odata/middleware/update.js +2 -2
  72. package/libx/odata/parse/afterburner.js +64 -30
  73. package/libx/odata/parse/grammar.peggy +95 -0
  74. package/libx/odata/parse/parser.js +1 -1
  75. package/libx/odata/utils/index.js +6 -1
  76. package/libx/odata/utils/metadata.js +69 -75
  77. package/libx/odata/utils/postProcess.js +24 -3
  78. package/package.json +1 -1
  79. package/server.js +1 -1
  80. package/lib/ql/parse.js +0 -36
  81. /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
package/lib/ql/SELECT.js CHANGED
@@ -1,89 +1,102 @@
1
- const Whereable = require('./Whereable'), { parse, predicate4 } = Whereable
1
+ const Whereable = require('./Whereable')
2
+ const is_number = x => !isNaN(x)
2
3
  const cds = require('../index')
3
-
4
- module.exports = class Query extends Whereable {
5
-
6
- get kind() { return 'SELECT' }
7
- static _api() {
8
- const $ = Object.assign
9
- return $((..._) => new this()._select_or_from(..._), {
10
- localized: $((...x) => new this({localized:true})._select_or_from(...x),{
11
- columns: (..._) => new this({localized:true}).columns(..._),
12
- from: (..._) => new this({localized:true}).from(..._),
13
- }),
14
- distinct: $((...x) => new this({distinct:true})._select_or_from(...x),{
15
- columns: (..._) => new this({distinct:true}).columns(..._),
16
- from: (..._) => new this({distinct:true}).from(..._),
17
- }),
18
- one: $((...x) => new this({one:true})._select_or_from(...x),{
19
- columns: (..._) => new this({one:true}).columns(..._),
20
- from: (..._) => new this({one:true}).from(..._),
21
- localized: (..._) => new this({one:true,localized:true}).from(..._)
22
- }),
23
- from: $((..._) => new this().from(..._), {
24
- localized: (..._) => new this({localized:true}).from(..._)
25
- }),
26
- columns: (..._) => new this().columns(..._),
27
- })
28
- }
29
-
30
- _select_or_from (cols, ...more) { // srv.read`title`.from`Books` or srv.read`Books` ?
31
- if (!cols) return this
32
- else if (is_number(cols)) return this.columns(...arguments) //> numbers can't be from
33
- else if (cols.name) return this.from (...arguments) //> clearly a from
34
- else if (cols.raw) { // tagged template string
35
- if (cols[0].startsWith('from ')) { // SELECT`from ...`, with an arbitrary long CQL tail...
36
- Object.assign (this.SELECT, SELECT_(' ',arguments))
37
- return this
38
- } else if (cols[0][0] === '{') { // SELECT`{a,b}`... -> it's columns
39
- let {columns:c} = SELECT_('from X', arguments)
40
- return this._add('columns',c)
41
- } else { // SELECT`Foo` -> ambiguous -> try parsing as columns...
42
- let {columns:c} = SELECT_('from X {', arguments, '}')
43
- if (c.length > 1 || !c[0].ref) return this._add('columns',c)
44
- // else cols = c[0] //> goes on below...
4
+ const $ = Object.assign
5
+
6
+ class SELECT extends Whereable {
7
+
8
+ /** @type import('./cqn').SELECT['SELECT'] */
9
+ SELECT = {}
10
+
11
+ static call = (..._) => (new this)._select_or_from(..._) // SELECT `x`...
12
+ static API = {
13
+ columns: (..._) => (new this).columns(..._),
14
+ from: $((..._) => (new this).from(..._), {
15
+ localized: (..._) => (new this).localized.from(..._)
16
+ }),
17
+ localized: $((..._) => (new this).localized._select_or_from(..._),{
18
+ columns: (..._) => (new this).localized.columns(..._),
19
+ from: (..._) => (new this).localized.from(..._),
20
+ }),
21
+ distinct: $((..._) => (new this).distinct._select_or_from(..._),{
22
+ localized: (..._) => (new this).distinct.localized.from(..._),
23
+ columns: (..._) => (new this).distinct.columns(..._),
24
+ from: (..._) => (new this).distinct.from(..._),
25
+ }),
26
+ one: $((..._) => (new this).one._select_or_from(..._),{
27
+ localized: (..._) => (new this).one.localized.from(..._),
28
+ columns: (..._) => (new this).one.columns(..._),
29
+ from: (..._) => (new this).one.from(..._),
30
+ }),
31
+ read: (..._) => (new this)._select_or_from(..._),
32
+ class: this
33
+ }
34
+
35
+ get localized() { this.SELECT.localized = true; return this }
36
+ get distinct() { this.SELECT.distinct = true; return this }
37
+ get one() { this.SELECT.one = true; return this }
38
+
39
+ /** @private */ _select_or_from (x,...etc) {
40
+ if (!x) return this
41
+ else if (x == '*') return this.columns (x,...etc)
42
+ else switch (typeof x) {
43
+ case 'string': {
44
+ if (etc.length) return this._ambiguous (x,...etc)
45
+ else (x = [x]).raw = x // single arg -> resolve in case 'object' block below
45
46
  }
46
- } else { // SELECT('Foo'|'*',[...]|(foo)=>{})
47
- if (cols === '*') return this.columns(...arguments)
48
- const c = _columns_or_not (cols)
49
- if (c) return this._add('columns',c)
50
- if (typeof cols === 'string') {
51
- try { parse.path(cols) }
52
- catch { //> it can't be a from
53
- try { return this.columns(...arguments) }
54
- catch(e) {
55
- if (!e.message.startsWith('CDS compilation failed')) throw e
56
- this._expected `Argument ${{cols}} to be a valid argument for SELECT.from or .columns`
57
- }
47
+ case 'object': { // eslint-disable-line no-fallthrough
48
+ if (x.raw) { // TODO: We might add a cache and reuse parsed [cqn,params] ...
49
+ const SELECT = cds.parse._select('',[x,...etc]), {columns:cols} = SELECT
50
+ if (SELECT.from || cols.length > 1 || !cols[0].ref) // SELECT `from Foo` | `a,b` | `max(a)`
51
+ return this._assign (SELECT)
52
+ const { columns:[{ expand, ...from }], ...more_clauses } = SELECT
53
+ if (expand) more_clauses.columns = expand // SELECT `Foo {a,b}`
54
+ if (0 in Object.keys(more_clauses)) // SELECT `Foo where x=1`
55
+ return this._assign ({from}, more_clauses)
56
+ else return this._ambiguous (x,...etc)
58
57
  }
58
+ else if (Array.isArray(x)) return this.columns(x)
59
+ else if (x.kind === 'element') return this.columns (x,...etc)
60
+ else if (x.name) return this.from (x,...etc)
61
+ else if (x.ref) return this._ambiguous (x,...etc)
62
+ else if (x.from) return this._assign(x)
63
+ else if (x.SELECT || x.SET) return this.from(x)
64
+ else break
59
65
  }
66
+ default: return this.columns(x)
60
67
  }
68
+ throw this._expected `Argument for SELECT(${{x}}) to be a valid argument for SELECT.from or .columns`
69
+ }
61
70
 
62
- // return a proxy assuming it's a from and switching to
63
- // columns on a subsequent call of .from, if any.
71
+ /** @private */ _ambiguous (...xy) {
64
72
  const {SELECT:_} = this, {one} = _
65
- return Object.defineProperties (this.from (cols, ...more), {
66
- from: { configurable:true, value:(...args) => { delete this.from
67
- if (!one) delete _.one; delete _.columns; delete _.where
68
- return this.from (...args) .columns (cols, ...more)
69
- }}
73
+ return this.from(...xy)._set('from', (...the_real_target) => {
74
+ if (!one) delete _.one; delete _.columns; delete this.from
75
+ return this.from (...the_real_target) .columns (...xy)
70
76
  })
71
77
  }
72
78
 
73
79
  columns (...cols) {
74
- if (cols[0]) this._add ('columns', _columns(cols))
80
+ if (cols[0]) this._add ('columns', cds.ql.columns(...cols))
75
81
  return this
76
82
  }
77
83
 
78
- from (target, second, third) {
79
- this.SELECT.from = target === '*' || this._target_ref4 (...arguments)
80
- if (!target.raw && second !== undefined) {
84
+ from (...args) {
85
+ const [target] = args
86
+ if (target.raw) {
87
+ let { from, ...more } = cds.parse._select ('from',args)
88
+ this.SELECT.from = this._target4 (from)
89
+ return this._assign(more)
90
+ }
91
+ this.SELECT.from = this._target4 (target)
92
+ const [,second,third] = args
93
+ if (second !== undefined) {
81
94
  if (third) {
82
95
  this.byKey(second)
83
96
  this.columns(third)
84
97
  } else {
85
- const cols = _columns_or_not (second)
86
- cols ? this._add('columns',cols) : this.byKey(second)
98
+ if (Array.isArray(second) || typeof second === 'function') this.columns(second)
99
+ else this.byKey(second)
87
100
  }
88
101
  }
89
102
  return this
@@ -95,19 +108,22 @@ module.exports = class Query extends Whereable {
95
108
  innerJoin (other, as) { return this.join (other, as, 'inner') }
96
109
  join (other, as, kind='inner') {
97
110
  const [, target, alias = as] = /(\S+)(?:\s+(?:as)?\s+(\S+))?/i.exec(other)
98
- const ref = { ref: [target] }
99
- if (alias) ref.as = alias
111
+ const ref = { ref: [target] }; if (alias) ref.as = alias
100
112
  this.SELECT.from = { join:kind, args: [this.SELECT.from, ref] }
101
113
  return Object.defineProperty(this, '_where_or_having', { value: 'on', configurable: true })
102
114
  }
103
115
  on (...args) {
104
- if (!this.SELECT.from || !this.SELECT.from.join || !this.SELECT.from.args) // `join` can also be a function, e.g. in SELECT.from(SELECT.from('foo'))
105
- throw new Error(`Invalid call of "SELECT.on()" without prior call of "SELECT.join()"`)
106
- return this._where (args,'and','on')
116
+ const {from} = this.SELECT
117
+ if (!from?.join) throw new Error(`Invalid call of "SELECT.on()" without prior call of "SELECT.join()"`)
118
+ // string values in on clause are interpreted as refs, not vals
119
+ const [o] = args; if (typeof o === 'object' && !o.raw && !Array.isArray(o)) {
120
+ for (let a in o) if (typeof o[a] === 'string') o[a] = {ref:o[a].split('.')}
121
+ }
122
+ return this._where (args,'on',from)
107
123
  }
108
124
 
109
125
  having(...args) {
110
- return this._where (args,'and','having')
126
+ return this._where (args,'having')
111
127
  }
112
128
 
113
129
  search (...args) {
@@ -118,13 +134,13 @@ module.exports = class Query extends Whereable {
118
134
 
119
135
  groupBy (...args) {
120
136
  if (!args[0]) return this
121
- const cqn = args[0].raw ? SELECT_('from X group by', args).groupBy : args.map(parse.expr)
137
+ const cqn = args[0].raw ? cds.parse._select('from X group by', args).groupBy : args.map(cds.parse.ref)
122
138
  return this._add('groupBy',cqn)
123
139
  }
124
140
 
125
141
  orderBy (...args) {
126
142
  if (!args[0]) return this
127
- return this._add('orderBy',_order_by(args))
143
+ return this._add('orderBy', cds.ql.orders(...args))
128
144
  }
129
145
 
130
146
  limit (rows, offset) {
@@ -153,101 +169,12 @@ module.exports = class Query extends Whereable {
153
169
  return super.valueOf('SELECT * FROM')
154
170
  }
155
171
 
156
- get _target_ref(){ return this.SELECT.from }
172
+ /** @private */ get _target_ref(){ return this.SELECT.from }
157
173
 
158
174
  get elements() { return this.elements = cds.infer.elements4 (this.SELECT.columns, this.source) }
159
175
  set elements(e) { this._set('elements',e) }
160
176
  }
161
177
 
162
178
 
163
- const _columns = (args) => {
164
- const x = args[0]
165
- if (x.raw) {
166
- if (x[0][0] === '{') return SELECT_('from X ',args).columns
167
- else return SELECT_('from X {',args,'}').columns
168
- } else {
169
- if (typeof x === 'string' && x[0] === '{') return parse.cql('SELECT from X '+ x).SELECT.columns
170
- else return _columns_or_not(x) || args.map(_column_expr)
171
- }
172
- }
173
-
174
- const _columns_or_not = (x) => {
175
- if (typeof x === 'function') return _projection4(x)
176
- if (Array.isArray(x)) return x.map(_column_expr)
177
- }
178
-
179
- const _column_expr = (x) => {
180
- if (is_cqn(x)) return x
181
- if (typeof x === 'string') return parse.column(x)
182
- if (typeof x === 'object') for (let one in x) return Object.assign(parse.expr(one),{as:x[one]})
183
- else return {val:x}
184
- }
185
-
186
- const _projection4 = (fn) => {
187
- const columns=[]; fn (new Proxy (fn,{
188
- apply: (_, __, args) => { // handle top-level projections such as (foo)=>{ foo('*') }
189
- if (!args.length) return columns.push('*')
190
- let [x] = Array.isArray(args[0]) ? args[0] : args
191
- columns.push (x === '*' || x === '.*' ? '*' : is_cqn(x) ? x : {ref:[x]})
192
- return { as: (alias) => (x.as = alias) }
193
- },
194
- get: (_, p) => { // handle top-level paths like (foo)=>{ foo.bar }
195
- const col = {ref:[p]}; columns.push(col)
196
- const nested = new Proxy(fn,{ // handle n-fold paths like (foo)=>{ foo.bar.car }
197
- get: (_, p) => {
198
- if (p === 'where') return (x) => ((col.where = predicate4([x])), nested)
199
- if (p === 'as') return (alias) => ((col.as = alias), nested)
200
- else return col.ref.push(p), nested
201
- },
202
- apply: (_, __, args) => { // handle nested projections e.g. (foo)=>{ foo.bar (b=>{ ... }) }
203
- const [a, b] = args
204
- if (!a) col.expand = ['*']
205
- else if (a.raw) switch (a[0]) {
206
- case '*': col.expand = ['*']; break
207
- case '.*': col.inline = ['*']; break
208
- default: Object.assign (col, SELECT_(col.ref.at(-1)+' ', args, ' from X').columns[0])
209
- }
210
- else if (Array.isArray(a)) col.expand = _columns(a)
211
- else if (a === '*') col.expand = ['*']
212
- else if (a === '.*') col.inline = ['*']
213
- else if (typeof a === 'string') col.ref.push(a)
214
- else if (typeof a === 'function') {
215
- let x = (col[/^\(?_\b/.test(a) ? 'inline' : 'expand'] = _projection4(a))
216
- if (b?.levels) while (--b.levels) x.push({ ...col, expand: (x = [...x]) })
217
- } else if (typeof b === 'function') {
218
- let x = (col[/^\(?_\b/.test(b) ? 'inline' : 'expand'] = _projection4(b))
219
- if (a?.depth) while (--a.depth) x.push({ ...col, expand: (x = [...x]) })
220
- }
221
- return nested
222
- },
223
- })
224
- return nested
225
- },
226
- }))
227
- return columns
228
- }
229
-
230
- const _order_by = (args) => {
231
- if (args[0].raw) return SELECT_('from X order by', args).orderBy
232
- if (Array.isArray(args[0])) args = args[0]
233
- const cqn=[], _add = (ref,ad) => {
234
- const obx = parse.expr(ref); cqn.push(obx)
235
- if (ad) obx.sort = ad == 1 ? 'asc' : ad == -1 ? 'desc' : ad
236
- }
237
- for (let each of args) {
238
- if (each.ref) cqn.push(each)
239
- else if (typeof each === 'string') _add (...each.split(' '))
240
- else for (let ref in each) _add (ref, each[ref])
241
- }
242
- return cqn
243
- }
244
-
245
- const {CQL} = parse, SELECT_ = (prefix, [ strings, ...more ], suffix) => {
246
- const tts = [...strings]; tts.raw = true
247
- if (prefix) tts[0] = `SELECT ${prefix} ${tts[0]}`
248
- if (suffix) tts[tts.length-1] += ` ${suffix}`
249
- return CQL(tts,...more).SELECT
250
- }
251
-
252
- const is_cqn = x => x === undefined || x === '*' || x.val !== undefined || x.xpr || x.ref || x.list || x.func || x.SELECT
253
- const is_number = x => !isNaN(x)
179
+ /** @type SELECT.API & (...columns:string[]) => SELECT */
180
+ module.exports = SELECT.init()
package/lib/ql/UPDATE.js CHANGED
@@ -1,18 +1,19 @@
1
1
  const Whereable = require('./Whereable')
2
- const { parse } = require('../index')
2
+ const cds = require('../index')
3
3
 
4
- module.exports = class Query extends Whereable {
4
+ class UPDATE extends Whereable {
5
5
 
6
- get kind() { return 'UPDATE' }
7
- static _api() {
8
- return Object.assign ((..._) => (new this).entity(..._), {
9
- entity: (..._) => (new this).entity(..._),
10
- })
6
+ /** @type import('./cqn').UPDATE['UPDATE'] */
7
+ UPDATE = {}
8
+
9
+ static call = (..._) => (new this).entity(..._)
10
+ static API = { class:this,
11
+ entity: (..._) => (new this).entity(..._)
11
12
  }
12
13
 
13
- entity (e, key) {
14
- this.UPDATE.entity = this._target4 (...arguments) // supporting tts
15
- if (key !== undefined) this.byKey(key)
14
+ entity (e, ...etc) {
15
+ this.UPDATE.entity = this._target4 (e, ...etc) // supporting tts
16
+ if (!e.raw && etc.length) this.byKey(etc[0])
16
17
  return this
17
18
  }
18
19
 
@@ -30,8 +31,7 @@ module.exports = class Query extends Whereable {
30
31
 
31
32
  // A tagged template string with a single expression, e.g. .with `my.stock -= 1`
32
33
  if (args[0].raw) {
33
- let [lhs,op,...rhs] = parse.CXL(...args).xpr || this._expected `${{args}} to contain expressions of form 'column = <expr>'`
34
- _add (this, lhs.ref.join('.'), op, ...rhs)
34
+ _add (this, ..._parse_set_expr (this, String.raw(...args)))
35
35
  }
36
36
 
37
37
  // Alternating expr fragment / values args, e.g. .with ('my.stock -=',1, 'lastOrder =', '$now')
@@ -45,8 +45,7 @@ module.exports = class Query extends Whereable {
45
45
  // A single string with comma-separated expressions, e.g. .with ('my.stock -= 1, lastOrder = $now')
46
46
  else if (typeof args[0] === 'string') {
47
47
  for (let each of _comma_separated_exprs(args[0])) {
48
- let [lhs,op,...rhs] = parse.expr(each).xpr || this._expected `${{args}} to contain expressions of form 'column = <expr>'`
49
- _add (this, lhs.ref.join('.'), op, ...rhs)
48
+ _add (this, ..._parse_set_expr (this, each))
50
49
  }
51
50
  }
52
51
 
@@ -97,4 +96,14 @@ const _comma_separated_exprs = (s) => {
97
96
  return all
98
97
  }
99
98
 
99
+ const _parse_set_expr = (q,expr) => {
100
+ const [,lhs,rhs] = /^\s*(\w+)\s*=\s*(.*)/.exec(expr)
101
+ || q._expected `${{expr}} to contain expressions of form 'column = <expr>'`
102
+ return [ lhs, '=', cds.parse.expr(rhs) ]
103
+ }
104
+
100
105
  const operators = { '=':1, '-=':2, '+=':2, '*=':2, '/=':2, '%=':2 }
106
+
107
+
108
+ /** @type UPDATE.API & (...entries:[]) => UPDATE */
109
+ module.exports = UPDATE.init()
package/lib/ql/UPSERT.js CHANGED
@@ -1,3 +1,16 @@
1
- module.exports = class Query extends require('./INSERT') {
2
- get kind() { return 'UPSERT' }
1
+ const { class:INSERT } = require('./INSERT')
2
+ class UPSERT extends INSERT {
3
+
4
+ /** @type import('./cqn').UPSERT['UPSERT'] */
5
+ UPSERT = {}
6
+
7
+ constructor() { delete super().INSERT }
8
+ static call = (..._) => (new this).entries(..._)
9
+ static API = { class:this,
10
+ into: (..._) => (new this).into(..._)
11
+ }
12
+
3
13
  }
14
+
15
+ /** @type UPSERT.API & (...entries:[]) => UPSERT */
16
+ module.exports = UPSERT.init()
@@ -1,131 +1,57 @@
1
- const { error } = require ('../index')
2
1
  const cds = require('../index')
3
- const parse = require('./parse')
2
+ const Query = require('./cds.ql-Query')
4
3
 
5
- class Query extends require('./Query') {
6
4
 
7
- where(...x) { return this._where (x,'and','where') }
8
- and(...x) { return this._where (x,'and') }
9
- or(...x) { return this._where (x,'or') }
10
- _where (args, and_or, _where) {
11
- if (!args[0]) return this
12
- let pred = predicate4(args, _where)
13
- if (pred && pred.length > 0) {
14
- let {_} = this
15
- const clause = _where ?? (
16
- _.having ? 'having' :
17
- _.where ? 'where' :
18
- _.from?.on ? 'on' :
19
- error (`Invalid attempt to call '${this.kind}.${and_or}()' before a prior call to '${this.kind}.where()'`)
20
- )
21
- if (clause === 'on') _ = _.from
22
- let left = _[clause]
23
- if (!left) { //> .where() called first time
24
- // SELECT.from `X` .where `x or y` .and `z` -> SELECT from X where (x or y) and z
25
- if (pred.includes('or')) this._left_has_or = true
26
- _[clause] = pred
27
- } else { //> .where(), .and(), .or() called successively
28
- if (_where) {
29
- // SELECT.from `X` .where `x` .or `y` .where `z` -> SELECT from X where (x or y) and z
30
- if (left.includes('or')) left = [{xpr:left}]
31
- } else if (and_or === 'and') {
32
- // SELECT.from `X` .where `x` .or `y` .and `z` -> SELECT from X where x or y and z
33
- if (this._left_has_or) { left = [{xpr:left}]; delete this._left_has_or }
34
- }
35
- // SELECT.from `X` .where `x` .and `y or z` -> SELECT from X where x and (y or z)
36
- if (pred.includes('or')) pred = [{xpr:pred}]
37
- _[clause] = [ ...left, and_or, ...pred ]
38
- }
5
+ class Whereable extends Query {
6
+
7
+ where (...args) { return this._where (args) }
8
+ and (...args) { return this._and_or ('and',args) }
9
+ or (...args) { return this._and_or ('or',args) }
10
+
11
+ /** @protected */
12
+ _where (args, clause='where', cqn=this._) {
13
+ let tail = cds.ql.predicate(...args); if (!tail?.length) return this
14
+ let head = cqn[clause]
15
+ if (!head) cqn[clause] = tail
16
+ else { //> always wrap ORs on subsequent .where() calls, as these happen somewhere else
17
+ if (head.includes('or')) head = [{xpr:head}]
18
+ if (tail.includes('or')) tail = [{xpr:tail}]
19
+ cqn[clause] = [ ...head, 'and', ...tail ]
39
20
  }
21
+ this._clause = this._direct = clause
40
22
  return this
41
23
  }
42
24
 
43
- byKey(key) {
44
- if (typeof key !== 'object' || key === null) key = { [Object.keys(this._target.keys||{ID:1})[0]]: key }
45
- if (this.SELECT) this.SELECT.one = true
46
- if (cds.env.features.keys_into_where) return this.where(key)
47
- if (this.UPDATE) { this.UPDATE.entity = { ref: [{ id: this.UPDATE.entity.ref.at(-1), where: predicate4([key]) }] }; return this }
48
- if (this.SELECT) { this.SELECT.from.ref[this.SELECT.from.ref.length-1] = { id: this.SELECT.from.ref.at(-1), where: predicate4([key]) }; return this }
49
- if (this.DELETE) { this.DELETE.from = { ref: [{ id: this.DELETE.from.ref.at(-1), where: predicate4([key]) }] }; return this }
50
- return this.where(key)
51
- }
52
- }
53
-
54
- const predicate4 = (args, _clause) => {
55
- if (args.length === 0) return; /* else */ const x = args[0]
56
- if (x.raw) {
57
- let cxn = parse.CXL(...args)
58
- return cxn.xpr ?? [cxn] //> the fallback is for single-item exprs like `1` or `ref`
59
- }
60
- if (args.length === 1 && typeof x === 'object') {
61
- if (is_array(x)) return x
62
- if (is_cqn(x)) return args
63
- else return _object_predicate(args,_clause)
25
+ /** @private */
26
+ _and_or (ao, args) {
27
+ let clause = this._clause ??= ( //> some projects use .and()/.or() with manually filled in .where
28
+ this._.having ? 'having' :
29
+ this._.where ? 'where' :
30
+ this._.from?.on ? 'on' :
31
+ cds.error `Unexpected use of .${ao}() without prior .where(), .having(), or .on()`
32
+ )
33
+ let tail = cds.ql.predicate(...args); if (!tail?.length) return this
34
+ let cqn = clause === 'on' ? this._.from : this._
35
+ let head = cqn[clause], direct = this._direct; delete this._direct
36
+ if (direct && ao === 'and') //> wrap ORs for .and() directly following .where()
37
+ if (head.includes('or')) head = [{xpr:head}]
38
+ if (tail.includes('or')) tail = [{xpr:tail}]
39
+ cqn[clause] = [ ...head, ao, ...tail ]
40
+ return this
64
41
  }
65
- else return _fluid_predicate(args)
66
- }
67
42
 
68
- const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock: {'>=':1})
69
- const pred = []
70
- for (const k in arg) {
71
- const x = arg[k]
72
- if (k === 'and') {
73
- if (x.or) pred.push('and', {xpr:predicate4([x],_clause)})
74
- else pred.push('and', ...predicate4([x],_clause))
75
- continue
76
- }
77
- if (k === 'or') {
78
- pred.push('or', ...predicate4([x],_clause))
79
- continue
80
- }
81
- if (k === 'not') {
82
- pred.push('not', {xpr:predicate4([x],_clause)})
83
- continue
84
- }
85
- if (k === 'exists') {
86
- pred.push('and', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
87
- continue
88
- }
89
- if (k === 'not exists') {
90
- pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
91
- continue
92
- }
93
- if (k === 'in') {
94
- pred.push('in', val(x))
95
- continue
96
- }
97
- else pred.push('and', parse.expr(k))
98
- if (!x || x==='*') pred.push('=', {val:x})
99
- else if (x.SELECT || x.list) pred.push('in', x)
100
- else if (is_array(x)) pred.push('in', {list:x.map(val)})
101
- else if (is_cqn(x)) pred.push('=', x)
102
- else if (x instanceof Buffer) pred.push('=', {val:x})
103
- else if (x instanceof RegExp) pred.push('like', {val:x})
104
- else if (x instanceof Date) pred.push('=', {val:x})
105
- else if (typeof x === 'object') for (let op in x) x[op]?.in ? pred.push(op, ...predicate4([x[op]],_clause)) : pred.push(op, val(x[op])) // REVIST: Should always be proper recursion
106
- else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
107
- else pred.push('=', {val:x})
43
+ byKey (key) {
44
+ if (typeof key !== 'object' || key === null)
45
+ key = cds.env.features.keys_as_val ? { val: key } : { [Object.keys(this._target.keys||{ID:1})[0]]: key }
46
+ if (this.SELECT) this.SELECT.one = true
47
+ if (cds.env.features.keys_into_where) return this.where(key)
48
+ const {ref} = this._target_ref
49
+ ref[ref.length-1] = { id: ref.at(-1), where: cds.ql.predicate(key) }
50
+ return this
108
51
  }
109
- return pred[0] === 'and' ? pred.slice(1) : pred
110
- }
111
52
 
112
- const _fluid_predicate = (args) => { // e.g. .where ('ID=',4711, 'and stock >=',1)
113
- if (args.length === 3 && args[1] in operators) return [ ref(args[0]), args[1], val(args[2]) ] // REVISIT: Legacy!
114
- if (args.length % 2 === 0) args.push('')
115
- const expr = args.filter((_, i) => i % 2 === 0).join(' ? ')
116
- const vals = args.filter((_, i) => i % 2 === 1)
117
- const {xpr} = parse.expr(expr)
118
- ;(function _fill_in_vals_into (xpr) { xpr.forEach ((x,i) => {
119
- if (x.xpr) _fill_in_vals_into (x.xpr)
120
- if (x.param) xpr[i] = val(vals.shift())
121
- })})(xpr)
122
- return xpr
53
+ /** @private */ set _clause (clause) { this._set('_clause',clause) }
54
+ /** @private */ set _direct (clause) { this._set('_direct',clause) }
123
55
  }
124
56
 
125
- const ref = x => is_cqn(x) ? x : {ref:x.split('.')}
126
- const val = x => !x ? {val:x} : is_array(x) ? {list:x.map(val)} : is_cqn(x) ? x : {val:x}
127
- const is_cqn = x => x.val !== undefined || x.xpr || x.ref || x.list || x.func || x.SELECT
128
- const is_array = Array.isArray
129
- const operators = { '=':1, '<':2, '<=':2, '>':2, '>=':2, '!=':3, '<>':3, in:4, like:4, IN:4, LIKE:4 }
130
-
131
- module.exports = Object.assign (Query, { predicate4, parse })
57
+ module.exports = Whereable