@sap/cds 8.7.2 → 8.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 (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/_i18n/i18n.properties +3 -0
  3. package/_i18n/i18n_cs.properties +6 -6
  4. package/_i18n/i18n_de.properties +3 -0
  5. package/_i18n/i18n_en.properties +3 -0
  6. package/_i18n/i18n_es.properties +3 -0
  7. package/_i18n/i18n_fr.properties +3 -0
  8. package/_i18n/i18n_it.properties +3 -0
  9. package/_i18n/i18n_ja.properties +3 -0
  10. package/_i18n/i18n_pl.properties +7 -4
  11. package/_i18n/i18n_pt.properties +3 -0
  12. package/_i18n/i18n_ru.properties +3 -0
  13. package/app/index.js +2 -30
  14. package/lib/compile/parse.js +1 -1
  15. package/lib/env/cds-env.js +1 -1
  16. package/lib/env/cds-requires.js +16 -9
  17. package/lib/env/schemas/cds-package.js +1 -1
  18. package/lib/env/schemas/cds-rc.js +17 -4
  19. package/lib/index.js +1 -1
  20. package/lib/ql/SELECT.js +6 -1
  21. package/lib/ql/cds.ql-predicates.js +2 -1
  22. package/lib/req/request.js +5 -2
  23. package/lib/req/validate.js +4 -2
  24. package/lib/srv/bindings.js +31 -20
  25. package/lib/srv/cds-connect.js +1 -1
  26. package/lib/srv/middlewares/auth/mocked-users.js +1 -0
  27. package/lib/srv/protocols/okra.js +5 -7
  28. package/lib/srv/srv-dispatch.js +0 -5
  29. package/lib/test/cds-test.js +34 -6
  30. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -0
  31. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  32. package/libx/_runtime/common/generic/auth/service.js +2 -2
  33. package/libx/_runtime/common/generic/auth/utils.js +2 -1
  34. package/libx/_runtime/common/generic/input.js +1 -1
  35. package/libx/_runtime/common/utils/binary.js +1 -35
  36. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -8
  37. package/libx/_runtime/fiori/lean-draft.js +2 -4
  38. package/libx/common/utils/path.js +1 -5
  39. package/libx/common/utils/streaming.js +76 -0
  40. package/libx/odata/middleware/create.js +5 -1
  41. package/libx/odata/middleware/delete.js +1 -1
  42. package/libx/odata/middleware/operation.js +48 -4
  43. package/libx/odata/middleware/read.js +1 -1
  44. package/libx/odata/middleware/stream.js +29 -101
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +21 -1
  47. package/libx/odata/parse/grammar.peggy +108 -26
  48. package/libx/odata/parse/multipartToJson.js +17 -10
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/metadata.js +28 -5
  51. package/libx/odata/utils/normalizeTimeData.js +11 -8
  52. package/libx/rest/RestAdapter.js +2 -16
  53. package/libx/rest/middleware/operation.js +38 -18
  54. package/libx/rest/middleware/parse.js +5 -25
  55. package/libx/rest/post-processing.js +33 -0
  56. package/libx/rest/pre-processing.js +38 -0
  57. package/package.json +1 -1
  58. package/libx/common/utils/index.js +0 -5
package/CHANGELOG.md CHANGED
@@ -4,6 +4,56 @@
4
4
  - The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## Version 8.8.1 - 2025-03-07
8
+
9
+ ### Fixed
10
+
11
+ - Requests violating `cds.odata.max_batch_header_size` are terminated with `431 Request Header Fields Too Large` instead of `400 - Bad Request`
12
+ - `cds.parse.<x>` writing directly to `stdout`
13
+ - Instance-based authorization for programmatic action invocations
14
+ - Implicit function parameter calls with Array or Object values
15
+ - OData: Throw an error by `POST` with payload that contains array of entity representation
16
+ - `cds.validate` filters out annotations according to OData V4 spec
17
+ - Crash for requests with invalid time data format
18
+ - Add missing 'and' between conditions in object notation of QL
19
+ - Multiline payloads in `$batch` sub requests
20
+ - Instance-based authorization for modeling like `$user.<property> is null`
21
+ - Respect `cds.odata.contextAbsoluteUrl` in new OData adapter
22
+ - `cds.odata.context_with_columns` also applies to singletons
23
+
24
+ ## Version 8.8.0 - 2025-03-03
25
+
26
+ ### Added
27
+
28
+ - `cds.ql` method `SELECT.hints()` which passes hints to the database query optimizer that can influence the execution plan
29
+ - Schema updates for MTX configuration
30
+ - Deprecate `cds.requires.db.database` in JSON schema
31
+ - Service level restrictions for application service calls can be enforced with `cds.features.service_level_restrictions=true`
32
+ + With `@sap/cds^9`, this becomes the new default.
33
+ - Support implicit function parameters calls with @prefix
34
+ - `cds.test` now uses package `@cap-js/cds-test` if installed, otherwise prints a hint to install it. With cds 9, this package will be required.
35
+ - Operation response streaming
36
+ + OData: Operations returning `cds.LargeBinary` annotated with `@Core.MediaType` may send stream responses.
37
+ + REST: Operations may send stream responses.
38
+ + Annotations `@Core.MediaType`, `@Core.ContentDisposition.Filename` and `@Core.ContentDisposition.Type` on operation return types will be considered.
39
+
40
+ ### Changed
41
+
42
+ - The default index page now shows links to CDS functions with their parameter names but no default values anymore.
43
+
44
+ ### Fixed
45
+
46
+ - Order by virtual fields in draft-related requests
47
+ - Erroneous cleansing when draft activation is invoked programmatically
48
+ - Skip validation for mandatory fields in update scenarios for entities in draft activation
49
+ - Simplified default configuration: `cds.requires.messaging = true`
50
+ - `cds.connect` called with options erroneously filled in `cds.services`
51
+ - Mocked users won't have a tenant in single-tenant mode
52
+ - Allow usage of latest versions of `chai` and `chai-as-promised` on Node >= 23 with the built-in test runner and `mocha`. The `jest` runner is not able though to load these ESM modules.
53
+ - Reject navigations in expand
54
+ - Activation of drafts for entities using `@cds.api.ignore`
55
+ - Prevent uncaught type error during validation of composition entries
56
+
7
57
  ## Version 8.7.2 - 2025-02-14
8
58
 
9
59
  ### Fixed
@@ -70,6 +70,9 @@ LanguageCode=Language Code
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Language code as specified by ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Time Zone Code
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=User ID
75
78
 
@@ -26,16 +26,16 @@
26
26
  #----------------------------------------------------------------------------------------------------------------------
27
27
 
28
28
  #XTIT: Created By (Answer to: "Which user has created a certain entity?")
29
- CreatedBy=Vytvo\u0159il(a)
29
+ CreatedBy=Vytvo\u0159il
30
30
 
31
31
  #XTIT: Created On (Answer to: "When has a certain entity been created?")
32
- CreatedAt=Vytvo\u0159eno dne
32
+ CreatedAt=Vytvo\u0159eno
33
33
 
34
34
  #XTIT: Changed By (Answer to: "Which user has changed a certain entity?")
35
- ChangedBy=Autor zm\u011Bny
35
+ ChangedBy=Zm\u011Bnil
36
36
 
37
37
  #XTIT: Changed On (Answer to: "When has a certain entity been changed?")
38
- ChangedAt=Zm\u011Bn\u011Bno dne
38
+ ChangedAt=Zm\u011Bn\u011Bno
39
39
 
40
40
  #XTIT: Currency
41
41
  Currency=M\u011Bna
@@ -92,7 +92,7 @@ Draft_DraftAdministrativeData=Spr\u00E1vn\u00ED data n\u00E1vrhu
92
92
  Draft_DraftUUID=N\u00E1vrh (technick\u00E9 ID)
93
93
 
94
94
  #XTIT: Creation time of a draft
95
- Draft_CreationDateTime=N\u00E1vrh vytvo\u0159en dne
95
+ Draft_CreationDateTime=N\u00E1vrh vytvo\u0159en
96
96
 
97
97
  #XTIT: User created the draft
98
98
  Draft_CreatedByUser=N\u00E1vrh vytvo\u0159il
@@ -101,7 +101,7 @@ Draft_CreatedByUser=N\u00E1vrh vytvo\u0159il
101
101
  Draft_DraftIsCreatedByMe=N\u00E1vrh vytvo\u0159en mnou
102
102
 
103
103
  #XTIT: Time a draft was last changed on
104
- Draft_LastChangeDateTime=N\u00E1vrh naposledy zm\u011Bn\u011Bn dne
104
+ Draft_LastChangeDateTime=N\u00E1vrh naposledy zm\u011Bn\u011Bn
105
105
 
106
106
  #XTIT: User that changed the draft last
107
107
  Draft_LastChangedByUser=N\u00E1vrh naposledy zm\u011Bnil
@@ -70,6 +70,9 @@ LanguageCode=Sprachencode
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Sprachencode gem\u00E4\u00DF ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Zeitzonencode
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=Benutzer-ID
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=Language Code
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Language code as specified by ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Time Zone Code
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=User ID
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=C\u00F3digo de idioma
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=C\u00F3digo de idioma seg\u00FAn especificado por ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=C\u00F3digo de zona horaria
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=ID de usuario
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=Code langue
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Code langue tel qu'indiqu\u00E9 par ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Code du fuseau horaire
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=ID utilisateur
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=Codice lingua
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Codice lingua come indicato da ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Codice fuso orario
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=ID utente
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=\u8A00\u8A9E\u30B3\u30FC\u30C9
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=ISO 639-1 \u3067\u6307\u5B9A\u3055\u308C\u3066\u3044\u308B\u8A00\u8A9E\u30B3\u30FC\u30C9
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u30B3\u30FC\u30C9
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=\u30E6\u30FC\u30B6 ID
75
78
 
@@ -29,13 +29,13 @@
29
29
  CreatedBy=Utworzone przez
30
30
 
31
31
  #XTIT: Created On (Answer to: "When has a certain entity been created?")
32
- CreatedAt=Utworzono dnia
32
+ CreatedAt=Data utworzenia
33
33
 
34
34
  #XTIT: Changed By (Answer to: "Which user has changed a certain entity?")
35
35
  ChangedBy=Zmienione przez
36
36
 
37
37
  #XTIT: Changed On (Answer to: "When has a certain entity been changed?")
38
- ChangedAt=Zmieniono dnia
38
+ ChangedAt=Data zmiany
39
39
 
40
40
  #XTIT: Currency
41
41
  Currency=Waluta
@@ -70,6 +70,9 @@ LanguageCode=Kod j\u0119zyka
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=Kod j\u0119zyka okre\u015Blony zgodnie z norm\u0105 ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=Kod strefy czasowej
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=Identyfikator u\u017Cytkownika
75
78
 
@@ -89,7 +92,7 @@ Draft_DraftAdministrativeData=Dane administracyjne wersji roboczej
89
92
  Draft_DraftUUID=Wersja robocza (identyfikator techniczny)
90
93
 
91
94
  #XTIT: Creation time of a draft
92
- Draft_CreationDateTime=Wersja robocza utworzona dnia
95
+ Draft_CreationDateTime=Data utworzenia wersji roboczej
93
96
 
94
97
  #XTIT: User created the draft
95
98
  Draft_CreatedByUser=Wersja robocza utworzona przez
@@ -98,7 +101,7 @@ Draft_CreatedByUser=Wersja robocza utworzona przez
98
101
  Draft_DraftIsCreatedByMe=Wersja robocza utworzona przeze mnie
99
102
 
100
103
  #XTIT: Time a draft was last changed on
101
- Draft_LastChangeDateTime=Wersja robocza ostatnio zmieniona dnia
104
+ Draft_LastChangeDateTime=Data ostatniej zmiany wersji roboczej
102
105
 
103
106
  #XTIT: User that changed the draft last
104
107
  Draft_LastChangedByUser=Wersja robocza ostatnio zmieniona przez
@@ -70,6 +70,9 @@ LanguageCode=C\u00F3digo de idioma
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=C\u00F3digo de idioma como especificado pelo ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=C\u00F3digo de fuso hor\u00E1rio
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=ID do usu\u00E1rio
75
78
 
@@ -70,6 +70,9 @@ LanguageCode=\u041A\u043E\u0434 \u044F\u0437\u044B\u043A\u0430
70
70
  #XTIT: Language Code Description
71
71
  LanguageCode.Description=\u041A\u043E\u0434 \u044F\u0437\u044B\u043A\u0430 \u0432 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 ISO 639-1
72
72
 
73
+ #XTIT Time zone code
74
+ TimeZoneCode=\u041A\u043E\u0434 \u0447\u0430\u0441\u043E\u0432\u043E\u0433\u043E \u043F\u043E\u044F\u0441\u0430
75
+
73
76
  #XTIT: User Identifier
74
77
  UserID=\u0418\u0434. \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
75
78
 
package/app/index.js CHANGED
@@ -36,7 +36,7 @@ module.exports = { get html(){
36
36
  <ul>${_operations_in(srv).map (e => `
37
37
  <li id="${asHtmlId(srv.name)}-${endpoint.kind}-${asHtmlId(e.name)}" class="operation">
38
38
  <div>
39
- <a href="${endpoint.path}/${e.name}${e.params}" title="${endpoint.path}/${e.name}${e.params}"><span>${e.name}()</span></a>
39
+ <a href="${endpoint.path}/${e.name}" title="${endpoint.path}/${e.name}"><span>${e.name} ${e.params}</span></a>
40
40
  </div>
41
41
  </li>`).join('')}
42
42
  </ul>
@@ -74,41 +74,13 @@ function _operations_in (service) {
74
74
  for (let name in operations) {
75
75
  const op = cds.model.definitions[service.name + '.' + name]
76
76
  if (op?.kind === 'function') {
77
- const params = '('+ Object.values(op.params||[]).map(p => {
78
- let val = _sampleValue(p)
79
- if (typeof val === 'string') val = encodeURIComponent(`'${val}'`)
80
- else if (typeof val === 'object') val = encodeURIComponent(`'${JSON.stringify(val)}'`)
81
- return `${p.name}=${val}`
82
- }).join(',') + ')'
77
+ const params = '('+ Object.keys(op.params||[]) + ')'
83
78
  exposed.push ({ name, params })
84
79
  }
85
80
  }
86
81
  return exposed
87
82
  }
88
83
 
89
- function _sampleValue (param) {
90
- if (param.items) // many
91
- return [ _sampleValue(param.items) ]
92
- if (param.elements) { // structured
93
- return Object.entries(param.elements).reduce((all,[n,p]) => {
94
- all[n] = _sampleValue(p)
95
- return all
96
- },{})
97
- }
98
- // scalar
99
- const type = param._type || param.type
100
- if (type === 'cds.String') return 'hello'
101
- if (type === 'cds.Boolean') return true
102
- if (type === 'cds.Decimal'||type === 'cds.Double') return '4.2'
103
- if (type === 'cds.Date') return '2021-12-31'
104
- if (type === 'cds.Time') return '23:42:42'
105
- if (type === 'cds.DateTime') return '2021-12-31T23:42:42Z'
106
- if (type === 'cds.Timestamp') return '2021-12-31T23:42:42.123Z'
107
- if (type === 'cds.UUID') return cds.utils.uuid()
108
- if (type?.match(/cds\..*Int.*/i)) return 42
109
- return type // fallback
110
- }
111
-
112
84
  function _moreLinks (srv, endpoint, entity, div=true) {
113
85
  return (srv.$linkProviders || [])
114
86
  .map (linkProv => linkProv(entity, endpoint))
@@ -29,7 +29,7 @@ exports.path = function path (x,...etc) {
29
29
  const [,head,tail] = /^([\w._]+)(?::(\w+))?$/.exec(x)||[]
30
30
  if (tail) return {ref:[head,...tail.split('.')]}
31
31
  if (head) return {ref:[head]}
32
- const {SELECT} = cdsc.parse.cql('SELECT from '+x)
32
+ const {SELECT} = cdsc.parse.cql('SELECT from '+x, undefined, { messages: [] })
33
33
  return SELECT.from
34
34
  }
35
35
 
@@ -255,7 +255,7 @@ class Config {
255
255
  if (!val || val._is_linked) return val
256
256
  if (val === true) {
257
257
  let x = kinds[key]
258
- if (x) val = x; else return val
258
+ if (x) val = x; else if (key+'-defaults' in kinds) val = {kind:key+'-defaults'}; else return val
259
259
  }
260
260
  if (typeof val === 'string') {
261
261
  let x = kinds[val] || kinds[val+'-'+key] || kinds[key+'-'+val]
@@ -130,6 +130,7 @@ const _resolve = (preferred, fallback) => {
130
130
 
131
131
  const _databases = {
132
132
 
133
+ "db-defaults": { kind: 'sql' },
133
134
  "sql": {
134
135
  '[development]': { kind: 'sqlite', credentials: { url: ':memory:' } },
135
136
  '[production]': { kind: 'hana' },
@@ -175,7 +176,6 @@ const _databases = {
175
176
  "label": "service-manager"
176
177
  }
177
178
  },
178
-
179
179
  }
180
180
 
181
181
 
@@ -197,15 +197,12 @@ const _outbox = {
197
197
 
198
198
  const _messaging = {
199
199
 
200
- "local-messaging": {
201
- impl: `${_runtime}/messaging/service.js`,
202
- local: true
203
- },
204
- "file-based-messaging": {
205
- impl: `${_runtime}/messaging/file-based.js`,
206
- file:'~/.cds-msg-box',
207
- outbox: true
200
+ "messaging-defaults": {
201
+ "[development]": { kind: "file-based-messaging" },
202
+ "[production]": { kind: "enterprise-messaging" },
203
+ "[hybrid]": { kind: "enterprise-messaging-amqp" },
208
204
  },
205
+
209
206
  "default-messaging": {
210
207
  "[development]": { kind: "local-messaging" },
211
208
  "[hybrid]": { kind: "enterprise-messaging-amqp" },
@@ -214,6 +211,16 @@ const _messaging = {
214
211
  "[multitenant]": { kind: "enterprise-messaging-http" }
215
212
  }
216
213
  },
214
+
215
+ "local-messaging": {
216
+ impl: `${_runtime}/messaging/service.js`,
217
+ local: true
218
+ },
219
+ "file-based-messaging": {
220
+ impl: `${_runtime}/messaging/file-based.js`,
221
+ file:'~/.cds-msg-box',
222
+ outbox: true
223
+ },
217
224
  "enterprise-messaging": {
218
225
  kind: "enterprise-messaging-http",
219
226
  },
@@ -20,7 +20,7 @@ module.exports = {
20
20
  type: 'object',
21
21
  additionalProperties: true,
22
22
  $ref: 'cdsJsonSchema://schemas/cds-rc.json#/$defs/cdsRoot',
23
- description: 'CDS configuration',
23
+ description: 'CDS configuration root',
24
24
  default: {}
25
25
  }
26
26
  }
@@ -19,6 +19,7 @@ module.exports = {
19
19
  {
20
20
  properties: {
21
21
  cds: {
22
+ description: 'CDS configuration root',
22
23
  $ref: '#/$defs/cdsRoot'
23
24
  }
24
25
  }
@@ -59,7 +60,8 @@ module.exports = {
59
60
  enum: [
60
61
  'mtx-sidecar',
61
62
  'with-mtx-sidecar',
62
- 'java'
63
+ 'java',
64
+ 'subscription-manager'
63
65
  ]
64
66
  },
65
67
  {
@@ -242,7 +244,9 @@ module.exports = {
242
244
  properties: {
243
245
  database: {
244
246
  type: 'string',
245
- format: 'uri-reference'
247
+ format: 'uri-reference',
248
+ deprecated: true,
249
+ description: 'Deprecated: Use \'url\' instead.'
246
250
  }
247
251
  }
248
252
  },
@@ -329,6 +333,10 @@ module.exports = {
329
333
  },
330
334
  messaging: {
331
335
  oneOf: [
336
+ {
337
+ type: 'boolean',
338
+ description: 'Shortcut to enable messaging.'
339
+ },
332
340
  {
333
341
  type: 'string',
334
342
  description: 'Settings for the primary messaging service (shortcut).',
@@ -425,7 +433,7 @@ module.exports = {
425
433
  type: 'boolean'
426
434
  },
427
435
  {
428
- $ref: '#/$defs/extensionRestrictions'
436
+ $ref: '#/$defs/extensibilitySettings'
429
437
  }
430
438
  ]
431
439
  },
@@ -802,11 +810,16 @@ module.exports = {
802
810
  ]
803
811
  },
804
812
 
805
- extensionRestrictions: {
813
+ extensibilitySettings: {
806
814
  type: 'object',
807
815
  description: 'Extensibility settings',
808
816
  additionalProperties: false,
809
817
  properties: {
818
+ 'check-existing-extensions': {
819
+ type: 'boolean',
820
+ description: `Specifies whether the extension linter includes existing extensions. Default will be 'true' with the next major release.`,
821
+ default: false
822
+ },
810
823
  'element-prefix': {
811
824
  type: 'array',
812
825
  description: 'Field names must start with one of these strings.',
package/lib/index.js CHANGED
@@ -95,7 +95,7 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
95
95
  get utils() { return super.utils = require('./utils/cds-utils') }
96
96
  get error() { return super.error = require('./log/cds-error') }
97
97
  get exec() { return super.exec = require('../bin/serve').exec }
98
- get test() { return super.test = require('./test/cds-test.js') }
98
+ get test() { return super.test = require('./test/cds-test') }
99
99
  get log() { return super.log = require('./log/cds-log') }
100
100
  get debug() { return super.debug = this.log.debug }
101
101
  clone(x) { return structuredClone(x) }
package/lib/ql/SELECT.js CHANGED
@@ -165,10 +165,15 @@ class SELECT extends Whereable {
165
165
  return this.then(rows => rows.map(callback))
166
166
  }
167
167
 
168
- valueOf() {
168
+ valueOf () {
169
169
  return super.valueOf('SELECT * FROM')
170
170
  }
171
171
 
172
+ hints (...args) {
173
+ if (args.length) this.SELECT.hints = args.flat()
174
+ return this
175
+ }
176
+
172
177
  /** @private */ get _target_ref(){ return this.SELECT.from }
173
178
 
174
179
  get elements() { return this.elements = cds.infer.elements4 (this.SELECT.columns, this.source) }
@@ -48,6 +48,8 @@ function _qbe (o, xpr=[]) {
48
48
  let count = 0
49
49
  for (let k in o) { const x = o[k]
50
50
 
51
+ if (k !== 'and' && k !== 'or' && count++) xpr.push('and') //> add 'and' between conditions
52
+
51
53
  if (k.startsWith('not ')) { xpr.push('not'); k = k.slice(4) }
52
54
  switch (k) { // handle special cases like {and:{...}} or {or:{...}}
53
55
  case 'between':
@@ -83,7 +85,6 @@ function _qbe (o, xpr=[]) {
83
85
  }
84
86
 
85
87
  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
88
  if (!x || typeof x !== 'object') xpr.push (a,'=',{val:x})
88
89
  else if (is_array(x)) xpr.push (a,'in',{list:x.map(_val)})
89
90
  else if (x.SELECT || x.list) xpr.push (a,'in',x)
@@ -1,5 +1,5 @@
1
+ const cds = require('../index'), {production} = cds.env
1
2
  const { Responses, Errors } = require('./response')
2
- const cds = require('../../lib'), {production} = cds.env
3
3
 
4
4
  /**
5
5
  * Class Request represents requests received via synchronous protocols.
@@ -93,7 +93,10 @@ class Request extends require('./event') {
93
93
  return super.subject = { ref }
94
94
  }
95
95
 
96
- reply (results) { return this.results = results }
96
+ reply (results, ...etc) {
97
+ if (etc.length) Object.assign (results, ...etc)
98
+ return this.results = results
99
+ }
97
100
  notify (...args) { return this._messages.add (1, ...args) }
98
101
  info (...args) { return this._messages.add (2, ...args) }
99
102
  warn (...args) { return this._messages.add (3, ...args) }
@@ -48,7 +48,7 @@ class Validation {
48
48
  }
49
49
 
50
50
  unknown(e,d,input) {
51
- if (e.startsWith('@')) return delete input[e] //> skip all annotations, like @odata.Type
51
+ if (e.match(/@.*\./)) return delete input[e] //> skip all annotations, like @odata.Type (according to OData spec annotations contain an "@" and a ".")
52
52
  d['@open'] || cds.error (`Property "${e}" does not exist in ${d.name}`, {status:400})
53
53
  }
54
54
  }
@@ -193,7 +193,9 @@ const $any = class any {
193
193
  class struct extends $any {
194
194
  validate (data, path, /** @type {Validation} */ ctx, elements = this.elements, skip={}) {
195
195
  const path_ = !path ? [] : [...path, this.name]; if (path?.row) path_.push({...path})
196
- if (data == null) return
196
+ if (data == null) return
197
+ // REVISIT cds^9: rm typeof data !== 'function', misuse in cds-mtxs
198
+ if (typeof data !== 'object' && typeof data !== 'function') return ctx.error ('ASSERT_DATA_TYPE', path_, this.name, data, this.target)
197
199
  // check for required elements in case of inserts -- note: null values are handled in the payload loop below
198
200
  if (ctx.insert || data && path_.length && this._is_insert(data)) for (let each of this._required (elements)) {
199
201
  if (each.name in data) continue // got value for required element
@@ -3,6 +3,7 @@ const cds = require ('..'), DEBUG = cds.debug('serve|bindings',{label:'cds'})
3
3
  const { readFile, readFileSync, writeFile, writeFileSync } = require ('fs')
4
4
  const [ read, write ] = [ readFile, writeFile ].map(require('util').promisify)
5
5
  const registry = '~/.cds-services.json'
6
+ const filename = registry.replace(/^~/, require('os').homedir())
6
7
 
7
8
  /** TODO: Add documentation */
8
9
  module.exports = class Bindings {
@@ -17,27 +18,29 @@ module.exports = class Bindings {
17
18
  return bindings.import() .then (r,e)
18
19
  }
19
20
 
20
- constructor(url) {
21
- this._source = require ('path') .resolve (cds.root, registry.replace(/^~/, require('os').homedir()))
22
- this.cds = {provides:{}}
23
- this.url = url
21
+ constructor() {
22
+ this.provides = {}
23
+ this.servers = {}
24
24
  }
25
25
 
26
26
  async load (sync) {
27
- DEBUG?.('reading bindings from:', this._source)
28
- try { Object.assign (this, JSON.parse (sync ? readFileSync (this._source) : await read (this._source))) }
27
+ DEBUG?.('reading bindings from:', registry)
28
+ try {
29
+ let {cds} = JSON.parse (sync ? readFileSync (filename) : await read (filename))
30
+ Object.assign (this,cds)
31
+ }
29
32
  catch { /* ignored */ }
30
33
  return this
31
34
  }
32
35
  async store (sync) {
33
- DEBUG?.('writing bindings to:', this._source)
34
- const json = JSON.stringify ({cds:this.cds},null,' ')
35
- return sync ? writeFileSync (this._source, json) : write (this._source, json)
36
+ DEBUG?.('writing bindings to:', registry)
37
+ const json = JSON.stringify ({cds:this},null,' ')
38
+ return sync ? writeFileSync (filename, json) : write (filename, json)
36
39
  }
37
40
 
38
41
  async import() {
39
42
  const required = cds.requires; if (!required) return this
40
- const provided = (await this.load()) .cds.provides
43
+ const provided = (await this.load()) .provides
41
44
  for (let each in required) {
42
45
  const req = required[each]; if (typeof req !== 'object') continue
43
46
  const bound = provided [req.service||each]
@@ -52,9 +55,14 @@ module.exports = class Bindings {
52
55
  }
53
56
 
54
57
  async export (services, url) {
55
- this.cleanup (this.url = url)
58
+ this.cleanup (url)
59
+ const { servers, provides } = this, { pid } = process
60
+ // register our server
61
+ servers[pid] = {
62
+ root: 'file://' + cds.root,
63
+ url
64
+ }
56
65
  // register our services
57
- const provides = this.cds.provides
58
66
  for (let each of services) {
59
67
  // if (each.name in cds.env.requires) continue
60
68
  const options = each.options || {}
@@ -63,7 +71,8 @@ module.exports = class Bindings {
63
71
  credentials: {
64
72
  ...options.credentials,
65
73
  url: url + each.path
66
- }
74
+ },
75
+ server: pid
67
76
  }
68
77
  }
69
78
  process.on ('exit', ()=>this.purge())
@@ -72,17 +81,19 @@ module.exports = class Bindings {
72
81
 
73
82
  purge() {
74
83
  this.load(true)
75
- DEBUG?.('purging bindings from:', this._source)
84
+ DEBUG?.('purging bindings from:', registry)
76
85
  this.cleanup()
77
86
  this.store(true)
78
87
  }
79
88
 
80
- cleanup (url=this.url) {
81
- // remove all services served at the same url
82
- const all = this.cds.provides
83
- for (let [key,srv] of Object.entries (all)) {
84
- if (srv.credentials && srv.credentials.url && srv.credentials.url.startsWith(url)) delete all [key]
85
- }
89
+ /**
90
+ * Remove all services served by this server or at the given url.
91
+ */
92
+ cleanup (url) {
93
+ const { servers, provides } = this, { pid } = process
94
+ for (let [key,srv] of Object.entries (provides))
95
+ if (srv.server === pid || url && srv.credentials?.url?.startsWith(url)) delete provides [key]
96
+ delete servers [pid]
86
97
  return this
87
98
  }
88
99
  }
@@ -45,7 +45,7 @@ connect.to = (datasource, options) => {
45
45
  // construct new service instance
46
46
  let srv = await new Service (datasource,m,o); await (Service._is_service_class ? srv.init?.() : Service.init?.(srv))
47
47
  if (o.outbox) srv = cds.outboxed(srv)
48
- if (datasource) {
48
+ if (datasource && !options) {
49
49
  if (datasource === 'db') cds.db = srv
50
50
  cds.services[datasource] = srv
51
51
  delete _pending[datasource]
@@ -7,6 +7,7 @@ class MockedUsers {
7
7
  const tenants = this.tenants = options.tenants || {}
8
8
  const users = this.users = options.users || {}
9
9
  for (let [k,v] of Object.entries(users)) {
10
+ if (!cds.env.requires.multitenancy) delete v.tenant
10
11
  if (typeof v === 'boolean') continue
11
12
  if (typeof v === 'string') v = { password:v }
12
13
  let id = _configured(v).id || k