@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@
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 9.8.1 - 2026-03-09
8
+
9
+ ### Fixed
10
+
11
+ - OData batch parallel processing: Preserve request sequence for OData v2
12
+ - In inbound messaging, only load the extended model if there is a tenant
13
+ - Ensure `cds.fiori.direct_crud` is considered during `cds build`
14
+
15
+ ## Version 9.8.0 - 2026-03-06
16
+
17
+ ### Added
18
+
19
+ - Calculated elements are now properly calculated in draft state. So far, the elements have been treated as regular elements ignoring the calculation.
20
+ + In case this causes issues, you can opt-out with `cds.fiori.calc_elements = false` until cds10
21
+ - $compute supports decimal constants
22
+ - Support `/$metadata` requests in OData batch
23
+ - Support for `@odata.bind` parameters in collection bound actions
24
+ - Support for renaming of foreign keys of managed associations while resolving views
25
+ - Support for `Prefer: return=minimal` header in generic draft actions
26
+ - OData batch: Parallel processing of atomicity groups via `cds.odata.max_batch_parallelization=<number>` (default: `1`)
27
+ - Only applicable if `$batch` request exclusively contains `GET` requests
28
+ - Note: Parallel processing of atomicity groups is in conflict with OData specification for `multipart/mixed`!
29
+ - Additional experimental feature: Bundle independent `GET` requests into single transaction via `cds.odata.group_parallel_gets=true`
30
+ - `hcql`: request raw stream via http header `Accept: application/octet-stream`
31
+
32
+ ### Changed
33
+
34
+ - Destination caching is no longer modified at runtime. Caching configuration is now managed by the Cloud SDK.
35
+ - `cds.env` now supports for credentials lookup Kubernetes secrets with a structure like `${SERVICE_BINDING_ROOT}/${SERVICE}/${INSTANCE}/${BINDING}`. Previously only one level between the root and the binding was possible.
36
+ - Allow using ESlint 10 by opening version range `^9 || ^10` for `@eslint/js`
37
+ - Outbound `hcql` requests always via `POST`
38
+
39
+ ### Fixed
40
+
41
+ - OData JSON batch:
42
+ + Response value formatting and escaping for non `application/json` responses
43
+ + Setting correct `content-type` header value for all content types
44
+ - `content-length` is now set for OData multipart/mixed batch subrequest responses
45
+ - Connection issues when a Kafka cluster has been created with public endpoints
46
+ - Fix duplicated columns in case of `$expand=*`
47
+ - Unhandled promise rejection in `cds.spawn` if extended model could not be loaded
48
+ - Set `msg.tenant` & `msg.event` of messages received from Kafka, based on the stringified header values
49
+ - `$expand=*` expands only the exposed associations
50
+ - Broken `odata-v2` formatting for values used in `beween- and`, `in` and `lambda` type expressions
51
+ - Appending `/$value` to an entity that is not a media entity returns `400 Bad Request`
52
+ - In Jest test runs in ESM projects, files are now loaded properly
53
+ - Correctly resolve dependencies in workspace setups where the CAP project is not at the root
54
+
7
55
  ## Version 9.7.1 - 2026-02-06
8
56
 
9
57
  ### Fixed
@@ -1,113 +1,58 @@
1
- #Text Types
2
- #
3
- #XACT: Text with explicit importance for accessibility.
4
- #XBUT: Button
5
- #XCKL: Checkbox
6
- #XFLD: Field label
7
- #XLNK: Hyperlink
8
- #XMIT: Menu item (Menu item, either top-level like "File" or lower-level like "Save as...")
9
- #XMSG: Message
10
- #XRBL: Radio button
11
- #XSEL: Selection (Values in a drop-down list, or a status. For example: "In Process", "Shipped" or "Open".)
12
- #XTIT: Title (or heading) of a non-actionable user interface element like a column, wizard, or screen area.
13
- #XTOL: Explanatory text for an UI element, such as a tooltip, input help.
14
- #YINS: Instruction for a user, for example, a permanent text on a screen that introduces a group of fields.
15
- #----------------------------------------------------------------------------------------------------------------------
16
- #For text elements that are not supposed to be translated, use the text type NOTR
17
- #----------------------------------------------------------------------------------------------------------------------
18
- #Recommended pattern
19
- #
20
- #<TextType>:<AdditionalContextInformation>
21
- #If there is a maximum length restriction, please indicate as shown below.
22
- #<TextType>,<MaximumLength>:<AdditionalContextInformation>
23
- #----------------------------------------------------------------------------------------------------------------------
24
- # This is the resource bundle for foundation
25
- # __ldi.translation.uuid=dd6c5800-b108-11e8-be90-bd1cf6ac87fb
26
- #----------------------------------------------------------------------------------------------------------------------
27
-
28
- #XTIT: Created By (Answer to: "Which user has created a certain entity?")
1
+
29
2
  CreatedBy=Bx7yxWg0AHyeOti1YDOOXw_Created By
