@sap/cds 9.7.1 → 9.8.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 (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +1 -56
  3. package/_i18n/messages_en_US_saptrc.properties +1 -92
  4. package/eslint.config.mjs +4 -1
  5. package/lib/compile/cds-compile.js +1 -0
  6. package/lib/compile/for/direct_crud.js +23 -0
  7. package/lib/compile/for/lean_drafts.js +12 -0
  8. package/lib/compile/for/odata.js +1 -18
  9. package/lib/compile/to/edm.js +1 -0
  10. package/lib/compile/to/json.js +4 -2
  11. package/lib/env/defaults.js +1 -0
  12. package/lib/env/serviceBindings.js +15 -5
  13. package/lib/index.js +1 -1
  14. package/lib/log/cds-error.js +33 -20
  15. package/lib/req/spawn.js +2 -2
  16. package/lib/srv/bindings.js +6 -13
  17. package/lib/srv/cds.Service.js +8 -36
  18. package/lib/srv/protocols/hcql.js +19 -2
  19. package/lib/utils/cds-utils.js +25 -16
  20. package/lib/utils/tar-win.js +106 -0
  21. package/lib/utils/tar.js +23 -158
  22. package/libx/_runtime/common/generic/crud.js +8 -7
  23. package/libx/_runtime/common/generic/sorting.js +7 -3
  24. package/libx/_runtime/common/utils/resolveView.js +47 -40
  25. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -0
  26. package/libx/_runtime/fiori/lean-draft.js +11 -2
  27. package/libx/_runtime/messaging/kafka.js +6 -5
  28. package/libx/_runtime/messaging/service.js +3 -1
  29. package/libx/_runtime/remote/Service.js +3 -0
  30. package/libx/_runtime/remote/utils/client.js +2 -4
  31. package/libx/_runtime/remote/utils/query.js +4 -4
  32. package/libx/odata/middleware/batch.js +323 -339
  33. package/libx/odata/middleware/create.js +0 -5
  34. package/libx/odata/middleware/delete.js +0 -5
  35. package/libx/odata/middleware/operation.js +10 -8
  36. package/libx/odata/middleware/read.js +0 -10
  37. package/libx/odata/middleware/stream.js +1 -0
  38. package/libx/odata/middleware/update.js +0 -6
  39. package/libx/odata/parse/afterburner.js +47 -22
  40. package/libx/odata/parse/cqn2odata.js +6 -1
  41. package/libx/odata/parse/grammar.peggy +14 -2
  42. package/libx/odata/parse/multipartToJson.js +2 -1
  43. package/libx/odata/parse/parser.js +1 -1
  44. package/package.json +2 -2
@@ -139,9 +139,9 @@ const compat_function_factory = (api, srv, it) => cds.utils.deprecated (ns => {
139
139
  */
140
140
  class Service extends ReflectionAPI {
141
141
 
142
- /**
143
- * @param {string} name
144
- * @param {import('../core/linked-csn').LinkedCSN} model
142
+ /**
143
+ * @param {string} name
144
+ * @param {import('../core/linked-csn').LinkedCSN} model
145
145
  */
146
146
  constructor (name, model, options) { super()
147
147
  if (typeof name === 'object') [ model, options, name = _service_in(model) ] = [ name, model ]
@@ -191,49 +191,21 @@ class Service extends ReflectionAPI {
191
191
  if (this._resolve) return this._resolve
192
192
 
193
193
  const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
194
- const PERSISTENCE_TABLE = '@cds.persistence.table'
195
-
196
- const _isPersistenceTable = target =>
197
- Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
198
- const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
199
194
 
200
- this._resolve = (query, abortCondition) => {
195
+ this._resolve = query => {
201
196
  const ctx = cds.context
202
197
  const model = ctx?.model || this.model
203
- return resolveView(query, model, this, abortCondition || _defaultAbort(this))
198
+ return resolveView(query, model, this)
204
199
  }
205
200
 
206
- // REVISIT: Remove argument `skipForbiddenViewCheck` once we get rid of composition tree
207
- this._resolve.transitions = (query, abortCondition, skipForbiddenViewCheck) => {
201
+ // NOTE: used in lean-draft and odata stream middleware
202
+ this._resolve.transitions = query => {
208
203
  const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined
209
204
  const _tx = typeof tx === 'function' ? cds.context?.tx : this
210
205
  const event = query?.INSERT ? 'INSERT' : query?.UPDATE ? 'UPDATE' : query?.DELETE ? 'DELETE' : undefined
211
- return getTransition(target, _tx, skipForbiddenViewCheck, event, {
212
- abort: abortCondition ?? (this.isDatabaseService ? this.resolve._abortDB : _defaultAbort(this))
213
- })
206
+ return getTransition(target, _tx, null, event)
214
207
  }
215
208
 
216
- this._resolve.resolve4db = query => {
217
- return this.resolve(query, this, this.resolve.abortDB)
218
- }
219
-
220
- // REVISIT: Remove once we get rid of composition tree
221
- this._resolve.table = target => {
222
- if (target.query?._target && !_isPersistenceTable(target)) {
223
- return this.resolve.table(target.query._target)
224
- }
225
- return target
226
- }
227
-
228
- // REVISIT: Remove once we get rid of old db
229
- this._resolve.abortDB = target => {
230
- return !!(_isPersistenceTable(target) || !target.query?._target)
231
- }
232
-
233
- this._resolve.transitions4db = (query, skipForbiddenViewCheck) => {
234
- return this.resolve.transitions(query, this.resolve.abortDB, skipForbiddenViewCheck)
235
- }
236
-
237
209
  return this._resolve
238
210
  }
239
211
  }
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../index'), {inspect} = cds.utils
2
2
  const express = require('express')
3
+ const { pipeline } = require('node:stream/promises')
3
4
 
4
5
  const LOG = cds.log('hcql')
5
6
  const PROD = process.env.NODE_ENV === 'production'
@@ -69,7 +70,18 @@ class HCQLAdapter extends require('./http') {
69
70
  * The ultimate handler for all CRUD requests.
70
71
  */
71
72
  crud (req, res, next) {
72
- let query = this.query4 (req)
73
+ let query = this.query4(req)
74
+
75
+ if (query.stream && can_stream(req))
76
+ return this.service
77
+ .tx(() =>
78
+ query.stream().then(results => {
79
+ res.set('content-type', 'application/octet-stream')
80
+ return pipeline(results, res)
81
+ })
82
+ )
83
+ .catch(next)
84
+
73
85
  return this.service.run (query)
74
86
  .then (results => this.reply (results, res))
75
87
  .catch (next)
@@ -81,7 +93,9 @@ class HCQLAdapter extends require('./http') {
81
93
  * which is expected to be a plain CQN object or a CQL string.
82
94
  */
83
95
  query4 (/** @type express.Request */ req) {
84
- let q = req.body = cds.ql(req.body ?? {}) || this.error (400, 'Invalid query', { query: req.body })
96
+ let q = req.body = cds.ql(req.body ?? {})
97
+ if (!q.bind) this.error(400, 'Invalid query', { query: req.body })
98
+ q.bind(this.service)
85
99
  // handle request headers
86
100
  if (q.SELECT) {
87
101
  if (req.get('Accept-Language')) q.SELECT.localized = true
@@ -148,4 +162,7 @@ const ql_fragment = x => {
148
162
  }
149
163
  return x
150
164
  }
165
+ const can_stream = req =>
166
+ req.headers.accept?.split?.(',').find(h => h.split(';')[0].trim() === 'application/octet-stream')
167
+
151
168
  module.exports = HCQLAdapter
@@ -1,10 +1,6 @@
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
-
8
4
  exports = module.exports = new class {
9
5
  get colors() { return super.colors = require('./colors') }
10
6
  get inflect() { return super.inflect = require('./inflect') }
@@ -17,9 +13,19 @@ exports = module.exports = new class {
17
13
  const {format} = require('node:util')
18
14
  return super.format = format
19
15
  }
16
+ get yaml() {
17
+ const yaml = require('js-yaml')
18
+ return super.yaml = Object.assign(yaml,{parse:yaml.load})
19
+ }
20
+ get tar() {
21
+ if (process.platform === 'win32') try { require.resolve('tar')
22
+ return super.tar = require('./tar-lib')
23
+ } catch {
24
+ return super.tar = require('./tar-win')
25
+ }
26
+ else return super.tar = require('./tar')
27
+ }
20
28
  get uuid() { return super.uuid = require('crypto').randomUUID }
21
- get yaml() { const yaml = require('js-yaml'); return super.yaml = Object.assign(yaml,{parse:yaml.load}) }
22
- get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
23
29
  get semver() { return super.semver = require('./version') }
24
30
  }
25
31
 
@@ -309,17 +315,20 @@ exports.csv = require('./csv-reader')
309
315
  * Loads a file through ESM or CommonJs.
310
316
  * @returns { Promise<any> }
311
317
  */
312
- // TODO find a better place.
313
- exports._import = id => {
314
- try {
315
- return require(id) // try CommonJS first
316
- } catch (err) {
317
- if (err.code !== 'ERR_REQUIRE_ESM') throw err
318
- // else try w/ ESM
319
- const { pathToFileURL } = require('url')
320
- return import (pathToFileURL(id).href) // must use a file: URL, esp. on Windows for C:\... paths
318
+ exports._import = id => { // TODO find a better place.
319
+ if (id.endsWith('.mjs')) return _import (id)
320
+ if (id.endsWith('.cjs')) return require (id)
321
+ else try { return require(id) } catch (err) {
322
+ if (err.message === 'Cannot use import statement outside a module') return _import (id) // for jest
323
+ if (err.code === 'ERR_REQUIRE_ESM') return _import (id)
324
+ else throw err
321
325
  }
322
326
  }
327
+ const _import = process.platform === 'win32' ? (()=>{
328
+ const url = require('url') // On Windows we must use a file: URL, esp. for C:\... paths
329
+ return id => import (url.pathToFileURL(id).href)
330
+ })() : id => import (id)
331
+
323
332
 
324
333
  const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i
325
334
  /**
@@ -349,7 +358,7 @@ exports.redacted = function _redacted(cred) {
349
358
 
350
359
 
351
360
  /**
352
- * A variant of child_process.exec that returns a promise,
361
+ * A variant of child_process.exec that returns a promise,
353
362
  * which resolves with the command's stdout split into lines.
354
363
  * @example
355
364
  * await cds.utils.sh `npm ls -lp --depth=0`
@@ -0,0 +1,106 @@
1
+ // This module monkey patches ./tar.js to work on Windows, where tar does not work properly w/o these changes.
2
+
3
+ exports = module.exports = require('./tar')
4
+
5
+ exports._spawn_tar_c = (dir, args) => {
6
+ args.push('.')
7
+ if (Array.isArray(args[0])) return winSpawnTempDir(dir, args)
8
+ else return winSpawnDir(dir, args)
9
+ }
10
+
11
+ exports._path = path => {
12
+ if (!path) return path
13
+ if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
14
+ if (Array.isArray(path)) return path.map(el => exports._path(el))
15
+ }
16
+
17
+
18
+ const child_process = require('child_process')
19
+ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
20
+ Error.captureStackTrace(spawn,spawn)
21
+ process.stderr.write(cmd +' ', args.join(' ') +' '+ spawn.stack.slice(7) + '\n')
22
+ return child_process.spawn(cmd, args, options)
23
+ } : child_process.spawn
24
+
25
+ const cds = require('../index'), { fs, path, exists, rimraf } = cds.utils
26
+ const { PassThrough } = require('stream')
27
+
28
+
29
+
30
+ // spawn tar on Windows, using the cli version
31
+ const winSpawnDir = (dir, args) => {
32
+ if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', exports._path(dir), ...exports._path(args)])
33
+ else return spawn ('tar', ['cf', '-', '-C', exports._path(dir), ...exports._path(args)])
34
+ }
35
+
36
+ // copy a directory recursively on Windows, using fs.promises
37
+ async function winCopyDir(src, dest) {
38
+ if ((await fs.promises.stat(src)).isDirectory()) {
39
+ const entries = await fs.promises.readdir(src)
40
+ return Promise.all(entries.map(async each => winCopyDir(path.join(src, each), path.join(dest, each))))
41
+ } else {
42
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true })
43
+ return fs.promises.copyFile(src, dest)
44
+ }
45
+ }
46
+
47
+ // copy resources containing files and folders to temp dir on Windows
48
+ // cli tar has a size limit on Windows
49
+ const winCreateTemp = async (root, resources) => {
50
+ // Asynchronously copies the entire content from src to dest.
51
+ const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
52
+ for (let resource of resources) {
53
+ const destination = path.join(temp, path.relative(root, resource))
54
+ if ((await fs.promises.stat(resource)).isFile()) {
55
+ const dirName = path.dirname(destination)
56
+ if (!await exists(dirName)) {
57
+ await fs.promises.mkdir(dirName, { recursive: true })
58
+ }
59
+ await fs.promises.copyFile(resource, destination)
60
+ } else {
61
+ if (fs.promises.cp) {
62
+ await fs.promises.cp(resource, destination, { recursive: true })
63
+ } else {
64
+ // node < 16
65
+ await winCopyDir(resource, destination)
66
+ }
67
+ }
68
+ }
69
+
70
+ return temp
71
+ }
72
+
73
+ // spawn tar on Windows, using a temp dir, which is copied from the original dir
74
+ // cli tar has a size limit on Windows
75
+ const winSpawnTempDir = (dir, args) => {
76
+ // Synchronous trick: use a PassThrough as placeholder
77
+ const stdout = new PassThrough()
78
+ const stderr = new PassThrough()
79
+ const c = {
80
+ stdout,
81
+ stderr,
82
+ on: (...a) => { stdout.on(...a); stderr.on(...a); return c },
83
+ once: (...a) => { stdout.once(...a); stderr.once(...a); return c },
84
+ kill: () => {},
85
+ }
86
+
87
+ // async copy, then swap streams/events
88
+ winCreateTemp(dir, args.shift()).then(tempPath => {
89
+ const real = winSpawnDir(tempPath, args)
90
+ real.stdout.pipe(stdout)
91
+ real.stderr && real.stderr.pipe(stderr)
92
+ const cleanup = () => exists(tempPath) && rimraf(tempPath)
93
+ real.on('close', (...ev) => {
94
+ stdout.emit('close', ...ev)
95
+ stderr.emit('close', ...ev)
96
+ cleanup()
97
+ })
98
+ real.on('error', (...ev) => {
99
+ stdout.emit('error', ...ev)
100
+ stderr.emit('error', ...ev)
101
+ cleanup()
102
+ })
103
+ c.kill = (...ev) => real.kill(...ev)
104
+ })
105
+ return c
106
+ }
package/lib/utils/tar.js CHANGED
@@ -1,4 +1,3 @@
1
- const { PassThrough } = require('stream')
2
1
  const child_process = require('child_process')
3
2
  const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
4
3
  Error.captureStackTrace(spawn,spawn)
@@ -6,137 +5,9 @@ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
6
5
  return child_process.spawn(cmd, args, options)
7
6
  } : child_process.spawn
8
7
 
9
- const cds = require('../index'), { fs, path, mkdirp, exists, rimraf } = cds.utils
8
+ const cds = require('../index'), { fs, path, mkdirp } = cds.utils
10
9
  const _resolve = (...x) => path.resolve (cds.root,...x)
11
10
 
12
- // ======= ONLY_FOR_WINDOWS ======
13
- // This section contains logic relevant for Windows OS.
14
-
15
- // tar does not work properly on Windows w/o this change
16
- const win = path => {
17
- if (!path) return path
18
- if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
19
- if (Array.isArray(path)) return path.map(el => win(el))
20
- }
21
-
22
- // spawn tar on Windows, using the cli version
23
- const winSpawnDir = (dir, args) => {
24
- if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
25
- else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
26
- }
27
-
28
- // copy a directory recursively on Windows, using fs.promises
29
- async function winCopyDir(src, dest) {
30
- if ((await fs.promises.stat(src)).isDirectory()) {
31
- const entries = await fs.promises.readdir(src)
32
- return Promise.all(entries.map(async each => winCopyDir(path.join(src, each), path.join(dest, each))))
33
- } else {
34
- await fs.promises.mkdir(path.dirname(dest), { recursive: true })
35
- return fs.promises.copyFile(src, dest)
36
- }
37
- }
38
-
39
- // copy resources containing files and folders to temp dir on Windows
40
- // cli tar has a size limit on Windows
41
- const winCreateTemp = async (root, resources) => {
42
- // Asynchronously copies the entire content from src to dest.
43
- const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
44
- for (let resource of resources) {
45
- const destination = path.join(temp, path.relative(root, resource))
46
- if ((await fs.promises.stat(resource)).isFile()) {
47
- const dirName = path.dirname(destination)
48
- if (!await exists(dirName)) {
49
- await fs.promises.mkdir(dirName, { recursive: true })
50
- }
51
- await fs.promises.copyFile(resource, destination)
52
- } else {
53
- if (fs.promises.cp) {
54
- await fs.promises.cp(resource, destination, { recursive: true })
55
- } else {
56
- // node < 16
57
- await winCopyDir(resource, destination)
58
- }
59
- }
60
- }
61
-
62
- return temp
63
- }
64
-
65
- // spawn tar on Windows, using a temp dir, which is copied from the original dir
66
- // cli tar has a size limit on Windows
67
- const winSpawnTempDir = (dir, args) => {
68
- // Synchronous trick: use a PassThrough as placeholder
69
- const stdout = new PassThrough()
70
- const stderr = new PassThrough()
71
- const c = {
72
- stdout,
73
- stderr,
74
- on: (...a) => { stdout.on(...a); stderr.on(...a); return c },
75
- once: (...a) => { stdout.once(...a); stderr.once(...a); return c },
76
- kill: () => {},
77
- }
78
-
79
- // async copy, then swap streams/events
80
- winCreateTemp(dir, args.shift()).then(tempPath => {
81
- const real = winSpawnDir(tempPath, args)
82
- real.stdout.pipe(stdout)
83
- real.stderr && real.stderr.pipe(stderr)
84
- const cleanup = () => exists(tempPath) && rimraf(tempPath)
85
- real.on('close', (...ev) => {
86
- stdout.emit('close', ...ev)
87
- stderr.emit('close', ...ev)
88
- cleanup()
89
- })
90
- real.on('error', (...ev) => {
91
- stdout.emit('error', ...ev)
92
- stderr.emit('error', ...ev)
93
- cleanup()
94
- })
95
- c.kill = (...ev) => real.kill(...ev)
96
- })
97
- return c
98
- }
99
-
100
- // ====== END ONLY_FOR_WINDOWS ======
101
-
102
- const tarInfo = async (info) => {
103
- let cmd, param
104
- if (info === 'version') {
105
- cmd = 'tar'
106
- param = ['--version']
107
- } else {
108
- cmd = process.platform === 'win32' ? 'where' : 'which'
109
- param = ['tar']
110
- }
111
-
112
- const c = spawn (cmd, param)
113
-
114
- return {__proto__:c,
115
- then (resolve, reject) {
116
- let data=[], stderr=''
117
- c.stdout.on('data', d => {
118
- data.push(d)
119
- })
120
- c.stderr.on('data', d => stderr += d)
121
- c.on('close', code => {
122
- code ? reject(new Error(stderr)) : resolve(Buffer.concat(data).toString().replace(/\n/g,'').replace(/\r/g,''))
123
- })
124
- c.on('error', reject)
125
- }
126
- }
127
- }
128
-
129
- const logDebugTar = async () => {
130
- const LOG = cds.log('tar')
131
- if (!LOG?._debug) return
132
- try {
133
- LOG (`tar path: ${await tarInfo('path')}`)
134
- LOG (`tar version: ${await tarInfo('version')}`)
135
- } catch (err) {
136
- LOG('tar error', err)
137
- }
138
- }
139
-
140
11
  /**
141
12
  * Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
142
13
  * @example ```js
@@ -162,26 +33,12 @@ const logDebugTar = async () => {
162
33
  * - `.to()` is a convenient shortcut to pipe the output into a write stream
163
34
  */
164
35
  exports.create = (dir='.', ...args) => {
165
- logDebugTar()
36
+
166
37
  if (typeof dir === 'string') dir = _resolve(dir)
167
38
  if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
168
-
169
- let c
170
39
  args = args.filter(el => el)
171
- if (process.platform === 'win32') {
172
- args.push('.')
173
- if (Array.isArray(args[0])) c = winSpawnTempDir(dir, args)
174
- else c = winSpawnDir(dir, args)
175
- } else {
176
- if (Array.isArray(args[0])) {
177
- args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
178
- } else {
179
- args.push('.')
180
- }
181
-
182
- c = spawn ('tar', ['c', '-C', dir, ...args], { env: { COPYFILE_DISABLE: 1 }})
183
- }
184
40
 
41
+ const c = exports._spawn_tar_c (dir, args)
185
42
  return {__proto__:c, // returning a thenable + fluent ChildProcess...
186
43
 
187
44
  /**
@@ -219,6 +76,22 @@ exports.create = (dir='.', ...args) => {
219
76
  }
220
77
  }
221
78
 
79
+
80
+ // Extracted to allow os-specific implementations, e.g. for win32
81
+ exports._spawn_tar_c = (dir, args) => {
82
+ if (Array.isArray(args[0])) {
83
+ args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
84
+ } else {
85
+ args.push ('.')
86
+ }
87
+ return spawn ('tar', ['c', '-C', dir, ...args], { env: { COPYFILE_DISABLE: 1 }})
88
+ }
89
+
90
+
91
+ // Extracted to allow os-specific implementations, e.g. for win32
92
+ exports._path = p => p
93
+
94
+
222
95
  /**
223
96
  * Extracts a tar archive, from an in-memory Buffer, or piped from a read stream or file.
224
97
  * @example ```js
@@ -242,7 +115,7 @@ exports.extract = (archive, ...args) => ({
242
115
  to (...dest) {
243
116
  if (typeof dest === 'string') dest = _resolve(...dest)
244
117
  const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
245
- const x = spawn('tar', ['xf', win(input), '-C', win(dest), ...args])
118
+ const x = spawn('tar', ['xf', exports._path(input), '-C', exports._path(dest), ...args])
246
119
  if (archive === '-') return x.stdin
247
120
  if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
248
121
  if (typeof archive !== 'string') (archive.stdout || archive) .pipe (x.stdin)
@@ -251,8 +124,8 @@ exports.extract = (archive, ...args) => ({
251
124
  x.stderr.on ('data', d => stderr += d)
252
125
  return {__proto__:x,
253
126
  then (resolve, reject) {
254
- x.on('close', code => {
255
- if (code) return reject (new Error(stderr))
127
+ x.on('close', err => {
128
+ if (err) return reject (new Error(stderr))
256
129
  if (process.platform === 'linux') stdout = stderr
257
130
  resolve (stdout ? stdout.split('\n').slice(0,-1).map(x => x.replace(/^x |\r/g,'')): undefined)
258
131
  })
@@ -304,12 +177,4 @@ exports.t = tar.tf = tar.list
304
177
  * @example fs.createReadStream('t.tar') .pipe (tar.x.to('dest/dir'))
305
178
  * @returns `stdin` of the tar child process
306
179
  */
307
- exports.extract.to = function (..._) { return this('-').to(..._) }
308
-
309
-
310
-
311
- // ---------------------------------------------------------------------------------
312
- // Compatibility...
313
-
314
- exports.packTarArchive = (resources,d) => d ? tar.cz (d,resources) : tar.cz (resources)
315
- exports.unpackTarArchive = (x,dir) => tar.xz(x).to(dir)
180
+ exports.extract.to = function (..._) { return this('-').to(..._) }
@@ -11,19 +11,19 @@ module.exports = cds.service.impl(function () {
11
11
  this.on(['CREATE', 'READ', 'UPDATE', 'UPSERT', 'DELETE'], '*', async function handle_crud_requests(req) {
12
12
 
13
13
  if (!cds.db)
14
- return req.reject ('NO_DATABASE_CONNECTION') // REVISIT: error message
14
+ return req.reject('NO_DATABASE_CONNECTION') // REVISIT: error message
15
15
 
16
16
  if (!req.query)
17
- return req.reject (501, 'The request has no query and cannot be served generically.')
17
+ return req.reject(501, 'The request has no query and cannot be served generically.')
18
18
 
19
19
  if (typeof req.query !== 'string' && req.target?._hasPersistenceSkip)
20
- return req.reject (501, `Entity "${req.target.name}" is annotated with "@cds.persistence.skip" and cannot be served generically.`)
20
+ return req.reject(501, `Entity "${req.target.name}" is annotated with "@cds.persistence.skip" and cannot be served generically.`)
21
21
 
22
22
  // validate that all elements in path exist on db, if necessary
23
23
  // - INSERT has no where clause to do this in one roundtrip
24
24
  // - SELECT returns [] -> really empty collection or invalid path?
25
25
  const subject = req.query.INSERT?.into || req.query.SELECT?.from
26
- const pathExistsQuery = subject?.ref?.length > 1 && SELECT(1).from({ ref: subject.ref.slice(0,-1) })
26
+ const pathExistsQuery = subject?.ref?.length > 1 && SELECT(1).from({ ref: subject.ref.slice(0, -1) })
27
27
 
28
28
  if (req.event === 'CREATE' && pathExistsQuery) {
29
29
  // REVISIT: Why dont't we just run the insert and check affected rows?
@@ -33,10 +33,10 @@ module.exports = cds.service.impl(function () {
33
33
 
34
34
  if (req.event in { DELETE: 1, UPDATE: 1 } && req.target?._isSingleton) {
35
35
  if (req.event === 'DELETE' && !req.target['@odata.singleton.nullable'])
36
- return req.reject (400, 'SINGLETON_NOT_NULLABLE')
36
+ return req.reject(400, 'SINGLETON_NOT_NULLABLE')
37
37
 
38
38
  const selectSingleton = SELECT.one(req.target)
39
- const keyColumns = [...(req.target.keys||[])].filter(e => !e.isAssociation).map(e => e.name)
39
+ const keyColumns = [...(req.target.keys || [])].filter(e => !e.isAssociation).map(e => e.name)
40
40
 
41
41
  // if no keys available, select all columns so we can delete the singleton with same content
42
42
  if (keyColumns.length) selectSingleton.columns(keyColumns)
@@ -54,7 +54,8 @@ module.exports = cds.service.impl(function () {
54
54
 
55
55
  if (req.event === 'READ' && req.query?.SELECT && req.locale) req.query.SELECT.localized ??= true
56
56
 
57
- const result = await cds.run (req.query, req.data)
57
+ // REVISIT for cds^10: can we always use cds.db.dispatch(req)?
58
+ const result = req.iterator && !req.objectMode ? await cds.db.dispatch(req) : await cds.run(req.query, req.data)
58
59
 
59
60
  if (req.event === 'READ') {
60
61
  // do not execute additional select to distinguish between 412 and 404
@@ -50,6 +50,12 @@ const _addDefaultSortOrder = (req, select) => {
50
50
  )
51
51
  }
52
52
 
53
+ const containsAggregation = column => {
54
+ if (column.func) return true
55
+ if (column.xpr) return column.xpr.some(xpr => containsAggregation(xpr))
56
+ return false
57
+ }
58
+
53
59
  /**
54
60
  * 1. query options --> already set in req.query
55
61
  * 2. orders from view
@@ -63,9 +69,7 @@ const handle_sorting = function (req) {
63
69
  let select = req.query.SELECT
64
70
 
65
71
  // do not sort for /$count queries or queries only using aggregations
66
- if (select.columns && select.columns.length && select.columns.every(col => col.func)) {
67
- return
68
- }
72
+ if (select.columns?.length && select.columns.every(col => containsAggregation(col))) return
69
73
 
70
74
  if (select.from && select.from.SELECT) {
71
75
  // add default sort to root query