@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.
- package/CHANGELOG.md +48 -0
- package/_i18n/i18n_en_US_saptrc.properties +1 -56
- package/_i18n/messages_en_US_saptrc.properties +1 -92
- package/eslint.config.mjs +4 -1
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/direct_crud.js +23 -0
- package/lib/compile/for/lean_drafts.js +12 -0
- package/lib/compile/for/odata.js +1 -18
- package/lib/compile/to/edm.js +1 -0
- package/lib/compile/to/json.js +4 -2
- package/lib/env/defaults.js +1 -0
- package/lib/env/serviceBindings.js +15 -5
- package/lib/index.js +1 -1
- package/lib/log/cds-error.js +33 -20
- package/lib/req/spawn.js +2 -2
- package/lib/srv/bindings.js +6 -13
- package/lib/srv/cds.Service.js +8 -36
- package/lib/srv/protocols/hcql.js +19 -2
- package/lib/utils/cds-utils.js +25 -16
- package/lib/utils/tar-win.js +106 -0
- package/lib/utils/tar.js +23 -158
- package/libx/_runtime/common/generic/crud.js +8 -7
- package/libx/_runtime/common/generic/sorting.js +7 -3
- package/libx/_runtime/common/utils/resolveView.js +47 -40
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -0
- package/libx/_runtime/fiori/lean-draft.js +11 -2
- package/libx/_runtime/messaging/kafka.js +6 -5
- package/libx/_runtime/messaging/service.js +3 -1
- package/libx/_runtime/remote/Service.js +3 -0
- package/libx/_runtime/remote/utils/client.js +2 -4
- package/libx/_runtime/remote/utils/query.js +4 -4
- package/libx/odata/middleware/batch.js +323 -339
- package/libx/odata/middleware/create.js +0 -5
- package/libx/odata/middleware/delete.js +0 -5
- package/libx/odata/middleware/operation.js +10 -8
- package/libx/odata/middleware/read.js +0 -10
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +0 -6
- package/libx/odata/parse/afterburner.js +47 -22
- package/libx/odata/parse/cqn2odata.js +6 -1
- package/libx/odata/parse/grammar.peggy +14 -2
- package/libx/odata/parse/multipartToJson.js +2 -1
- package/libx/odata/parse/parser.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
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: [ '
|
|
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
|
|
package/lib/compile/for/odata.js
CHANGED
|
@@ -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})
|
package/lib/compile/to/edm.js
CHANGED
|
@@ -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
|
package/lib/compile/to/json.js
CHANGED
|
@@ -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(
|
|
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
|
package/lib/env/defaults.js
CHANGED
|
@@ -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
|
-
|
|
116
|
-
if (!binding)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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').
|
|
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
|
package/lib/log/cds-error.js
CHANGED
|
@@ -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, '
|
|
9
|
-
* cds.error (
|
|
10
|
-
* cds.error ({
|
|
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 `
|
|
20
|
+
* let x = y || cds.error `expected y to be defined`
|
|
18
21
|
*
|
|
19
22
|
* @param {number} [status] - HTTP status code
|
|
20
|
-
* @param {string} [
|
|
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
|
|
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,
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
|
|
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 = ([,
|
|
60
|
-
const [
|
|
61
|
-
return error (`Expected argument
|
|
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 = (
|
package/lib/srv/bindings.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
...
|
|
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
|