30
3
 
31
- #XTIT: Created On (Answer to: "When has a certain entity been created?")
32
4
  CreatedAt=CJxeZDpLHdREwzDQeluRuA_Created On
33
5
 
34
- #XTIT: Changed By (Answer to: "Which user has changed a certain entity?")
35
6
  ChangedBy=y0d7F56RLGVZiTV7YHU7Iw_Changed By
36
7
 
37
- #XTIT: Changed On (Answer to: "When has a certain entity been changed?")
38
8
  ChangedAt=8v5EfaWMYC2dmCGnaCSSUA_Changed On
39
9
 
40
- #XTIT: Currency
41
10
  Currency=bNSEwGmQtXNxy/Qh310iSQ_Currency
42
11
 
43
- #XTIT: Currency Code
44
12
  CurrencyCode=3cKgP4qz+IsDHtETO3InVQ_Currency Code
45
13
 
46
- #XTIT: Currency Code Description
47
14
  CurrencyCode.Description=pPQIrs2UIayPnWseWvdbuA_Currency code as specified by ISO 4217
48
15
 
49
- #XTIT: Currency Symbol
50
16
  CurrencySymbol=ICns/nlRq2+/URxMXV+T8g_Currency Symbol
51
17
 
52
- #XTIT: Currency Minor Unit Fractions (Answer to: "How many fractions has a currency's minor unit?", e.g. "0" or "2")
53
18
  CurrencyMinorUnit=0S6MF8n74OoGrKEYI1LO+Q_Currency Minor Unit Fractions
54
19
 
55
- #XTIT: Country/Region
56
20
  Country=AbsSu8Y0n1nvBQMk+UaLfw_Country/Region
57
21
 
58
- #XTIT: Country/Region Code
59
22
  CountryCode=IgAotY/RI8UnAUTEtFNGkw_Country/Region Code
60
23
 
61
- #XTIT: Country/Region Code Description
62
24
  CountryCode.Description=uSGs3P7TwJlYCpDq83TJNg_Country/region code as specified by ISO 3166-1
63
25
 
64
- #XTIT: Language
65
26
  Language=gFWTYTeLWksYL6uD/TAgFA_Language
66
27
 
67
- #XTIT: Language Code
68
28
  LanguageCode=NhI8Yd8pNFS7omWQsk5aJw_Language Code
69
29
 
70
- #XTIT: Language Code Description
71
30
  LanguageCode.Description=ajmXdjo3lK0nfaMLCpvPMw_Language code as specified by ISO 639-1
72
31
 
73
- #XTIT Time zone code
74
32
  TimeZoneCode=Y0KTpmsmzoysYLT6jDQEkQ_Time Zone Code
75
33
 
76
- #XTIT: User Identifier
77
34
  UserID=cjI0FCsEZ2aD8ERc6G/xZw_User ID
78
35
 
79
- #XTIT: Any kind of name
80
36
  Name=xOqOj8rcOinN3ZBO0WdWjA_Name
81
37
 
82
- #XTIT: Any kind of description
83
38
  Description=VGpOoz5siT25o1W0WDiHGw_Description
84
39
 
85
- #XTOL: A user's unique Indentifier
86
40
  UserID.Description=yOjW1w1qaIvgZeYb0qAsig_User's unique ID
87
41
 
88
- #XTIT: Admin data for a draft document
89
42
  Draft_DraftAdministrativeData=LsxY+0o1fWbFeMrxNNIRvg_Draft Administrative Data
90
43
 
91
- #XTIT: Technical ID of a draft document
92
44
  Draft_DraftUUID=Y5OedvjtpZGSeRwWXI4alQ_Draft (Technical ID)
