@sap/cds 8.5.1 → 8.6.1

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 +56 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +12 -9
  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/json.js +2 -2
  13. package/lib/compile/to/sql.js +15 -8
  14. package/lib/core/linked-csn.js +10 -4
  15. package/lib/dbs/cds-deploy.js +1 -1
  16. package/lib/env/cds-env.js +76 -66
  17. package/lib/env/defaults.js +1 -0
  18. package/lib/i18n/bundles.js +2 -1
  19. package/lib/i18n/files.js +3 -3
  20. package/lib/i18n/localize.js +2 -2
  21. package/lib/index.js +24 -18
  22. package/lib/ql/CREATE.js +11 -6
  23. package/lib/ql/DELETE.js +12 -9
  24. package/lib/ql/DROP.js +15 -8
  25. package/lib/ql/INSERT.js +19 -14
  26. package/lib/ql/SELECT.js +95 -168
  27. package/lib/ql/UPDATE.js +23 -14
  28. package/lib/ql/UPSERT.js +15 -2
  29. package/lib/ql/Whereable.js +44 -118
  30. package/lib/ql/cds-ql.js +222 -28
  31. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  32. package/lib/ql/cds.ql-predicates.js +133 -0
  33. package/lib/ql/cds.ql-projections.js +111 -0
  34. package/lib/ql/cqn.d.ts +146 -0
  35. package/lib/srv/cds-connect.js +3 -3
  36. package/lib/srv/cds-serve.js +2 -2
  37. package/lib/srv/cds.Service.js +132 -0
  38. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  39. package/lib/srv/cds.ServiceProvider.js +20 -0
  40. package/lib/srv/factory.js +20 -8
  41. package/lib/srv/protocols/hcql.js +2 -3
  42. package/lib/srv/protocols/index.js +3 -3
  43. package/lib/srv/srv-dispatch.js +7 -6
  44. package/lib/srv/srv-handlers.js +103 -113
  45. package/lib/srv/srv-methods.js +14 -14
  46. package/lib/srv/srv-tx.js +5 -3
  47. package/lib/utils/cds-utils.js +2 -2
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  49. package/libx/_runtime/cds.js +2 -1
  50. package/libx/_runtime/common/aspects/service.js +25 -0
  51. package/libx/_runtime/common/generic/auth/index.js +5 -0
  52. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  53. package/libx/_runtime/common/generic/auth/service.js +24 -0
  54. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  55. package/libx/_runtime/common/generic/etag.js +1 -1
  56. package/libx/_runtime/common/utils/cqn.js +1 -2
  57. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  58. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  59. package/libx/_runtime/common/utils/postProcess.js +4 -1
  60. package/libx/_runtime/common/utils/restrictions.js +1 -0
  61. package/libx/_runtime/fiori/lean-draft.js +54 -43
  62. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  63. package/libx/_runtime/remote/Service.js +2 -0
  64. package/libx/_runtime/remote/utils/client.js +12 -0
  65. package/libx/odata/index.js +5 -3
  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 +63 -29
  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 +5 -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/cds-ql.js CHANGED
