@sap/cds 9.7.0 → 9.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/_i18n/i18n_en_US_saptrc.properties +1 -56
- package/_i18n/messages_en_US_saptrc.properties +1 -92
- package/eslint.config.mjs +1 -1
- package/lib/compile/for/flows.js +86 -79
- package/lib/compile/for/lean_drafts.js +12 -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/srv/protocols/http.js +1 -1
- 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/remote/Service.js +14 -2
- package/libx/_runtime/remote/utils/client.js +2 -4
- package/libx/_runtime/remote/utils/query.js +4 -4
- package/libx/odata/middleware/batch.js +316 -339
- package/libx/odata/middleware/create.js +0 -5
- package/libx/odata/middleware/delete.js +2 -6
- 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,55 @@
|
|
|
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.0 - 2026-03-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Calculated elements are now properly calculated in draft state. So far, the elements have been treated as regular elements ignoring the calculation.
|
|
12
|
+
+ In case this causes issues, you can opt-out with `cds.fiori.calc_elements = false` until cds10
|
|
13
|
+
- $compute supports decimal constants
|
|
14
|
+
- Support `/$metadata` requests in OData batch
|
|
15
|
+
- Support for `@odata.bind` parameters in collection bound actions
|
|
16
|
+
- Support for renaming of foreign keys of managed associations while resolving views
|
|
17
|
+
- Support for `Prefer: return=minimal` header in generic draft actions
|
|
18
|
+
- OData batch: Parallel processing of atomicity groups via `cds.odata.max_batch_parallelization=<number>` (default: `1`)
|
|
19
|
+
- Only applicable if `$batch` request exclusively contains `GET` requests
|
|
20
|
+
- Note: Parallel processing of atomicity groups is in conflict with OData specification for `multipart/mixed`!
|
|
21
|
+
- Additional experimental feature: Bundle independent `GET` requests into single transaction via `cds.odata.group_parallel_gets=true`
|
|
22
|
+
- `hcql`: request raw stream via http header `Accept: application/octet-stream`
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Destination caching is no longer modified at runtime. Caching configuration is now managed by the Cloud SDK.
|
|
27
|
+
- `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.
|
|
28
|
+
- Allow using ESlint 10 by opening version range `^9 || ^10` for `@eslint/js`
|
|
29
|
+
- Outbound `hcql` requests always via `POST`
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- OData JSON batch:
|
|
34
|
+
+ Response value formatting and escaping for non `application/json` responses
|
|
35
|
+
+ Setting correct `content-type` header value for all content types
|
|
36
|
+
- `content-length` is now set for OData multipart/mixed batch subrequest responses
|
|
37
|
+
- Connection issues when a Kafka cluster has been created with public endpoints
|
|
38
|
+
- Fix duplicated columns in case of `$expand=*`
|
|
39
|
+
- Unhandled promise rejection in `cds.spawn` if extended model could not be loaded
|
|
40
|
+
- Set `msg.tenant` & `msg.event` of messages received from Kafka, based on the stringified header values
|
|
41
|
+
- `$expand=*` expands only the exposed associations
|
|
42
|
+
- Broken `odata-v2` formatting for values used in `beween- and`, `in` and `lambda` type expressions
|
|
43
|
+
- Appending `/$value` to an entity that is not a media entity returns `400 Bad Request`
|
|
44
|
+
- In Jest test runs in ESM projects, files are now loaded properly
|
|
45
|
+
- Correctly resolve dependencies in workspace setups where the CAP project is not at the root
|
|
46
|
+
|
|
47
|
+
## Version 9.7.1 - 2026-02-06
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- `DELETE` requests nulling a `@mandatory` property
|
|
52
|
+
- Correctly call remote collection bound action for `odata-v4` services
|
|
53
|
+
- Flow annotation validation at compile time strictly follows the documentation: only enum status values are allowed
|
|
54
|
+
+ Status value validation can be disabled via `cds.features.skip_flows_validation=true`
|
|
55
|
+
|
|
7
56
|
## Version 9.7.0 - 2026-02-02
|
|
8
57
|
|
|
9
58
|
### Added
|
|
@@ -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
|
@@ -75,7 +75,7 @@ export const defaults = {
|
|
|
75
75
|
* ESLint config for jest and mocha test.
|
|
76
76
|
*/
|
|
77
77
|
export const tests = {
|
|
78
|
-
files: [ '
|
|
78
|
+
files: [ '**/+(test|tests)/**/*.+(js|cjs|mjs)', '**/*.test.+(js|cjs|mjs)', '**/*-test.+(js|cjs|mjs)' ],
|
|
79
79
|
languageOptions: {
|
|
80
80
|
globals: {
|
|
81
81
|
mocha: 'readonly',
|
package/lib/compile/for/flows.js
CHANGED
|
@@ -147,7 +147,7 @@ function enhanceCSNwithFlowAnnotations4FE(csn) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
module.exports = function cds_compile_for_flows(csn) {
|
|
150
|
-
const { history_for_flows } = cds.env.features
|
|
150
|
+
const { history_for_flows, skip_flows_validation } = cds.env.features
|
|
151
151
|
|
|
152
152
|
const _requires_history = !history_for_flows
|
|
153
153
|
? () => false
|
|
@@ -168,6 +168,61 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
const _validate_status_types = skip_flows_validation
|
|
172
|
+
? () => {}
|
|
173
|
+
: (name, def, status, csn, errors) => {
|
|
174
|
+
// enum
|
|
175
|
+
let enumVals
|
|
176
|
+
if (status.type === 'cds.Association') {
|
|
177
|
+
const target = csn.definitions[status.target]
|
|
178
|
+
if (!status.keys || status.keys.length !== 1) {
|
|
179
|
+
errors.push(`Status element in entity ${name} must have exactly one key when used as association`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const code = target.elements[status.keys[0].ref[0]]
|
|
183
|
+
enumVals = code.enum || csn.definitions[code.type]?.enum
|
|
184
|
+
} else {
|
|
185
|
+
enumVals = status.enum ?? csn.definitions[status.type]?.enum
|
|
186
|
+
}
|
|
187
|
+
if (!enumVals) {
|
|
188
|
+
errors.push(`Status element in entity ${name} must be an enum or target an entity with an enum named "code"`)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
// actions
|
|
192
|
+
for (const each in def.actions) {
|
|
193
|
+
const action = def.actions[each]
|
|
194
|
+
const from = action[FROM]
|
|
195
|
+
if (from !== undefined) {
|
|
196
|
+
if (Array.isArray(from)) {
|
|
197
|
+
for (let i = 0; i < from.length; i++) {
|
|
198
|
+
if (from[i] !== null) {
|
|
199
|
+
let val = from[i]['#']
|
|
200
|
+
if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
|
|
201
|
+
errors.push(`Invalid ${FROM} value at position ${i} in action ${each}`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else if (from !== null) {
|
|
206
|
+
let val = from['#']
|
|
207
|
+
if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
|
|
208
|
+
errors.push(`Invalid ${FROM} value in action ${each}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const to = action[TO]
|
|
213
|
+
if (to !== undefined) {
|
|
214
|
+
if (Array.isArray(to)) {
|
|
215
|
+
errors.push(`${TO} must not be an array in action ${each}`)
|
|
216
|
+
} else if (to !== null && to['='] !== FLOW_PREVIOUS) {
|
|
217
|
+
let val = to['#']
|
|
218
|
+
if (!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val))) {
|
|
219
|
+
errors.push(`Invalid ${TO} value in action ${each}`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
171
226
|
/*
|
|
172
227
|
* 1. propagate flows for well-known actions from extensions to definitions
|
|
173
228
|
*/
|
|
@@ -185,11 +240,14 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
185
240
|
}
|
|
186
241
|
}
|
|
187
242
|
|
|
243
|
+
const errors = []
|
|
188
244
|
const to_be_extended = new Set()
|
|
189
245
|
|
|
190
246
|
for (const name in csn.definitions) {
|
|
191
247
|
const def = csn.definitions[name]
|
|
192
248
|
|
|
249
|
+
if (!def.kind || def.kind !== 'entity' || !def.elements || !def.actions) continue
|
|
250
|
+
|
|
193
251
|
/*
|
|
194
252
|
* 2. propagate @flow.status to respective element and make it @readonly
|
|
195
253
|
*/
|
|
@@ -201,8 +259,6 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
201
259
|
}
|
|
202
260
|
}
|
|
203
261
|
|
|
204
|
-
if (!def.kind || def.kind !== 'entity' || !def.actions) continue
|
|
205
|
-
|
|
206
262
|
/*
|
|
207
263
|
* 3. normalize @from and @to annotations
|
|
208
264
|
*/
|
|
@@ -213,7 +269,19 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
213
269
|
}
|
|
214
270
|
|
|
215
271
|
/*
|
|
216
|
-
* 4.
|
|
272
|
+
* 4. validate annotations
|
|
273
|
+
*/
|
|
274
|
+
let status = Object.values(def.elements).filter(e => e['@flow.status'])
|
|
275
|
+
if (status.length === 0) continue
|
|
276
|
+
if (status.length > 1) {
|
|
277
|
+
errors.push(`Entity ${name} has multiple status elements. Only one @flow.status element is allowed per entity`)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
status = status[0]
|
|
281
|
+
_validate_status_types(name, def, status, csn, errors)
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* 5. automatically apply aspect FlowHistory if needed and not present yet
|
|
217
285
|
*/
|
|
218
286
|
if (!_requires_history(def)) continue
|
|
219
287
|
|
|
@@ -226,6 +294,17 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
226
294
|
to_be_extended.add(base_name)
|
|
227
295
|
}
|
|
228
296
|
|
|
297
|
+
/*
|
|
298
|
+
* 6. throw validation errors, if any
|
|
299
|
+
*/
|
|
300
|
+
if (errors.length) {
|
|
301
|
+
if (errors.length === 1) cds.error(errors[0])
|
|
302
|
+
else cds.error('MULTIPLE_ERRORS', { details: errors.map(message => ({ message })) })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* 7. apply the extensions
|
|
307
|
+
*/
|
|
229
308
|
if (to_be_extended.size) {
|
|
230
309
|
// REVISIT: ensure sap.common.FlowHistory is there
|
|
231
310
|
csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory)
|
|
@@ -254,81 +333,9 @@ module.exports = function cds_compile_for_flows(csn) {
|
|
|
254
333
|
csn = dsn
|
|
255
334
|
}
|
|
256
335
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (status === null) return true
|
|
261
|
-
if (enumVals) {
|
|
262
|
-
let val = status['#']
|
|
263
|
-
if (
|
|
264
|
-
!(typeof val === 'string' && Object.entries(enumVals).some(([key]) => key === val)) &&
|
|
265
|
-
!(fromTo === TO && typeof status === 'object' && status['='] === FLOW_PREVIOUS)
|
|
266
|
-
) {
|
|
267
|
-
messages.push(`Invalid ${fromTo} value(s) in action ${action}`);
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
if (typeof status !== 'string' && !(fromTo === TO && typeof status === 'object' && status['='] === FLOW_PREVIOUS)) {
|
|
272
|
-
messages.push(`Invalid ${fromTo} value(s) in action ${action}`)
|
|
273
|
-
return false
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return true
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
for (const name in csn.definitions) {
|
|
281
|
-
const def = csn.definitions[name]
|
|
282
|
-
if (!def.kind || def.kind !== 'entity' || !def.actions || !def.elements) continue
|
|
283
|
-
|
|
284
|
-
const statusElements = Object.values(def.elements).filter(e => e['@flow.status'])
|
|
285
|
-
if (statusElements.length === 0) continue
|
|
286
|
-
if (statusElements.length > 1) {
|
|
287
|
-
messages.push(`Entity ${name} has multiple status elements. Only one @flow.status element is allowed per entity`)
|
|
288
|
-
continue
|
|
289
|
-
}
|
|
290
|
-
const status = statusElements[0]
|
|
291
|
-
|
|
292
|
-
let enumVals
|
|
293
|
-
if (status.type === 'cds.Association') {
|
|
294
|
-
const target = csn.definitions[status.target]
|
|
295
|
-
if (!status.keys || status.keys.length !== 1) {
|
|
296
|
-
messages.push(`Status element in entity ${name} must have exactly one key when used as association`)
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
const code = target.elements[status.keys[0].ref[0]]
|
|
300
|
-
enumVals = code.enum || csn.definitions[code.type]?.enum
|
|
301
|
-
} else if (status.enum) {
|
|
302
|
-
enumVals = status.enum
|
|
303
|
-
} else if (csn.definitions[status.type]?.enum) {
|
|
304
|
-
enumVals = csn.definitions[status.type].enum
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
for (const each in def.actions) {
|
|
308
|
-
const action = def.actions[each]
|
|
309
|
-
let froms = action[FROM]
|
|
310
|
-
if (froms !== undefined) {
|
|
311
|
-
froms = Array.isArray(froms) ? froms : [froms]
|
|
312
|
-
for (let from of froms) if (!validate(from, enumVals, each, FROM)) break
|
|
313
|
-
}
|
|
314
|
-
let tos = action[TO]
|
|
315
|
-
if (tos !== undefined) {
|
|
316
|
-
if (Array.isArray(tos)) {
|
|
317
|
-
messages.push(`${TO} must not be an array in action ${each}`)
|
|
318
|
-
continue
|
|
319
|
-
}
|
|
320
|
-
validate(tos, enumVals, each, TO)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (messages.length) {
|
|
325
|
-
if (messages.length === 1) cds.error(messages[0])
|
|
326
|
-
else {
|
|
327
|
-
const errors = messages.map(message => ({ message }))
|
|
328
|
-
cds.error ('MULTIPLE_ERRORS', { details: errors })
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
336
|
+
/*
|
|
337
|
+
* 8. exclude transitions_ from draft
|
|
338
|
+
*/
|
|
332
339
|
// REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false
|
|
333
340
|
for (const name in csn.definitions)
|
|
334
341
|
if (name.endsWith('.transitions_')) csn.definitions[name]['@odata.draft.enabled'] = false
|
|
@@ -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/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
|