93
45
 
94
- #XTIT: Creation time of a draft
95
46
  Draft_CreationDateTime=Y2ttcgWpNQmB69F6w8jYmw_Draft Created On
96
47
 
97
- #XTIT: User created the draft
98
48
  Draft_CreatedByUser=fVPXdEA4PQ/ruI+1ZC6lzg_Draft Created By
99
49
 
100
- #XTIT: The current user (me) created the draft
101
50
  Draft_DraftIsCreatedByMe=Oi4B0zRRBAKoN6wuBWyQPA_Draft Created By Me
102
51
 
103
- #XTIT: Time a draft was last changed on
104
52
  Draft_LastChangeDateTime=3ZVPB34hJDc38V1GIGr6cw_Draft Last Changed On
105
53
 
106
- #XTIT: User that changed the draft last
107
54
  Draft_LastChangedByUser=X7I4n0+rc50bDozET507FA_Draft Last Changed By
108
55
 
109
- #XTIT: User that is working on the draft
110
56
  Draft_InProcessByUser=/MEKNHLAPlLRsGiFC1PeOA_Draft In Process By
111
57
 
112
- #XTIT: The current user (me) is working on the draft
113
58
  Draft_DraftIsProcessedByMe=zZGCPf/2uR37cfpgf1znGA_Draft In Process By Me
@@ -1,194 +1,103 @@
1
- # Input Validation
2
1
 
3
- #XMSG: Enter a value between 0 and 100. // Implementation still provides 3 values
4
2
  ASSERT_RANGE=UsaDWZPG4eILDj9gBv9dQQ_Enter a value between {1} and {2}.
5
- #XMSG: Enter a valuematching the pattern /^abc/.
6
3
  ASSERT_FORMAT=lXdUpDxTyQee5ifT6kBnEg_Enter a value matching the pattern {1}.
7
- #XMSG: Enter one of the allowed values: High,Medium,Low. // Implementation still provides 2 values
8
4
  ASSERT_ENUM=vik7PhkgJHJK7qIQQ8YvPw_Enter one of the allowed values: {1}.
9
- #XMSG: Provide the missing value. // Error text displayed on a field, in case a mandatory value is not set.
10
5
  ASSERT_MANDATORY=fetcw29Zp30ja46xX6Y/Lw_Provide the missing value.
11
- #XMSG: Target with this key doesn't exist.
12
6
  ASSERT_TARGET=QJmMamMnIKhch4NMNFG+1A_Target with this key doesn''t exist.
13
7
 
14
- # Aggregating Error
15
8
 
16
- #XMSG
17
9
  MULTIPLE_ERRORS=G/CGK3r2VN2zrerNELfXsg_Multiple errors occurred, see details below.
18
10
 
19
- # Input format
20
11
 
21
- #NOTR
22
12
  ASSERT_VALID_ELEMENT=Element is not valid
23
- #NOTR
24
13
  ASSERT_DATA_TYPE=Value {0} is not a valid {1}
25
- #NOTR
26
14
  ASSERT_ARRAY=Value must be an array
27
15
 
28
- # Status Codes
29
16
 
30
- #NOTR
31
17
  400=Bad Request
32
- #NOTR
33
18
  401=Unauthorized
34
- #NOTR
35
19
  403=Forbidden
36
- #NOTR
37
20
  404=Not Found
38
- #NOTR
39
21
  405=Method Not Allowed
40
- #NOTR
41
22
  406=Not Acceptable
42
- #NOTR
43
23
  407=Proxy Authentication Required
44
- #NOTR
45
24
  408=Request Timeout
46
- #NOTR
47
25
  409=Conflict
48
- #NOTR
49
26
  410=Gone
50
- #NOTR
51
27
  411=Length Required
52
- #NOTR
53
28
  412=Precondition Failed
54
- #NOTR
55
29
  413=Payload Too Large
56
- #NOTR
57
30
  414=URI Too Long
58
- #NOTR
59
31
  415=Unsupported Media Type
60
- #NOTR
61
32
  416=Range Not Satisfiable
62
- #NOTR
63
33
  417=Expectation Failed
64
- #NOTR
65
34
  422=Unprocessable Content