@@ -1,37 +1,231 @@
1
- const Query = require('./Query')
2
- require = path => { // eslint-disable-line no-global-assign
3
- const clazz = module.require (path); if (!clazz._api) return clazz
4
- const factory = clazz._api()
5
- const constructor = Object.assign (function (...args) {
6
- if (new.target) return new clazz (...args) // allows: new SELECT
7
- return factory (...args) // allows: SELECT(...).from()
8
- }, factory) // allows: SELECT.from()
9
- constructor.class = clazz
10
- return constructor
11
- }
12
-
13
- module.exports = exports = {
14
- Query,
15
- SELECT: require('./SELECT'),
16
- INSERT: require('./INSERT'),
17
- UPSERT: require('./UPSERT'),
18
- UPDATE: require('./UPDATE'),
19
- DELETE: require('./DELETE'),
20
- CREATE: require('./CREATE'),
21
- DROP: require('./DROP'),
1
+ const cds = require ('../index')
2
+
3
+ /**
4
+ * This is the default export, accessible to users through cds.ql.
5
+ * It's a function turning given input into an instance of cds.ql.Query.
6
+ *
7
+ * - If input is already a Query, it is returned as is.
8
+ * - If input is a string, it is parsed into a CQN object.
9
+ * - If input is a CQN object, an instance of the corresponding
10
+ * Query subclass is contructed and returned.
11
+ *
12
+ * Use it in a cast-like way like this:
13
+ *
14
+ * @example
15
+ * let q = cds.ql ({ SELECT: { from: {ref:[ Books.name ]} }})
16
+ * let q = cds.ql (CQL`SELECT from ${Books}`)
17
+ * let q = cds.ql `SELECT from ${Books}`
18
+ *
19
+ * @returns { import('./SELECT').class }, or any other subclass of Query; but IntelliSense doen't allow to specify this
20
+ */
21
+ const ql = module.exports = exports = (q,...etc) => {
22
+ if (q instanceof Query) return q
23
+ if (q.raw || typeof q === 'string') return ql (cds.parse.cql(q,...etc))
24
+ for (let k in q) if (k in ql) return new ql[k](q[k])
25
+ return q //> no-op
22
26
  }
23
27
 
24
- exports.clone = function (q,_) {
25
- return Query.prototype.clone.call(q,_)
28
+ // Base class and subclasses for all kinds of queries...
29
+ const Query = exports.Query = require('./cds.ql-Query');
30
+ exports.SELECT = require('./SELECT')
31
+ exports.INSERT = require('./INSERT')
32
+ exports.UPSERT = require('./UPSERT')
33
+ exports.UPDATE = require('./UPDATE')
34
+ exports.DELETE = require('./DELETE')
35
+ exports.CREATE = require('./CREATE')
36
+ exports.DROP = require('./DROP')
37
+
38
+
39
+ exports.predicate = require('./cds.ql-predicates')
40
+ exports.columns = require('./cds.ql-projections')
41
+ /** @import cqn from './cqn' */
42
+
43
+
44
+ /**
45
+ * Constructs a CXN `{ref}` object from given input, which can be one of:
46
+ *
47
+ * - several path segment strings
48
+ * - a single array of the same
49
+ * - a tagged template literal in CXL path syntax
50
+ *
51
+ * @returns {cqn.ref}
52
+ */
53
+ exports.ref = function (...ref) {
54
+ if (ref[0].raw) ref = String.raw(...ref).split('.')
55
+ return {ref}
26
56
  }
27
57
 
28
- exports.query = function (q) {
29
- if (q instanceof Query) return q
30
- for (let k in q) return k in this ? new this[k](q[k]) : q
58
+
59
+ /**
60
+ * Constructs CXN `{val}` object from given input, which can be one of:
61
+ * - a single `string`, `number`, `boolean`, or `null`
62
+ * - a tagged template literal in CXL literal syntax
63
+ *
64
+ * @example
65
+ * val(`foo`) //> {val:'foo'}`
66
+ * val`foo` //> {val:'foo'}
67
+ * val`11` //> {val:11}
68
+ * val(11) //> {val:11}
69
+ *
70
+ * @returns {cqn.val}
71
+ */
72
+ exports.val = (...val) => {
73
+ if (val?.[0]?.raw) val = String.raw(...val)
74
+ else [val] = val
75
+ return {val}
76
+ }
77
+
78
+
79
+ /**
80
+ * Constructs a CXN `xpr` object from given input, which can be one of:
81
+ *
82
+ * - multiple CXN `expr` objects, or strings representing keywords or operators
83
+ * - a single array of the same
84
+ * - a tagged template literal in CXL syntax
85
+ *
86
+ * @example
87
+ * xpr([ref`foo`,'=',val(11)]) //> {xpr:[{ref:['foo']},'=',{val:11}]}
88
+ * xpr(ref`foo`,'=',val(11)) //> {xpr:[{ref:['foo']},'=',{val:11}]}
89
+ * xpr`foo = 11` //> {xpr:[{ref:['foo']},'=',{val:11}]}
90
+ * xpr`foo` //> {xpr:[{ref:['foo']}]}
91
+ * xpr`'foo'` //> {xpr:[{val:'foo'}]}
92
+ * xpr`11` //> {xpr:[{val:11}]}
93
+ * xpr('=') //> {xpr:['=']}
94
+ * xpr('like') //> {xpr:['like']}
95
+ *
96
+ * @see {@link ql.expr `expr`}
97
+ * @returns {cqn.xpr}
98
+ */
99
+ exports.xpr = (...xpr) => {
100
+ const x = ql.expr(...xpr)
101
+ return x.xpr ? x : {xpr:[x]} // always returns an `{xpr}` object
102
+ }
103
+
104
+
105
+ /**
106
+ * Same as {@link ql.xpr `xpr`}, but if the result contains only single
107
+ * entries these are returned as is.
108
+ *
109
+ * @example
110
+ * expr([ref`foo`,'=',val(11)]) //> {xpr:[{ref:['foo']},'=',{val:11}]}
111
+ * expr(ref`foo`,'=',val(11)) //> {xpr:[{ref:['foo']},'=',{val:11}]}
112
+ * expr`foo = 11` //> {xpr:[{ref:['foo']},'=',{val:11}]}
113
+ * expr`foo` //> {ref:['foo']}
114
+ * expr`11` //> {val:11}
115
+ *
116
+ * @returns { cqn.ref & cqn.val & cqn.xpr & cqn.list & cqn.func } the constructed CXN `expr` object.
117
+ */
118
+ exports.expr = (...xpr) => {
119
+ const [x] = xpr; if (x?.raw) return cds.parse.expr(...xpr) //> tagged template literal
120
+ else if (is_array(x)) xpr = x //> entries are supposed to be CXN objects
121
+ return xpr.length === 1 ? xpr[0] : {xpr} //> single entries are returned as is
122
+ }
123
+
124
+
125
+ /**
126
+ * Constructs a CXN `list` object from given input, with can be one of:
127
+ *
128
+ * - multiple CXN `expr` objects, or values turned into `{val}`s, including strings
129
+ * - a single array of the same
130
+ *
131
+ * @example
132
+ * list([`foo`,11]) //> {list:[{val:'foo'},{val:11}]}
133
+ * list(`foo`,11) //> {list:[{val:'foo'},{val:11}]}
134
+ * expr`'foo',11` //> {list:[{val:'foo'},{val:11}]}
135
+ * expr`foo,11` //> {list:[{ref:['foo']},{val:11}]}
136
+ *
137
+ * @see Use {@link ql.expr `expr()`} to get the same via a tagged template literal.
138
+ * @returns {cqn.list}
139
+ */
140
+ exports.list = (...args) => {
141
+ const [x] = args; if (is_array(x)) args = x
142
+ return { list: args.map (_cqn_or_val) }
143
+ }
144
+
145
+
146
+ /**
147
+ * Constructs a CXN `func` object from given input. The first argument is the
148
+ * function name, the remaining `args` can the same as in {@link ql.list `list()`},
149
+ * and are handled the same way.
150
+ *
151
+ * @example
152
+ * func('substring',[`foo`,1]) //> {func:'substring',args:[{val:'foo'},{val:1}]}
153
+ * func('substring',`foo`,1) //> {func:'substring',args:[{val:'foo'},{val:1}]}
154
+ * expr`substring('foo',1)` //> {func:'substring',args:[{val:'foo'},{val:1}]}
155
+ * expr`substring(foo,1)` //> {func:'substring',args:[{ref:['foo']},{val:1}]}
156
+ * expr`substring(foo,1)` //> {func:'substring',args:[{ref:['foo']},{val:1}]}
157
+ *
158
+ * @see Use {@link ql.expr `expr()`} to get the same via a tagged template literal.
159
+ * @returns {cqn.func}
160
+ */
161
+ exports.func = (func,...args) => {
162
+ const [x] = args; if (is_array(x)) args = x
163
+ return { func, args: args.map (_cqn_or_val) }
164
+ }
165
+
166
+
167
+ /** @returns { cqn.ref & cqn.as & cqn.infix &{ columns: cqn.column[] }} */
168
+ exports.nested = (ref, ...args) => {
169
+ if (ref.raw) return ql.nested (ql.ref(ref,...args))
170
+ else if (!ref.ref) ref = ql.ref(ref)
171
+ for (let each of args) {
172
+ if (each.as || each.where || each.orderBy) ref = {...ref, ...each}
173
+ else ref.columns = ql.columns(each)
174
+ }
175
+ ref.columns ??= ['*']
176
+ return ref
177
+ }
178
+
179
+ exports.expand = (...args) => {
180
+ let { columns, ...rest } = ql.nested (...args)
181
+ return { ...rest, expand: columns }
182
+ }
183
+
184
+ exports.inline = (...args) => {
185
+ let { columns, ...rest } = ql.nested (...args)
186
+ return { ...rest, inline: columns }
187
+ }
188
+
189
+ /** @returns {{ where: cqn.xo[] }} */
190
+ exports.where = (...args) => ({where: ql.predicate(...args)})
191
+
192
+ /** @returns {{ orderBy: cqn.order[] }} */
193
+ exports.orderBy = (...args) => ({orderBy: ql.orders(...args)})
194
+ exports.orders = (...args) => {
195
+ const [x] = args; if (x.raw) return cds.parse._select('from X order by',args).orderBy
196
+ if (is_array(x)) args = x
197
+ else if (is_object(x) && !x.ref) {
198
+ return Object.entries(x) .map (each => _ordering_term (...each))
199
+ }
200
+ return args.map (each => {
201
+ if (each.ref) return each
202
+ if (typeof each === 'string') return _ordering_term (...each.split(' '))
203
+ })
204
+ function _ordering_term (ref,sort) {
205
+ const ot = cds.parse.ref(ref)
206
+ if (sort) ot.sort = sort == 1 ? 'asc' : sort == -1 ? 'desc' : sort
207
+ return ot
208
+ }
209
+ }
210
+
211
+ const _cqn_or_val = x => typeof x === 'object' ? x : {val:x}
212
+ const is_object = x => typeof x === 'object'
213
+ const is_array = Array.isArray
214
+
215
+
216
+ /**
217
+ * Returns a new instance of Query for the given input,
218
+ * that has the given one as prototype.
219
+ */
220
+ exports.clone = function (q,_) {
221
+ return Query.prototype.clone.call(q,_)
31
222
  }
32
223
 
33
- exports._reset = ()=>{ // for strange tests only
34
- const cds = require('../index')
224
+ /**
225
+ * For questionable tests only.
226
+ * @protected @deprecated
227
+ */
228
+ exports._reset = ()=>{
35
229
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
36
230
  Object.defineProperty (Query.prototype,'valueOf',{ configurable:1, value: function(kind=this.kind) {
37
231
  return `${kind} ${_name(this._target.name)} `
@@ -1,16 +1,36 @@
1
1
  const { AsyncResource } = require('async_hooks')
2
2
  const cds = require('../index')
3
3
 
4
+ const cloned = from => from.ref ? { ...from, ref:[...from.ref] } : { ...from }
5
+ const cached = {}
6
+
7
+
4
8
  class Query {
5
9
 
10
+ /** @private */
11
+ static init () {
12
+ const self = this, kind = self.name
13
+ Object.defineProperty (this.prototype, 'kind', {value:kind})
14
+ Object.defineProperty (this, 'name', {value:'cds.ql'})
15
+ return Object.assign (function chimera (x,...etc) {
16
+ if (!new.target) return self.call (x,...etc)
17
+ let q = new self; if (x) q[kind] = x
18
+ return q
19
+ }, this.API)
20
+ }
21
+
22
+ /** @private to be implemented in subclassed*/
23
+ static call() {}
24
+
25
+ /** @private */
26
+ get _(){ return this[this.kind] }
27
+
28
+
6
29
  /**
7
30
  * The kind of query, as in CQN's SELECT, INSERT, UPDATE, DELETE, CREATE, DROP.
8
- * Overridden in subclasses to return the respective kind.
9
31
  */
10
32
  get kind() { return this.constructor.name }
11
- get _(){ return this[this.kind] }
12
33
 
13
- constructor(_={}) { this[this.kind] = _ }
14
34
 
15
35
  /**
16
36
  * Note to self: We can't use .as (alias) as that would conflict with
@@ -37,6 +57,7 @@ class Query {
37
57
 
38
58
  /** Binds this query to be executed with the given service */
39
59
  bind (srv) {
60
+ // TODO: resolve target from srv.entities -> but better do that in .from
40
61
  return Object.defineProperty (this,'_srv',{ value:srv, configurable:true, writable:true })
41
62
  }
42
63
 
@@ -47,59 +68,48 @@ class Query {
47
68
  return (r,e) => q.runInAsyncScope (srv.run, srv, this).then (r,e)
48
69
  }
49
70
 
50
- _target4 (...args) {
51
- return this._target_ref4 (...args)
71
+ /** @private */ get _target_ref() { return this._.from } // overridden in subclasses
72
+ /** @private */ set _target(t) { this._set('_target', t.ref ? {name:t.ref[0]} : t) }
73
+
74
+ /** @private */ _target4 (t,...etc) {
75
+ switch (typeof t) {
76
+ case 'string': {
77
+ // NOTE: even though this._target will be {name:ref[0]} this returns the parsed {ref}
78
+ // NOTE: we need to clone cached refs, as they might get modified afterwards
79
+ return this._target = cloned (cached[t] ??= cds.parse.path(t))
80
+ }
81
+ case 'object': {
82
+ if (t.raw) return this._target = !etc.length ? this._target4(t[0]) : cds.parse.path(t,...etc)
83
+ if (t.name) return {ref:[ (this._target = t).name ]}
84
+ if (t.ref || t.SELECT || t.SET) return this._target = t
85
+ }
86
+ }
87
+ throw this._expected `${{target:t}} to be an entity path string, a CSN definition, a {ref}, a {SELECT}, or a {SET}`
52
88
  }
53
89
 
54
- _target_ref4 (target, arg2) {
55
-
56
- // If called with a linked entity -> use it as this.target
57
- if (target instanceof cds.entity) this.target = target
58
-
59
- // REVISIT: this._target is not reliable !!!
60
- this._set ('_target', function _target(t) { return t && (
61
- typeof t === 'string' ? { name: t } :
62
- t.ref ? { name: t.ref[0] } :
63
- t.raw ? _target(arg2) :
64
- t.SELECT ? _target(t.SELECT.from) :
65
- t //> default is assumed to be a csn definition or a look-alike or a SELECT
66
- )}(target))
67
-
68
- // Determine from.ref or from.SELECT
69
- const from = target && (
70
- target.name ? {ref:[target.name]} :
71
- typeof target === 'string' ? cds.parse.path(target) :
72
- target.raw ? cds.parse.path(...arguments) :
73
- target.ref ? target :
74
- target.SELECT ? target :
75
- target.SET ? target : 0
76
- )
77
- return from || this._expected `${{target}} to be an entity path string, a CSN definition, a {ref}, a {SELECT}, or a {SET}`
90
+ /** @private */ _expected (...args) {
91
+ return cds.error.expected (...args)
78
92
  }
79
93
 
80
- _expected (...args) {
81
- return cds.error.expected (...args)
94
+ /** @private */ _assign (...aspects) {
95
+ Object.assign (this._, ...aspects)
96
+ return this
82
97
  }
83
98
 
84
- _add (property, values) {
99
+ /** @private */ _add (property, values) {
85
100
  const {_} = this, pd = Reflect.getOwnPropertyDescriptor (_,property)
86
- _[property] = !pd || !pd.value ? values : [ ...pd.value, ...values ]
101
+ _[property] = !pd?.value ? values : [ ...pd.value, ...values ]
87
102
  return this
88
103
  }
89
104
 
90
- _set (property, value) {
91
- Reflect.defineProperty (this, property, { value, configurable:true, writable:true })
92
- return value
105
+ /** @private */ _set (property, value) {
106
+ return Object.defineProperty (this, property, { value, configurable:true, writable:true })
93
107
  }
94
108
 
95
109
  valueOf (prelude = this.kind) {
96
110
  return `${prelude} ${_name(this._target.name)} `
97
111
  }
98
112
 
99
- get _target_ref() {
100
- throw cds.error `Query subclass ${this.constructor.name} must implement '_target_ref'`
101
- }
102
-
103
113
  /**
104
114
  * Returns the inferred query's source, which is the entity referred
105
115
  * to in SELECT.from, INSERT.into, UPDATE.entity, or DELETE.from,
@@ -118,8 +128,9 @@ class Query {
118
128
  * returns the target of the sub query, recursively.
119
129
  */
120
130
  get target() {
131
+ if (this._target instanceof cds.entity) return super.target = this._target
121
132
  const m = this._srv?.model || cds.context?.model || cds.model
122
- return cds.infer (this, m?.definitions)
133
+ return this.target = cds.infer (this, m?.definitions)
123
134
  }
124
135
  set target(t) { this._set('target',t) }
125
136
 
@@ -0,0 +1,133 @@
1
+ const cds = require('../index')
2
+ module.exports = predicate
3
+
4
+
5
+ /** @import cqn from './cqn' */
6
+ /** @returns {cqn.xo[]} */
7
+ function predicate (...args) {
8
+ const [x] = args
9
+ if (x) switch (typeof x) {
10
+ case 'object':
11
+ if (x.raw) return _ttl(args)
12
+ if (args.length > 1) return _legacy(...args) //> legacy support for predicate (ref`a`, '=', val`b`)
13
+ if (is_array(x)) return x
14
+ if (is_cqn(x)) return [x]
15
+ else return _qbe(x)
16
+ case 'string': return _fluid(args)
17
+ default: return args.map(_val)
18
+ }
19
+ }
20
+
21
+
22
+ function _ttl (args) {
23
+ const cxn = cds.parse.expr(...args)
24
+ return cxn.xpr ?? [cxn] //> the fallback is for single-item exprs like `1` or `ref`
25
+ }
26
+
27
+
28
+ function _fluid (args) {
29
+ if (args.length === 3) switch (args[1]) {
30
+ case '=': case '<': case '<=': case '>': case '>=': case '!=': case '<>': case 'like': case 'in': case 'IN': case 'LIKE':
31
+ return _legacy (...args)
32
+ }
33
+ if (args.length % 2 === 0) args.push('')
34
+ const expr = args.filter((_, i) => i % 2 === 0).join(' ? ')
35
+ const vals = args.filter((_, i) => i % 2 === 1)
36
+ const { xpr } = _pred_expr(expr); (function _fill_in_vals_into (xpr) {
37
+ xpr.forEach((x, i) => {
38
+ if (x.xpr) _fill_in_vals_into(x.xpr)
39
+ if (x.param) xpr[i] = _val(vals.shift())
40
+ })
41
+ })(xpr)
42
+ return xpr
43
+ }
44
+
45
+
46
+ function _qbe (o, xpr=[]) {
47
+
48
+ let count = 0
49
+ for (let k in o) { const x = o[k]
50
+
51
+ if (k.startsWith('not ')) { xpr.push('not'); k = k.slice(4) }
52
+ switch (k) { // handle special cases like {and:{...}} or {or:{...}}
53
+ case 'between':
54
+ xpr.push('between', _val(x), 'and', _val(o.and))
55
+ return xpr
56
+ case 'and': xpr.push(k)
57
+ x.or ? xpr.push({xpr:_qbe(x)}) : _qbe(x,xpr)
58
+ continue
59
+ case 'or': xpr.push(k)
60
+ _qbe(x,xpr)
61
+ continue
62
+ case 'not': xpr.push(k)
63
+ if (x && typeof x === 'object') x.in || x.like || x.exists || x.between ? _qbe(x,xpr) : xpr.push({xpr:_qbe(x)})
64
+ else xpr.push(x === null ? 'null' : {val:x})
65
+ continue
66
+ case 'is': xpr.push('is')
67
+ if (x && typeof x === 'object') _qbe(x,xpr)
68
+ else xpr.push(x === null ? 'null' : {val:x})
69
+ continue
70
+ case 'is not': xpr.push('is','not')
71
+ if (x && typeof x === 'object') _qbe(x,xpr)
72
+ else xpr.push(x === null ? 'null' : {val:x})
73
+ continue
74
+ case 'exists':
75
+ xpr.push(k,_ref(x)||x)
76
+ continue
77
+ case 'in': case 'IN': // REVISIT: 'IN' is for compatibility only
78
+ xpr.push(k, x.SELECT ? x : { list: x.map(_val) })
79
+ continue
80
+ case '=': case '<': case '<=': case '>': case '>=': case '!=': case '<>': case 'like': case 'LIKE': // REVISIT: 'LIKE' is for compatibility only
81
+ xpr.push(k,_val(x))
82
+ continue
83
+ }
84
+
85
+ const a = cds.parse.ref(k) //> turn key into a ref for the left side of the expression
86
+ if (count++) xpr.push('and') //> add 'and' between conditions
87
+ if (!x || typeof x !== 'object') xpr.push (a,'=',{val:x})
88
+ else if (is_array(x)) xpr.push (a,'in',{list:x.map(_val)})
89
+ else if (x.SELECT || x.list) xpr.push (a,'in',x)
90
+ else if (is_cqn(x)) xpr.push (a,'=',x)
91
+ else if (x instanceof Date) xpr.push (a,'=',{val:x})
92
+ else if (x instanceof Buffer) xpr.push (a,'=',{val:x})
93
+ else if (x instanceof RegExp) xpr.push (a,'like',{val:x})
94
+ else { xpr.push(a); _qbe(x,xpr) } //> recurse into nested qbe
95
+ }
96
+ return xpr
97
+ }
98
+
99
+
100
+ /** @deprecated */
101
+ const _legacy = (x,o,y) => [ _ref(x)||x, o, _val(y) ]
102
+
103
+ const _pred_expr = x => {
104
+ if (typeof x !== 'string') return {val:x}
105
+ if (x === '*') return x
106
+ const t = /^\s*([\w.'?]+)(?:\s*([!?\\/:=\-+<~>]+|like)\s*([\w.'?]+))?\s*$/.exec(x)
107
+ if (!t) return cds.parse.expr(x)
108
+ const [,lhs,op,rhs] = t
109
+ return !op ? _ref_or_val(lhs) : {xpr:[ _ref_or_val(lhs), op, _ref_or_val(rhs) ]}
110
+ }
111
+
112
+ const _ref_or_val = (x) => {
113
+ if (x[0] === '?') return { param: true, ref: x }
114
+ if (x[0] === "'") return { val: x.slice(1,-1).replace(/''/g, "'") }
115
+ if (x === 'null') return { val: null }
116
+ if (x === 'true') return { val: true }
117
+ if (x === 'false') return { val: false }
118
+ if (!isNaN(x)) return { val: Number(x) }
119
+ else return { ref: x.split('.') }
120
+ }
121
+
122
+ const _ref = x => typeof x === 'string' ? { ref: x.split('.') } : x // if x is a string, turn it into a ref; if not we assume it's a cxn object
123
+ const _val = x => !x ? {val:x} : is_array(x) ? { list: x.map(_val) } : is_cqn(x) ? x : {val:x}
124
+
125
+ const is_cqn = x => typeof x === 'object' && (
126
+ 'ref' in x ||
127
+ 'val' in x ||
128
+ 'xpr' in x ||
129
+ 'list' in x ||
130
+ 'func' in x ||
131
+ 'SELECT' in x
132
+ )
133
+ const is_array = Array.isArray
@@ -0,0 +1,111 @@
1
+ const predicate = require('./cds.ql-predicates')
2
+ const cds = require('../index')
3
+
4
+ /** @import cqn from './cqn' */
5
+ /** @returns {cqn.column[]} */
6
+ module.exports = function projection (...args) {
7
+ const [x] = args
8
+ if (x) switch (typeof x) {
9
+ case 'function':
10
+ return _function(x)
11
+ case 'object':
12
+ if (x.raw) return _ttl(args)
13
+ if (is_array(x)) return _array(x)
14
+ break //> processing all args below
15
+ case 'string':
16
+ if (x[0] === '{') return _string(x)
17
+ }
18
+ return _array (args)
19
+ }
20
+
21
+
22
+ function _ttl (args) {
23
+ if (args[0][0][0] === '{') return cds.parse._select('from X',args).columns
24
+ return cds.parse._select('',args,'from X').columns
25
+ }
26
+
27
+ function _string (x) {
28
+ return cds.parse.cql('SELECT from X '+ x).SELECT.columns
29
+ }
30
+
31
+
32
+ function _array (args) {
33
+ return args.map (column_expr)
34
+ function column_expr (x) {
35
+ switch (typeof x) {
36
+ case 'string': {
37
+ if (x === '*') return x
38
+ let alias = /\s+as\s+(\w+)$/i.exec(x)
39
+ if (alias) return { ...cds.parse.ref(x.slice(0,alias.index)), as: alias[1] }
40
+ return cds.parse.ref(x)
41
+ }
42
+ case 'object': {
43
+ if (x.name) return { ref: [x.name] } //> reflected element
44
+ if (is_cqn(x)) return x //> already a CQN object
45
+ else for (let e in x) return {...cds.parse.ref(e), as: x[e] } //> { element: alias }
46
+ throw this._expected`Argument for SELECT.columns(${x}) to be a valid column expression`
47
+ }
48
+ default: return { val: x }
49
+ }
50
+ }
51
+ }
52
+
53
+
54
+ function _function (fn) {
55
+ const columns = []; fn(new Proxy(fn, {
56
+ apply: (_, __, args) => {
57
+ if (!args.length) return columns.push('*')
58
+ let [x] = is_array(args[0]) ? args[0] : args
59
+ columns.push(x === '*' || x === '.*' ? '*' : is_cqn(x) ? x : { ref: [x] })
60
+ return { as: (alias) => (x.as = alias) }
61
+ },
62
+ get: (_, p) => {
63
+ const col = { ref: [p] }; columns.push(col)
64
+ const nested = new Proxy(fn, {
65
+ get: (_, p) => {
66
+ if (p === 'where') return (x) => ((col.where = predicate(x)), nested)
67
+ if (p === 'as') return (alias) => ((col.as = alias), nested)
68
+ else return col.ref.push(p), nested
69
+ },
70
+ apply: (_, __, args) => {
71
+ const [a, b] = args
72
+ if (!a) col.expand = ['*']
73
+ else if (a.raw) switch (a[0]) {
74
+ case '*': col.expand = ['*']; break
75
+ case '.*': col.inline = ['*']; break
76
+ default: {
77
+ // The ttl is the tail of a column expression including infic filter and nested projection.
78
+ // So, we need to add the col name as prefix to be able to parse it...
79
+ const {columns} = cds.parse._select(col.ref.at(-1), args, 'from X')
80
+ Object.assign(col, columns[0])
81
+ }
82
+ }
83
+ else if (is_array(a)) col.expand = _array(a)
84
+ else if (a === '*') col.expand = ['*']
85
+ else if (a === '.*') col.inline = ['*']
86
+ else if (typeof a === 'string') col.ref.push(a)
87
+ else if (typeof a === 'function') {
88
+ let x = (col[/^\(?_\b/.test(a) ? 'inline' : 'expand'] = _function(a))
89
+ if (b?.levels) while (--b.levels) x.push({ ...col, expand: (x = [...x]) })
90
+ } else if (typeof b === 'function') {
91
+ let x = (col[/^\(?_\b/.test(b) ? 'inline' : 'expand'] = _function(b))
92
+ if (a?.depth) while (--a.depth) x.push({ ...col, expand: (x = [...x]) })
93
+ }
94
+ return nested
95
+ },
96
+ })
97
+ return nested
98
+ },
99
+ }))
100
+ return columns
101
+ }
102
+
103
+ const is_cqn = x => typeof x === 'object' && (
104
+ 'ref' in x ||
105
+ 'val' in x ||
106
+ 'xpr' in x ||
107
+ 'list' in x ||
108
+ 'func' in x ||
109
+ 'SELECT' in x
110
+ )
111
+ const is_array = Array.isArray