@sap/cds 8.6.1 → 8.7.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 (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +4 -7
  3. package/bin/serve.js +3 -1
  4. package/lib/compile/for/lean_drafts.js +1 -1
  5. package/lib/compile/for/nodejs.js +1 -0
  6. package/lib/compile/to/sql.js +12 -8
  7. package/lib/core/classes.js +3 -4
  8. package/lib/core/types.js +1 -0
  9. package/lib/env/cds-requires.js +2 -2
  10. package/lib/ql/cds-ql.js +8 -1
  11. package/lib/ql/cds.ql-Query.js +9 -2
  12. package/lib/req/validate.js +3 -2
  13. package/lib/srv/cds-connect.js +1 -1
  14. package/lib/srv/cds-serve.js +2 -9
  15. package/lib/srv/cds.Service.js +0 -1
  16. package/lib/srv/factory.js +56 -71
  17. package/lib/srv/middlewares/auth/ias-auth.js +44 -14
  18. package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
  19. package/lib/srv/middlewares/auth/xssec.js +1 -1
  20. package/lib/srv/middlewares/errors.js +8 -10
  21. package/lib/utils/cds-utils.js +5 -1
  22. package/lib/utils/tar-lib.js +58 -0
  23. package/libx/_runtime/common/Service.js +0 -4
  24. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  25. package/libx/_runtime/common/generic/input.js +3 -1
  26. package/libx/_runtime/common/utils/csn.js +5 -1
  27. package/libx/_runtime/common/utils/resolveView.js +1 -1
  28. package/libx/_runtime/fiori/lean-draft.js +1 -1
  29. package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
  30. package/libx/odata/middleware/batch.js +22 -23
  31. package/libx/odata/middleware/create.js +4 -0
  32. package/libx/odata/middleware/delete.js +2 -0
  33. package/libx/odata/middleware/operation.js +2 -0
  34. package/libx/odata/middleware/read.js +4 -0
  35. package/libx/odata/middleware/stream.js +4 -0
  36. package/libx/odata/middleware/update.js +4 -0
  37. package/libx/odata/parse/afterburner.js +9 -4
  38. package/libx/odata/parse/grammar.peggy +7 -8
  39. package/libx/odata/parse/multipartToJson.js +0 -1
  40. package/libx/odata/parse/parser.js +1 -1
  41. package/libx/odata/utils/normalizeTimeData.js +43 -0
  42. package/libx/odata/utils/readAfterWrite.js +1 -1
  43. package/libx/outbox/index.js +1 -1
  44. package/libx/rest/RestAdapter.js +20 -4
  45. package/package.json +6 -2
  46. package/lib/srv/protocols/odata-v2.js +0 -26
  47. package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
  48. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
  49. package/libx/_runtime/common/code-ext/config.js +0 -13
  50. package/libx/_runtime/common/code-ext/execute.js +0 -123
  51. package/libx/_runtime/common/code-ext/handlers.js +0 -50
  52. package/libx/_runtime/common/code-ext/worker.js +0 -70
  53. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
@@ -1,17 +1,17 @@
1
1
  const { isStandardError } = require('../../../libx/_runtime/common/error/standardError')
2
2
 
3
3
  const production = process.env.NODE_ENV === 'production'
4
- const cds = require ('../..')
4
+ const cds = require('../..')
5
5
  const LOG = cds.log('error')
6
6
  const { inspect } = cds.utils
7
7
 
8
-
9
8
  module.exports = () => {
9
+ // eslint-disable-next-line no-unused-vars
10
10
  return async function http_error(error, req, res, next) {
11
11
  if (isStandardError(error) && cds.env.server.shutdown_on_uncaught_errors) {
12
12
  cds.log().error('❗️Uncaught', error)
13
13
  await cds.shutdown(error)
14
- return;
14
+ return
15
15
  }
16
16
 
17
17
  // In case of 401 require login if available by auth strategy
@@ -26,11 +26,13 @@ module.exports = () => {
26
26
  error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g, '')
27
27
 
28
28
  if (400 <= status && status < 500) {
29
- LOG.warn (status, '>', inspect(error))
29
+ LOG.warn(status, '>', inspect(error))
30
30
  } else {
31
- LOG.error (status, '>', inspect(error))
31
+ LOG.error(status, '>', inspect(error))
32
32
  }
33
33
 
34
+ if (res.headersSent) return
35
+
34
36
  // Expose as little information as possible in production, and as much as possible in development
35
37
  if (production) {
36
38
  Object.defineProperties(error, {
@@ -44,14 +46,10 @@ module.exports = () => {
44
46
  })
45
47
  }
46
48
 
47
- if (res.headersSent) {
48
- return next(error)
49
- }
50
-
51
49
  // Send the error response
52
50
  return res.status(status).json({ error })
53
51
 
54
52
  // Note: express returns errors as XML, we prefer JSON
55
- // _next (error)
53
+ // next(error)
56
54
  }
57
55
  }
@@ -1,6 +1,10 @@
1
1
  const cwd = process.env._original_cwd || process.cwd()
2
2
  const cds = require('../index')
3
3
 
4
+ /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
5
+ // eslint-disable-next-line no-unused-vars
6
+ const _tarLib = () => { try { return require('tar') } catch(_) {} }
7
+
4
8
  module.exports = exports = new class {
5
9
  get colors() { return super.colors = require('./colors') }
6
10
  get inflect() { return super.inflect = require('./inflect') }
@@ -16,7 +20,7 @@ module.exports = exports = new class {
16
20
  get uuid() { return super.uuid = require('crypto').randomUUID }
17
21
  get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
18
22
  get pool() { return super.pool = require('@sap/cds-foss').pool }
19
- get tar() { return super.tar = require('./tar') }
23
+ get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
20
24
  }
21
25
 
22
26
  /** @type {import('node:path')} */
@@ -0,0 +1,58 @@
1
+ const cds = require('../index'), { path, mkdirp } = cds.utils
2
+ const tar = require('tar')
3
+ const { Readable } = require('stream')
4
+ const cons = require('stream/consumers')
5
+
6
+ const _resolve = (...x) => path.resolve (cds.root,...x)
7
+
8
+ exports.create = async (root, ...args) => {
9
+ if (typeof root === 'string') root = _resolve(root)
10
+ if (Array.isArray(root)) [ root, ...args ] = [ cds.root, root, ...args ]
11
+
12
+ const options = {}
13
+ if (args.includes('-z')) options.gzip = true
14
+ const index = args.findIndex(el => el === '-f')
15
+ if (index>=0) options.file = _resolve(args[index+1])
16
+ options.cwd = root
17
+
18
+ let dirs = []
19
+ for (let i=0; i<args.length; i++) {
20
+ if (args[i] === '-z' || args[i] === '-f') break
21
+ if (Array.isArray(args[i])) args[i].forEach(a => dirs.push(_resolve(a)))
22
+ else if (typeof args[i] === 'string') dirs.push(_resolve(args[i]))
23
+ }
24
+ if (!dirs.length) dirs.push(root)
25
+ dirs = dirs.map(d => path.relative(root, d))
26
+
27
+ const stream = await tar.c(options, dirs)
28
+
29
+ return stream && await cons.buffer(stream)
30
+ }
31
+
32
+ exports.extract = (archive, ...args) => ({
33
+ async to (dest) {
34
+ if (typeof dest === 'string') dest = _resolve(dest)
35
+ const stream = Readable.from(archive)
36
+
37
+ const options = { C: dest }
38
+ if (args.includes('-z')) options.gzip = true
39
+
40
+ return new Promise((resolve, reject) => {
41
+ const tr = tar.x(options)
42
+ stream.pipe(tr)
43
+ tr.on('close', () => resolve())
44
+ tr.on('error', e => reject(e))
45
+ })
46
+ }
47
+ })
48
+
49
+ const tar_ = exports
50
+ exports.c = tar_.create
51
+ exports.cz = (d,...args) => tar_.c (d, ...args, '-z')
52
+ exports.cf = (t,d,...args) => tar_.c (d, ...args, '-f',t)
53
+ exports.czf = (t,d,...args) => tar_.c (d, ...args, '-z', '-f',t)
54
+ exports.czfd = (t,...args) => mkdirp(path.dirname(t)).then (()=> tar_.czf (t,...args))
55
+ exports.x = tar_.xf = tar_.extract
56
+ exports.xz = tar_.xzf = a => tar_.x (a, '-z')
57
+ exports.xv = tar_.xvf = a => tar_.x (a)
58
+ exports.xvz = tar_.xvzf = a => tar_.x (a, '-z')
@@ -62,10 +62,6 @@ class ApplicationService extends cds.Service {
62
62
  require('./generic/sorting').call(this)
63
63
  }
64
64
 
65
- static handle_code_ext() {
66
- if (cds.env.requires.extensibility?.code) require('./code-ext/handlers').call(this)
67
- }
68
-
69
65
  static handle_fiori() {
70
66
  require('../fiori/lean-draft').impl.call(this)
71
67
  }
@@ -37,7 +37,7 @@ const getRejectReason = (req, annotation, definition, restrictedCount, unrestric
37
37
  }
38
38
 
39
39
  const _isNull = element => element.val === null || element.list?.length === 0
40
- const _isNotNull = element => element.val !== null && (!element.list || element.list.length)
40
+ const _isNotNull = element => element.val !== null && element.list?.length > 0
41
41
 
42
42
  const _processNullAttr = where => {
43
43
  if (!where) return
@@ -212,7 +212,7 @@ const _pick = element => {
212
212
  // should be a db feature, as we cannot handle completely on service level (cf. deep update)
213
213
  // -> add to attic env behavior once new dbs handle this
214
214
  // also happens in validate but because of draft activate we have to do it twice (where cleansing is suppressed)
215
- if (element['@Core.Immutable']) {
215
+ if (element['@Core.Immutable'] && !element.key) {
216
216
  categories.push('immutable')
217
217
  }
218
218
 
@@ -260,6 +260,8 @@ async function commonGenericInput(req) {
260
260
  const bound = req.target.actions?.[req.event] || req.target.actions?.[req._.event]
261
261
  if (bound) assertOptions.path = [bound['@cds.odata.bindingparameter.name'] || 'in']
262
262
 
263
+ if (req.protocol) assertOptions.rejectIgnore = true
264
+
263
265
  const errs = cds.validate(req.data, req.target, assertOptions)
264
266
  if (errs) {
265
267
  if (errs.length === 1) throw errs[0]
@@ -115,19 +115,23 @@ const prefixForStruct = element => {
115
115
  function getDraftTreeRoot(entity, model) {
116
116
  if (entity.own('__draftTreeRoot')) return entity.__draftTreeRoot
117
117
 
118
+ const previous = new Set() // track visited entities to identify hierarchies
118
119
  let parent
119
120
  let current = entity
120
121
  while (current && !current['@Common.DraftRoot.ActivationAction']) {
122
+ previous.add(current.name)
121
123
  const parents = []
122
124
  for (const k in model.definitions) {
125
+ if (previous.has(k)) continue
123
126
  const e = model.definitions[k]
124
127
  if (e.kind !== 'entity' || !e.compositions) continue
125
128
  for (const c in e.compositions)
126
129
  if (
127
130
  e.compositions[c].target === current.name ||
128
131
  e.compositions[c].target === current.name.replace(/\.drafts/, '')
129
- )
132
+ ) {
130
133
  parents.push(e)
134
+ }
131
135
  }
132
136
  if (parents.length > 1 && parents.some(p => p !== parents[0])) {
133
137
  // > unable to determine single parent
@@ -34,7 +34,7 @@ const _inverseTransition = transition => {
34
34
  const ref0 = value.ref[0]
35
35
  if (value.ref.length > 1) {
36
36
  // ignore flattened columns like author.name
37
- if (transition.target.elements[ref0].isAssociation) continue
37
+ if (transition.target.elements[ref0]?.isAssociation) continue
38
38
 
39
39
  const nested = inverseTransition.mapping.get(ref0) || {}
40
40
  if (!nested.transition) nested.transition = { mapping: new Map() }
@@ -1509,7 +1509,7 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
1509
1509
  }
1510
1510
 
1511
1511
  async function onNewCleanse(req) {
1512
- cds.validate(req.data, req.target, {})
1512
+ cds.validate(req.data, req.target, { insert: true })
1513
1513
  }
1514
1514
  onNewCleanse._initial = true
1515
1515
  async function onNew(req) {
@@ -14,13 +14,17 @@ class EnterpriseMessagingShared extends AMQPWebhookMessaging {
14
14
 
15
15
  getClient() {
16
16
  if (this.client) return this.client
17
+ this.client = new AMQPClient(this.getClientOptions())
18
+ return this.client
19
+ }
20
+
21
+ getClientOptions() {
17
22
  const optionsAMQP = optionsMessagingAMQP(this.options)
18
- this.client = new AMQPClient({
23
+ return {
19
24
  optionsAMQP,
20
25
  prefix: { topic: 'topic:', queue: 'queue:' },
21
26
  service: this
22
- })
23
- return this.client
27
+ }
24
28
  }
25
29
 
26
30
  getManagement() {
@@ -167,30 +167,23 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
167
167
  }
168
168
 
169
169
  const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
170
- if (group) {
171
- res.write(`--${boundary}${CRLF}`)
172
- res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
173
- }
174
- const header = group || boundary
170
+ res.write(`--${boundary}${CRLF}`)
171
+
175
172
  if (rejected) {
176
173
  const resp = responses.find(r => r.status === 'fail')
177
- if (resp.separator && res._writeSeparator) res.write(resp.separator)
178
174
  resp.txt.forEach(txt => {
179
- res.write(`--${header}${CRLF}`)
180
- res.write(`${txt}`)
175
+ res.write(`${txt}${CRLF}`)
181
176
  })
182
177
  } else {
178
+ if (group) res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
183
179
  for (const resp of responses) {
184
- if (resp.separator) res.write(resp.separator)
185
180
  resp.txt.forEach(txt => {
186
- res.write(`--${header}${CRLF}`)
187
- res.write(`${txt}`)
181
+ if (group) res.write(`--${group}${CRLF}`)
182
+ res.write(`${txt}${CRLF}`)
188
183
  })
189
184
  }
185
+ if (group) res.write(`--${group}--${CRLF}`)
190
186
  }
191
- if (group) res.write(`${CRLF}--${group}--${CRLF}`)
192
- // indicates that we need to write a potential separator before the next error response
193
- res._writeSeparator = true
194
187
  }
195
188
 
196
189
  const _writeResponseJson = (responses, res) => {
@@ -281,12 +274,18 @@ const _tx_done = async (tx, responses, isJson) => {
281
274
  delete txt.headers['content-length']
282
275
  res.txt = [JSON.stringify(txt)]
283
276
  } else {
284
- let txt = res.txt[0]
285
- txt = txt.replace(/HTTP\/1\.1 \d\d\d \w+/, `HTTP/1.1 ${statusCode} ${STATUS_CODES[statusCode]}`)
286
- txt = txt.split(/\r\n/)
287
- txt.splice(-1, 1, JSON.stringify({ error }))
288
- txt = txt.join('\r\n')
289
- res.txt = [txt]
277
+ const commitError = [
278
+ 'content-type: application/http',
279
+ 'content-transfer-encoding: binary',
280
+ '',
281
+ `HTTP/1.1 ${statusCode} ${STATUS_CODES[statusCode]}`,
282
+ 'odata-version: 4.0',
283
+ 'content-type: application/json;odata.metadata=minimal;IEEE754Compatible=true',
284
+ '',
285
+ JSON.stringify({ error })
286
+ ].join(CRLF)
287
+ res.txt = [commitError]
288
+ break
290
289
  }
291
290
  }
292
291
  }
@@ -421,14 +420,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
421
420
  .then(req => {
422
421
  const resp = { status: 'ok' }
423
422
  if (separator) resp.separator = separator
424
- else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
423
+ else separator = Buffer.from(',')
425
424
  resp.txt = _formatResponse(req, atomicityGroup)
426
425
  responses.push(resp)
427
426
  })
428
427
  .catch(failedReq => {
429
428
  const resp = { status: 'fail' }
430
429
  if (separator) resp.separator = separator
431
- else separator = isJson ? Buffer.from(',') : Buffer.from(CRLF)
430
+ else separator = Buffer.from(',')
432
431
  resp.txt = _formatResponse(failedReq, atomicityGroup)
433
432
  tx.failed = failedReq
434
433
  responses.push(resp)
@@ -446,7 +445,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
446
445
  ? _writeResponseJson(responses, res)
447
446
  : _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
448
447
  } else sendPreludeOnce()
449
- res.write(isJson ? ']}' : `${CRLF}--${boundary}--${CRLF}`)
448
+ res.write(isJson ? ']}' : `--${boundary}--${CRLF}`)
450
449
  res.end()
451
450
 
452
451
  return
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
6
6
  const postProcess = require('../utils/postProcess')
7
7
  const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
+ const normalizeTimeData = require('../utils/normalizeTimeData')
9
10
 
10
11
  const { getKeysAndParamsFromPath } = require('../../common/utils')
11
12
 
@@ -32,6 +33,7 @@ module.exports = (adapter, isUpsert) => {
32
33
 
33
34
  // payload & params
34
35
  const data = req.body
36
+ normalizeTimeData(data, model, target)
35
37
  const { keys, params } = getKeysAndParamsFromPath(from, { model })
36
38
  // add keys from url into payload (overwriting if already present)
37
39
  Object.assign(data, keys)
@@ -66,6 +68,8 @@ module.exports = (adapter, isUpsert) => {
66
68
  })
67
69
  })
68
70
  .then(result => {
71
+ if (res.headersSent) return
72
+
69
73
  handleSapMessages(cdsReq, req, res)
70
74
 
71
75
  // case: read after write returns no results, e.g., due to auth (academic but possible)
@@ -57,6 +57,8 @@ module.exports = adapter => {
57
57
  })
58
58
  })
59
59
  .then(() => {
60
+ if (res.headersSent) return
61
+
60
62
  handleSapMessages(cdsReq, req, res)
61
63
 
62
64
  res.sendStatus(204)
@@ -101,6 +101,8 @@ module.exports = adapter => {
101
101
  return service
102
102
  .run(() => service.dispatch(cdsReq))
103
103
  .then(result => {
104
+ if (res.headersSent) return
105
+
104
106
  handleSapMessages(cdsReq, req, res)
105
107
 
106
108
  if (operation.returns?.items && result == null) result = []
@@ -128,6 +128,8 @@ const _handleArrayOfQueriesFactory = adapter => {
128
128
  })
129
129
  })
130
130
  .then(result => {
131
+ if (res.headersSent) return
132
+
131
133
  handleSapMessages(cdsReq, req, res)
132
134
 
133
135
  if (req.url.match(/\/\$count/)) return res.set('Content-Type', 'text/plain').send(_count(result).toString())
@@ -238,6 +240,8 @@ module.exports = adapter => {
238
240
  })
239
241
  })
240
242
  .then(result => {
243
+ if (res.headersSent) return
244
+
241
245
  handleSapMessages(cdsReq, req, res)
242
246
 
243
247
  // 204
@@ -218,6 +218,8 @@ module.exports = adapter => {
218
218
  return service
219
219
  .run(() => {
220
220
  return service.dispatch(cdsReq).then(async result => {
221
+ if (res.headersSent) return
222
+
221
223
  _validateStream(req, result)
222
224
 
223
225
  if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
@@ -241,6 +243,8 @@ module.exports = adapter => {
241
243
  })
242
244
  })
243
245
  .then(() => {
246
+ if (res.headersSent) return
247
+
244
248
  handleSapMessages(cdsReq, req, res)
245
249
 
246
250
  res.end()
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
6
6
  const postProcess = require('../utils/postProcess')
7
7
  const readAfterWrite4 = require('../utils/readAfterWrite')
8
8
  const getODataResult = require('../utils/result')
9
+ const normalizeTimeData = require('../utils/normalizeTimeData')
9
10
 
10
11
  const { getKeysAndParamsFromPath } = require('../../common/utils')
11
12
 
@@ -69,6 +70,7 @@ module.exports = adapter => {
69
70
 
70
71
  // payload & params
71
72
  const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
73
+ normalizeTimeData(data, model, target)
72
74
  const { keys, params } = getKeysAndParamsFromPath(from, { model })
73
75
  // add keys from url into payload (overwriting if already present)
74
76
  if (!_propertyAccess) Object.assign(data, keys)
@@ -112,6 +114,8 @@ module.exports = adapter => {
112
114
  })
113
115
  })
114
116
  .then(result => {
117
+ if (res.headersSent) return
118
+
115
119
  handleSapMessages(cdsReq, req, res)
116
120
 
117
121
  // case: read after write returns no results, e.g., due to auth (academic but possible)
@@ -447,7 +447,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
447
447
  _resolveAliasesInXpr(ref[i].where, current)
448
448
  _processWhere(ref[i].where, current)
449
449
  }
450
- } else if (current.kind === 'element' && current.elements && i < ref.length - 1) {
450
+ } else if (current.kind === 'element' && current.type !== 'cds.Map' && current.elements && i < ref.length - 1) {
451
451
  // > structured
452
452
  continue
453
453
  } else {
@@ -474,11 +474,16 @@ function _processSegments(from, model, namespace, cqn, protocol) {
474
474
  Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
475
475
 
476
476
  // if we end up with structured, keep path as is, if we end up with property in structured, cut off property
477
- if (!current.elements) from.ref.splice(-1)
477
+ if (!current.elements || current.type === 'cds.Map') from.ref.splice(-1)
478
478
  break
479
479
  } else if (Object.keys(target.elements).includes(current.name)) {
480
480
  if (!cqn.SELECT.columns) cqn.SELECT.columns = []
481
- cqn.SELECT.columns.push({ ref: ref.slice(i) })
481
+ const propRef = ref.slice(i)
482
+ if (propRef[0].where?.length === 0) {
483
+ const msg = 'Parentheses are not allowed when addressing properties.'
484
+ throw Object.assign(new Error(msg), { statusCode: 400 })
485
+ }
486
+ cqn.SELECT.columns.push({ ref: propRef })
482
487
 
483
488
  // we need the keys to generate the correct @odata.context
484
489
  for (const key in target.keys || {}) {
@@ -650,7 +655,7 @@ const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
650
655
  function _validateXpr(xpr, target, isOne, model, aliases = []) {
651
656
  if (!xpr) return []
652
657
 
653
- const ignoredColumns = Object.values(target.elements ?? {})
658
+ const ignoredColumns = Object.values(target?.elements ?? {})
654
659
  .filter(element => element['@cds.api.ignore'] && !element.isAssociation)
655
660
  .map(element => element.name)
656
661
  const _aliases = []
@@ -74,7 +74,7 @@
74
74
  (col.ref && exp.ref && col.ref.join('') === exp.ref.join(''))
75
75
  const _remapFunc = columns => c => {
76
76
  if (Array.isArray(c)) return c.map(_remapFunc(columns))
77
- const fnObj = c.ref && columns.find(col => col.as && col.func && col.as === c.ref[0])
77
+ const fnObj = c.ref && columns.find(col => 'func' in col && col.as && col.as === c.ref[0])
78
78
  if (fnObj) return fnObj
79
79
  return c
80
80
  }
@@ -175,12 +175,11 @@
175
175
  return columns
176
176
  }, [])
177
177
  : aggregatedColumns
178
- if (cqn.where) {
179
- cqn.where = cqn.where.map(_remapFunc(cqn.columns))
180
- if (cqn.groupBy) {
181
- cqn.having = cqn.where.map(_replaceNullRef(cqn.groupBy))
182
- delete cqn.where
183
- }
178
+ if (cqn.where && cqn.groupBy) {
179
+ cqn.having = cqn.where
180
+ .map(_remapFunc(cqn.columns))
181
+ .map(_replaceNullRef(cqn.groupBy))
182
+ delete cqn.where
184
183
  }
185
184
  // expand navigation refs in aggregated columns
186
185
  cqn.columns = cqn.columns.reduce((columns, col) => {
@@ -823,7 +822,7 @@
823
822
  // / mathCalc - needs CAP support
824
823
  )
825
824
  func:aggregateWith? aggregateFrom? as:asAlias?
826
- { return { func, args: [ path ], as } }
825
+ { return { func, args: [ path ], as: as ?? path.ref[0] } }
827
826
  / identifier OPEN aggregateExpr CLOSE // needs CAP support
828
827
  // / customAggregate // needs CAP support
829
828
  aggregateWith
@@ -138,7 +138,6 @@ const _parseStream = async function* (body, boundary) {
138
138
 
139
139
  if (typeof ret !== 'number') {
140
140
  if (ret.message === 'Parse Error') {
141
- // console.trace(ret, ret.bytesParsed ? `\n\nin:\n${changed.substr(0, ret.bytesParsed + 1)}\n\n` : '')
142
141
  ret.statusCode = 400
143
142
  ret.message = `Error while parsing batch body at position ${ret.bytesParsed}: ${ret.reason}`
144
143
  }