66
- #NOTR
67
35
  424=Failed Dependency
68
- #NOTR
69
36
  428=Precondition Required
70
- #NOTR
71
37
  429=Too Many Requests
72
- #NOTR
73
38
  431=Request Header Fields Too Large
74
- #NOTR
75
39
  451=Unavailable For Legal Reasons
76
- #NOTR
77
40
  500=Internal Server Error
78
- #NOTR
79
41
  501=The server does not support the functionality required to fulfill the request
80
- #NOTR
81
42
  502=Bad Gateway
82
- #NOTR
83
43
  503=Service Unavailable
84
- #NOTR
85
44
  504=Gateway Timeout
86
45
 
87
- # fragments
88
46
 
89
- #NOTR
90
47
  ENTITY=entity
91
- #NOTR
92
48
  TYPE=type
93
- #NOTR
94
49
  FUNCTION=function
95
- #NOTR
96
50
  ACTION=action
97
51
 
98
- # db
99
52
 
100
- #NOTR
101
53
  NO_DATABASE_CONNECTION=No database connection
102
- #NOTR
103
54
  ENTITY_ALREADY_EXISTS=Entity already exists
104
- #NOTR
105
55
  ENTITY_LOCKED=Entity locked
106
- #NOTR
107
56
  UNIQUE_CONSTRAINT_VIOLATION=Unique constraint violation
108
- #NOTR
109
57
  FK_CONSTRAINT_VIOLATION=Foreign key constraint violation
110
58
 
111
- # remote
112
59
 
113
- #NOTR
114
60
  INVALID_CONTENT_TYPE_ONLY_JSON=Invalid content type. Only "application/json" is supported.
115
61
 
116
- # access control
117
62
 
118
- #NOTR
119
63
  INSERTABLE=insertable
120
- #NOTR
121
64
  READABLE=readable
122
- #NOTR
123
65
  UPDATABLE=updatable
124
- #NOTR
125
66
  DELETABLE=deletable
126
- #NOTR
127
67
  ENTITY_IS_INSERT_ONLY=Entity "{0}" is insert-only
128
- #NOTR
129
68
  ENTITY_IS_READ_ONLY=Entity "{0}" is read-only
130
- #NOTR
131
69
  ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
132
- #NOTR
133
70
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
134
- #NOTR
135
71
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
136
- #NOTR
137
72
  ENTITY_IS_AUTOEXPOSE_READONLY=Entity "{0}" is explicitly exposed as readonly
138
- #NOTR
139
73
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
140
74
 
141
- # rest protocol adapter
142
75
 
143
- #NOTR
144
76
  INVALID_RESOURCE="{0}" is not a valid resource
145
- #NOTR
146
77
  INVALID_PARAMETER="{0}" is not a valid parameter
147
- #NOTR
148
78
  INVALID_PARAMETER_VALUE_TYPE=Parameter value for "{0}" must be of type "{1}"
149
- #NOTR
150
79
  INVALID_OPERATION_FOR_ENTITY=Entity "{0}" has no {1} "{2}"
151
- #NOTR
152
80
  NO_MATCHING_RESOURCE=The server has not found a resource matching the requested URI
153
- #NOTR
154
81
  INVALID_POST=POST is only allowed on resource collections and actions
155
- #NOTR
156
82
  INVALID_PUT=PUT is only allowed on a specific resource
157
- #NOTR
158
83
  INVALID_PATCH=PATCH is only allowed on a specific resource
159
- #NOTR
160
84
  INVALID_DELETE=DELETE is only supported on a specific resource
161
- #NOTR
162
85
  CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
163
86
 
164
- # OData protocol adapter
165
87
 
166
- #NOTR
167
88
  BATCH_TOO_MANY_REQ=Batch request contains too many requests
168
89
 
169
- # draft
170
90
 
171
- #NOTR
172
91
  DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
173
- #NOTR
174
92
  DRAFT_ALREADY_EXISTS=A draft for this entity already exists
175
- #NOTR
176
93
  DRAFT_NOT_EXISTING=No draft for this entity exists
177
- #NOTR
178
94
  DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft-enabled entity can only be modified via its root entity
179
- #NOTR
180
95
  ACTIVE_MODIFICATION_VIA_DRAFT=Active entities cannot be modified via draft request
181
- #NOTR
182
96
  DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS=Entity cannot be deleted because a draft exists
183
97
 
184
- # singleton
185
98
 
186
- #NOTR
187
99
  SINGLETON_NOT_NULLABLE=The singleton entity is not nullable
188
100
 
189
- # flows
190
101
 
191
- #XMSG: Action "acceptTravel" requires "travelStatus" to be "Open".
192
102
  INVALID_FLOW_TRANSITION_SINGLE=35OYYAR7qBeFqZojBKMD7w_Action "{0}" requires "{1}" to be "{2}".
193
- #XMSG: Action "cancelTravel" requires "travelStatus" to be one of the following values: Open,Accepted.
194
- INVALID_FLOW_TRANSITION_MULTI=mqoLKfqWIZ4EX7lQDYHTzw_Action "{0}" requires "{1}" to be one of the following values: {2}.
103
+ INVALID_FLOW_TRANSITION_MULTI=mqoLKfqWIZ4EX7lQDYHTzw_Action "{0}" requires "{1}" to be one of the following values: {2}.
package/eslint.config.mjs CHANGED
@@ -14,6 +14,9 @@ export const defaults = {
14
14
  rules: {
15
15
  'no-unused-vars': 'warn',
16
16
  'no-console': 'warn',
17
+ 'preserve-caught-error': 'off',
18
+ 'no-useless-assignment': 'off',
19
+ 'no-unassigned-vars': 'off'
17
20
  },
18
21
 
19
22
  languageOptions: {
@@ -75,7 +78,7 @@ export const defaults = {
75
78
  * ESLint config for jest and mocha test.
76
79
  */
77
80
  export const tests = {
78
- files: [ '**/test/**/*.js', '**/test?/**/*.js', '**/*.test.js', '**/*-test.js' ],
81
+ files: [ '**/+(test|tests)/**/*.+(js|cjs|mjs)', '**/*.test.+(js|cjs|mjs)', '**/*-test.+(js|cjs|mjs)' ],
79
82
  languageOptions: {
80
83
  globals: {
81
84
  mocha: 'readonly',
@@ -6,6 +6,7 @@ const compile = module.exports = Object.assign (cds_compile, {
6
6
  for: new class {
7
7
  get java(){ return super.java = require('./for/java') }
8
8
  get nodejs() { return super.nodejs = require('./for/nodejs') }
9
+ get direct_crud() { return super.direct_crud = require('./for/direct_crud') }
9
10
  get lean_drafts() { return super.lean_drafts = require('./for/lean_drafts') }
10
11
  get flows() { return super.flows = require('./for/flows') }
11
12
  get assert() { return super.assert = require('./for/assert') }
@@ -0,0 +1,23 @@
1
+ const $compiled_for_direct_crud = Symbol('compiled_for_direct_crud')
2
+ const DRAFT_NEW = 'draftNew'
3
+
4
+ module.exports = function cds_compile_for_direct_crud(csn) {
5
+ if (csn[$compiled_for_direct_crud]) return csn
6
+ csn[$compiled_for_direct_crud] = true
7
+
8
+ for (const each in csn.definitions) {
9
+ const def = csn.definitions[each]
10
+ if (!def['@Common.DraftRoot.NewAction'] && def['@odata.draft.enabled']) {
11
+ const srvName = Object.keys(csn.definitions)
12
+ .filter(k => csn.definitions[k].kind === 'service')
13
+ .find(k => each.startsWith(`${k}.`))
14
+ def['@Common.DraftRoot.NewAction'] = `${srvName}.${DRAFT_NEW}`
15
+ const params = { in: { items: { type: '$self' } } }
16
+ // for UI pop-up asking for values for non-UUID keys
17
+ Object.keys(def.elements)
18
+ .filter(k => k !== 'IsActiveEntity' && def.elements[k].key && def.elements[k].type !== 'cds.UUID')
19
+ .forEach(k => (params[k] = { type: def.elements[k].type }))
20
+ def.actions[DRAFT_NEW] = { kind: 'action', params, returns: { type: each } }
21
+ }
22
+ }
23
+ }
@@ -65,6 +65,18 @@ function DraftEntity4(active, name = active.name + '.drafts') {
65
65
  const _pname = active['@cds.persistence.name']
66
66
  if (_pname) draft['@cds.persistence.name'] = _pname + '_drafts'
67
67
 
68
+ if (cds.env.fiori.calc_elements !== false) {
69
+ for (const each in draft.elements) {
70
+ const element = draft.elements[each]
71
+ // $calc = true indicates that the compiler cannot determine the calculated expression
72
+ if (element.$calc && element.$calc !== true) {
73
+ const e = { __proto__: element }
74
+ e.value = element.$calc
75
+ draft.elements[each] = e
76
+ }
77
+ }
78
+ }
79
+
68
80
  return draft
69
81
  }
70
82
 
@@ -9,24 +9,7 @@ module.exports = function cds_compile_for_odata (csn,_o) {
9
9
  let dsn = compile.for.odata (csn,o)
10
10
  if (o.sql_mapping) dsn['@sql_mapping'] = o.sql_mapping //> compat4 old Java stack
11
11
 
12
- if (cds.env.fiori.direct_crud) {
13
- const DRAFT_NEW = 'draftNew'
14
- for (const each in dsn.definitions) {
15
- const def = dsn.definitions[each]
16
- if (!def['@Common.DraftRoot.NewAction'] && def['@Common.DraftRoot.ActivationAction']) {
17
- const srvName = Object.keys(dsn.definitions)
18
- .filter(k => dsn.definitions[k].kind === 'service')
19
- .find(k => each.startsWith(`${k}.`))
20
- def['@Common.DraftRoot.NewAction'] = `${srvName}.${DRAFT_NEW}`
21
- const params = { in: { items: { type: '$self' } } }
22
- // for UI pop-up asking for values for non-UUID keys
23
- Object.keys(def.elements)
24
- .filter(k => k !== 'IsActiveEntity' && def.elements[k].key && def.elements[k].type !== 'cds.UUID')
25
- .forEach(k => (params[k] = { type: def.elements[k].type }))
26
- def.actions[DRAFT_NEW] = { kind: 'action', params, returns: { type: each } }
27
- }
28
- }
29
- }
12
+ if (cds.env.fiori.direct_crud) cds.compile.for.direct_crud(dsn)
30
13
 
31
14
  Object.defineProperty (csn, '_4odata', {value:dsn})
32
15
  Object.defineProperty (dsn, '_4odata', {value:dsn})
@@ -42,6 +42,7 @@ function cds_compile_to_edmx (csn,_o) {
42
42
  const next = () => {
43
43
  if (!result) {
44
44
  if (cds.env.features.annotate_for_flows) enhanceCSNwithFlowAnnotations4FE(csn)
45
+ if (cds.env.fiori.direct_crud) cds.compile.for.direct_crud(csn)
45
46
  result = o.service === 'all' ? _many('.xml', cdsc.to.edmx.all(csn, o)) : cdsc.to.edmx(csn, o)
46
47
  }
47
48
  return result
@@ -18,13 +18,15 @@ module.exports = (csn,o={}) => {
18
18
  } catch {/* ignored */}
19
19
 
20
20
  else if (v.kind === "service" && !v['@source'] && v.$location?.file) {
21
+ let fileLocation = v.$location.file
22
+
21
23
  // Preserve original sources for services so we can use them for finding
22
24
  // sibling implementation files when reloaded from csn.json.
23
- let file = relative(v.$location.file)
25
+ let file = relative(fileLocation)
24
26
  .replace(/\\/g,'/')
25
27
  .replace(relative_cds_home,'@sap/cds/')
26
28
  for (const mld of moduleLookupDirectories) { // node_modules/ usually, more for Java
27
- file = file.replace(mld, '')
29
+ file = file.replace(new RegExp('.*'+mld), '') // also remove relative `../` paths, needed for workspace setups
28
30
  }
29
31
 
30
32
  // If there is still a relative path pointing outside of cwd, convert it to a module path
@@ -166,6 +166,7 @@ module.exports = {
166
166
  containment: undefined,
167
167
  context_with_columns: false,
168
168
  max_batch_header_size: '64KiB', // instead of node's 16KiB
169
+ max_batch_parallelization: 1,
169
170
  },
170
171
 
171
172
  sql: {
@@ -112,11 +112,21 @@ function readServiceBindingsServicesFromPath(serviceBindingRoot) {
112
112
  for (const bindingEntry of fs.readdirSync(serviceBindingRoot, { withFileTypes: true })) {
113
113
  if (bindingEntry.isDirectory()) {
114
114
  const bindingPath = path.join(serviceBindingRoot, bindingEntry.name)
115
- const binding = readBinding(bindingPath, bindingEntry.name)
116
- if (!binding) continue
117
- const type = binding.type
118
- const bindings = bindingsForService[type] || (bindingsForService[type] = [])
119
- bindings.push(binding)
115
+ let binding = readBinding(bindingPath, bindingEntry.name)
116
+ if (!binding) {
117
+ // If binding is undefined, check if its not nested bindings
118
+ for (const dirEntry of fs.readdirSync(bindingPath, { withFileTypes: true })) {
119
+ if (!dirEntry.isDirectory() || !dirEntry.name) continue
120
+ const instancePath = path.join(bindingPath, dirEntry.name)
121
+ binding = readBinding(instancePath, dirEntry.name)
122
+ if (!binding) continue;
123
+ bindingsForService[binding.type] ??= []
124
+ bindingsForService[binding.type].push(binding)
125
+ }
126
+ continue
127
+ }
128
+ bindingsForService[binding.type] ??= []
129
+ bindingsForService[binding.type].push(binding)
120
130
  }
121
131
  }
122
132
  return Object.keys(bindingsForService).length > 0 ? bindingsForService : undefined
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require('./utils/version').check()
1
+ if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require('./utils/version').checkNodeVersion()
2
2
 
3
3
  const { AsyncLocalStorage } = require ('async_hooks')
4
4
  const context = new AsyncLocalStorage
@@ -5,33 +5,46 @@ const { format, inspect } = require('../utils/cds-utils')
5
5
  * Constructs and optionally throws an Error object.
6
6
  * Usage variants:
7
7
  *
8
- * cds.error (404, 'Not Found', { code, ... })
9
- * cds.error ('Not Found', { code, ... })
10
- * cds.error ({ code, message, ... })
8
+ * cds.error (404, 'code', 'message', { ... details })
9
+ * cds.error (404, { ... details })
10
+ * cds.error ({ ... any details })
11
+ * cds.error ('code', 'message', { ... details })
12
+ * cds.error ('message', { ... details })
11
13
  * cds.error `template string usage variant`
14
+ * cds.error (new Error, { ... any details })
12
15
  *
13
16
  * When called with `new` the newly created Error is returned.
14
17
  * When called without `new` the error is thrown immediately.
15
18
  * The latter is useful for usages like that:
16
19
  *
17
- * let x = y || cds.error `'y' must be truthy, got: ${y}`
20
+ * let x = y || cds.error `expected y to be defined`
18
21
  *
19
22
  * @param {number} [status] - HTTP status code
20
- * @param {string} [message] - Error message
23
+ * @param {string} [code] - Stable error code, which clients can rely on
24
+ * @param {string} [message] - Human-readable error message
21
25
  * @param {object} [details] - Additional error details
22
- * @param {Function} [caller] - The function calling this
26
+ * @param {Function} [caller] - The function calling us => stack trace cut off here
27
+ * @returns {Error} The constructed Error (only when called with `new`)
23
28
  */
24
- const error = exports = module.exports = function error ( status, message, details, caller ) {
25
- if (typeof status !== 'number') [ status, message, details, caller ] = status.raw ? [ undefined, error.message(...arguments) ] : [ undefined, status, message, details ]
26
- if (typeof message === 'object') [ message, details, caller ] = [ undefined, message, details ]
27
- let err = details && 'stack' in details ? details : Object.assign (new Error (message, details), details)
28
- if (caller) Error.captureStackTrace (err, caller); else Error.captureStackTrace (err, error)
29
- if (status) Object.defineProperty (err, 'status', {value:status})
30
- if (new.target) return err
31
- else throw err
29
+ const error = exports = module.exports = function error ( status, code, msg, details, caller ) {
30
+ let e = details
31
+ if (status?.raw) [ msg, status, code, e, caller ] = [ error.message(...arguments) ]
32
+ if (typeof status !== 'number') [ status, code, msg, e, caller ] = [ undefined, status, code, msg, e, caller ]
33
+ if (typeof code === 'object') [ code, msg, e, caller ] = [ undefined, undefined, code, msg ]
34
+ if (typeof msg === 'object') [ code, msg, e, caller ] = [ undefined, code, msg, e ]
35
+ if (typeof e === 'string') [ status, msg, e, caller ] = [ msg, e, caller, error ]
36
+ if (code && !msg) [ code, msg ] = [ undefined, code ]
37
+ if (e instanceof Error || typeof e === 'object' && 'stack' in e) { // is error?
38
+ e = Object.assign (e, caller) //> yes -> just decorate it
39
+ } else {
40
+ e = Object.assign (new Error (msg, e), e)
41
+ Error.captureStackTrace (e, caller || error)
42
+ }
43
+ if (status) e.status = status
44
+ if (code) e.code = code
45
+ if (new.target) return e; else throw e
32
46
  }
33
47
 
34
-
35
48
  /**
36
49
  * Constructs a message from a tagged template string. In contrast to usual
37
50
  * template strings embedded values are formatted using `util.format`
@@ -56,14 +69,14 @@ exports.message = (strings,...values) => {
56
69
  * typeof x === 'string' || cds.error.expected `${{x}} to be a string`
57
70
  * //> Error: Expected argument 'x' to be a string, but got: { foo: 'bar' }
58
71
  */
59
- exports.expected = ([,type], arg) => {
60
- const [ name, value ] = Object.entries(arg)[0]
61
- return error (`Expected argument '${name}'${type}, but got: ${inspect(value)}`, undefined, error.expected)
72
+ exports.expected = function expected ([,_to_be_expected], arg) {
73
+ const [name] = Object.keys(arg), value = arg[name]
74
+ return error (`Expected argument ${inspect(name)}${_to_be_expected}, but got: ${inspect(value)}`, undefined, expected)
62
75
  }
63
76
 
64
77
  exports.isSystemError = err => {
65
- // all errors thrown by the peggy parser should not crash the app
66
- if (err.name === 'SyntaxError' && err.constructor?.name === 'peg$SyntaxError') return false
78
+ // all errors thrown by the peggy parser or body-parser (used by express.json) should not crash the app
79
+ if (err.name === 'SyntaxError' && (err.constructor?.name === 'peg$SyntaxError' || err.type === 'entity.parse.failed')) return false
67
80
  return err.name in {
68
81
  TypeError:1,
69
82
  ReferenceError:1,
package/lib/req/spawn.js CHANGED
@@ -21,9 +21,9 @@ module.exports = function spawn (o, fn, /** @type {import('../index')} */ cds=th
21
21
  return tx.rollback(e)
22
22
  })
23
23
  .then (res => Promise.all(em.listeners('succeeded').map(each => each(res))))
24
- .catch (err => Promise.all(em.listeners('failed').map(each => each(err))))
25
- .finally (() => Promise.all(em.listeners('done').map(each => each())))
26
24
  })
25
+ .catch (err => Promise.all(em.listeners('failed').map(each => each(err))))
26
+ .finally (() => Promise.all(em.listeners('done').map(each => each())))
27
27
  }
28
28
  const em = new EventEmitter
29
29
  em.timer = (
@@ -29,23 +29,16 @@ class Bindings {
29
29
  const kind = [ required?.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
30
30
  const path = binding.endpoints [kind]
31
31
 
32
- // in case of cds.requires.Foo = { ... }
33
- if (typeof required === 'object') required.credentials = {
34
- ...required.credentials,
35
- ...binding.credentials,
36
- url: server.url + path
37
- }
38
-
39
- // in case of cds.requires.Foo = true
40
- else required = cds.requires[service] = cds.env.requires[service] = {
41
- ...cds.requires.kinds [binding.kind],
32
+ if (typeof required !== 'object') required = {}
33
+ cds.env.requires[service] = required = {
34
+ kind, ...cds.requires.kinds [kind], ...required,
42
35
  credentials: {
36
+ ...required.credentials,
43
37
  ...binding.credentials,
44
38
  url: server.url + path
45
- }
39
+ },
46
40
  }
47
-
48
- required.kind = kind
41
+ required.kind = kind // override kind from binding
49
42
 
50
43
  // REVISIT: temporary fix to inherit kind as well for mocked odata services
51
44
  // otherwise mocking with two services does not work for kind:odata-v2