@linkup-ai/abap-ai 2.0.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/README.md +384 -0
- package/dist/adt-client.js +364 -0
- package/dist/cli/activate.js +113 -0
- package/dist/cli/init.js +333 -0
- package/dist/cli/remove.js +80 -0
- package/dist/cli/status.js +229 -0
- package/dist/cli/systems.js +68 -0
- package/dist/cli.js +81 -0
- package/dist/index.js +1318 -0
- package/dist/knowledge/abap/abap-dictionary.md +199 -0
- package/dist/knowledge/abap/abap-sql.md +296 -0
- package/dist/knowledge/abap/amdp.md +273 -0
- package/dist/knowledge/abap/clean-code.md +293 -0
- package/dist/knowledge/abap/cloud-background-processing.md +250 -0
- package/dist/knowledge/abap/cloud-communication.md +265 -0
- package/dist/knowledge/abap/cloud-development.md +176 -0
- package/dist/knowledge/abap/cloud-extensibility.md +252 -0
- package/dist/knowledge/abap/cloud-released-apis.md +261 -0
- package/dist/knowledge/abap/constructor-expressions.md +289 -0
- package/dist/knowledge/abap/enhancements.md +232 -0
- package/dist/knowledge/abap/exceptions.md +271 -0
- package/dist/knowledge/abap/internal-tables.md +205 -0
- package/dist/knowledge/abap/object-orientation.md +298 -0
- package/dist/knowledge/abap/performance.md +216 -0
- package/dist/knowledge/abap/rap-abstract-entities.md +206 -0
- package/dist/knowledge/abap/rap-business-events.md +216 -0
- package/dist/knowledge/abap/rap-draft.md +191 -0
- package/dist/knowledge/abap/rap-eml.md +453 -0
- package/dist/knowledge/abap/rap-end-to-end.md +486 -0
- package/dist/knowledge/abap/rap-feature-control.md +185 -0
- package/dist/knowledge/abap/rap-numbering.md +280 -0
- package/dist/knowledge/abap/rap-service-exposure.md +163 -0
- package/dist/knowledge/abap/rap-unmanaged.md +468 -0
- package/dist/knowledge/abap/string-processing.md +180 -0
- package/dist/knowledge/abap/unit-testing.md +303 -0
- package/dist/knowledge/abap-cds/access-control.md +241 -0
- package/dist/knowledge/abap-cds/annotations.md +331 -0
- package/dist/knowledge/abap-cds/associations.md +254 -0
- package/dist/knowledge/abap-cds/expressions.md +230 -0
- package/dist/knowledge/abap-cds/functions.md +245 -0
- package/dist/knowledge/abap-cds/metadata-extensions.md +294 -0
- package/dist/knowledge/cap/authentication.md +278 -0
- package/dist/knowledge/cap/cdl-syntax.md +247 -0
- package/dist/knowledge/cap/cql-queries.md +266 -0
- package/dist/knowledge/cap/deployment.md +343 -0
- package/dist/knowledge/cap/event-handlers.md +287 -0
- package/dist/knowledge/cap/fiori-integration.md +303 -0
- package/dist/knowledge/cap/service-definitions.md +287 -0
- package/dist/knowledge/fiori/annotations.md +347 -0
- package/dist/knowledge/fiori/deployment.md +340 -0
- package/dist/knowledge/fiori/fiori-elements.md +332 -0
- package/dist/knowledge/fiori/fiori-side-effects.md +107 -0
- package/dist/knowledge/fiori/fiori-valuelist.md +144 -0
- package/dist/knowledge/fiori/ui5-controllers.md +358 -0
- package/dist/knowledge/fiori/ui5-data-binding.md +311 -0
- package/dist/knowledge/fiori/ui5-fragments-dialogs.md +330 -0
- package/dist/knowledge/fiori/ui5-manifest.md +411 -0
- package/dist/knowledge/fiori/ui5-routing.md +303 -0
- package/dist/knowledge/fiori/ui5-xml-views.md +294 -0
- package/dist/logger.js +114 -0
- package/dist/system-profile.js +207 -0
- package/dist/tools/abap-doc.js +72 -0
- package/dist/tools/abapgit.js +161 -0
- package/dist/tools/activate.js +68 -0
- package/dist/tools/atc-check.js +117 -0
- package/dist/tools/auth-object.js +56 -0
- package/dist/tools/breakpoints.js +76 -0
- package/dist/tools/call-hierarchy.js +84 -0
- package/dist/tools/cds-annotations.js +98 -0
- package/dist/tools/cds-dependencies.js +65 -0
- package/dist/tools/check.js +47 -0
- package/dist/tools/code-completion.js +70 -0
- package/dist/tools/code-coverage.js +111 -0
- package/dist/tools/create-amdp.js +111 -0
- package/dist/tools/create-dcl.js +81 -0
- package/dist/tools/create-transport.js +38 -0
- package/dist/tools/create.js +285 -0
- package/dist/tools/data-preview.js +37 -0
- package/dist/tools/delete.js +45 -0
- package/dist/tools/deploy-bsp.js +298 -0
- package/dist/tools/discovery.js +59 -0
- package/dist/tools/element-info.js +93 -0
- package/dist/tools/enhancements.js +186 -0
- package/dist/tools/extract-method.js +44 -0
- package/dist/tools/function-group.js +59 -0
- package/dist/tools/knowledge.js +275 -0
- package/dist/tools/lock-object.js +75 -0
- package/dist/tools/message-class.js +67 -0
- package/dist/tools/navigate.js +80 -0
- package/dist/tools/number-range.js +57 -0
- package/dist/tools/object-documentation.js +43 -0
- package/dist/tools/object-structure.js +78 -0
- package/dist/tools/object-versions.js +57 -0
- package/dist/tools/package-contents.js +60 -0
- package/dist/tools/pretty-printer.js +35 -0
- package/dist/tools/publish-binding.js +49 -0
- package/dist/tools/quick-fix.js +69 -0
- package/dist/tools/read.js +167 -0
- package/dist/tools/refactor-rename.js +60 -0
- package/dist/tools/release-transport.js +24 -0
- package/dist/tools/released-apis.js +51 -0
- package/dist/tools/repository-tree.js +90 -0
- package/dist/tools/scaffold-rap.js +642 -0
- package/dist/tools/search.js +73 -0
- package/dist/tools/shared/data-format.js +101 -0
- package/dist/tools/sql-console.js +17 -0
- package/dist/tools/system-info.js +270 -0
- package/dist/tools/traces.js +66 -0
- package/dist/tools/transport-contents.js +83 -0
- package/dist/tools/transports.js +67 -0
- package/dist/tools/unit-test.js +135 -0
- package/dist/tools/where-used.js +59 -0
- package/dist/tools/write.js +101 -0
- package/package.json +49 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# CDS Metadata Extensions — DDLX for UI annotations
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Separate UI annotations from CDS data model. Requires `@Metadata.allowExtensions: true` on the CDS view.
|
|
6
|
+
|
|
7
|
+
## Basic Metadata Extension
|
|
8
|
+
|
|
9
|
+
```abap
|
|
10
|
+
@Metadata.layer: #CORE
|
|
11
|
+
annotate entity ZC_Travel with
|
|
12
|
+
{
|
|
13
|
+
@UI.facet: [
|
|
14
|
+
{ id: 'Travel', type: #IDENTIFICATION_REFERENCE, label: 'Travel', position: 10 },
|
|
15
|
+
{ id: 'Booking', type: #LINEITEM_REFERENCE, label: 'Bookings', position: 20,
|
|
16
|
+
targetElement: '_Booking' }
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
@UI.hidden: true
|
|
20
|
+
TravelID;
|
|
21
|
+
|
|
22
|
+
@UI: {
|
|
23
|
+
lineItem: [{ position: 10 }],
|
|
24
|
+
selectionField: [{ position: 10 }],
|
|
25
|
+
identification: [{ position: 10 }]
|
|
26
|
+
}
|
|
27
|
+
AgencyID;
|
|
28
|
+
|
|
29
|
+
@UI: {
|
|
30
|
+
lineItem: [{ position: 20 }],
|
|
31
|
+
selectionField: [{ position: 20 }],
|
|
32
|
+
identification: [{ position: 20 }]
|
|
33
|
+
}
|
|
34
|
+
CustomerID;
|
|
35
|
+
|
|
36
|
+
@UI: {
|
|
37
|
+
lineItem: [{ position: 30 }],
|
|
38
|
+
identification: [{ position: 30 }]
|
|
39
|
+
}
|
|
40
|
+
BeginDate;
|
|
41
|
+
|
|
42
|
+
@UI.identification: [{ position: 40 }]
|
|
43
|
+
BookingFee;
|
|
44
|
+
|
|
45
|
+
@UI: {
|
|
46
|
+
lineItem: [{ position: 40 }],
|
|
47
|
+
identification: [{ position: 50 }]
|
|
48
|
+
}
|
|
49
|
+
TotalPrice;
|
|
50
|
+
|
|
51
|
+
@UI: {
|
|
52
|
+
lineItem: [{ position: 50 }],
|
|
53
|
+
selectionField: [{ position: 30 }],
|
|
54
|
+
identification: [{ position: 60 }],
|
|
55
|
+
textArrangement: #TEXT_ONLY
|
|
56
|
+
}
|
|
57
|
+
OverallStatus;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## @Metadata.layer
|
|
62
|
+
|
|
63
|
+
| Layer | Priority | Use |
|
|
64
|
+
|-------|:--------:|-----|
|
|
65
|
+
| `#CORE` | Lowest | Base annotations (application developer) |
|
|
66
|
+
| `#LOCALIZATION` | Low | Language-specific overrides |
|
|
67
|
+
| `#INDUSTRY` | Medium | Industry solution overrides |
|
|
68
|
+
| `#PARTNER` | High | Partner customizations |
|
|
69
|
+
| `#CUSTOMER` | Highest | Customer overrides (wins over all) |
|
|
70
|
+
|
|
71
|
+
Higher layer annotations override lower layer for the same field + annotation.
|
|
72
|
+
|
|
73
|
+
## UI Facets — Object Page Sections
|
|
74
|
+
|
|
75
|
+
```abap
|
|
76
|
+
@UI.facet: [
|
|
77
|
+
" Simple identification section
|
|
78
|
+
{ id: 'General',
|
|
79
|
+
type: #IDENTIFICATION_REFERENCE,
|
|
80
|
+
label: 'General Information',
|
|
81
|
+
position: 10 },
|
|
82
|
+
|
|
83
|
+
" Child entity table
|
|
84
|
+
{ id: 'Items',
|
|
85
|
+
type: #LINEITEM_REFERENCE,
|
|
86
|
+
label: 'Order Items',
|
|
87
|
+
position: 20,
|
|
88
|
+
targetElement: '_Item' },
|
|
89
|
+
|
|
90
|
+
" Field group section
|
|
91
|
+
{ id: 'Pricing',
|
|
92
|
+
type: #FIELDGROUP_REFERENCE,
|
|
93
|
+
label: 'Pricing',
|
|
94
|
+
position: 30,
|
|
95
|
+
targetQualifier: 'PricingGroup' },
|
|
96
|
+
|
|
97
|
+
" Collection (groups sub-facets)
|
|
98
|
+
{ id: 'Admin',
|
|
99
|
+
type: #COLLECTION,
|
|
100
|
+
label: 'Administration',
|
|
101
|
+
position: 40 },
|
|
102
|
+
{ id: 'AdminDates',
|
|
103
|
+
type: #FIELDGROUP_REFERENCE,
|
|
104
|
+
label: 'Dates',
|
|
105
|
+
position: 10,
|
|
106
|
+
parentId: 'Admin',
|
|
107
|
+
targetQualifier: 'DatesGroup' },
|
|
108
|
+
{ id: 'AdminUsers',
|
|
109
|
+
type: #FIELDGROUP_REFERENCE,
|
|
110
|
+
label: 'Users',
|
|
111
|
+
position: 20,
|
|
112
|
+
parentId: 'Admin',
|
|
113
|
+
targetQualifier: 'UsersGroup' }
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Facet Types
|
|
118
|
+
|
|
119
|
+
| Type | Target | Use |
|
|
120
|
+
|------|--------|-----|
|
|
121
|
+
| `#IDENTIFICATION_REFERENCE` | Fields with `@UI.identification` | Main details form |
|
|
122
|
+
| `#LINEITEM_REFERENCE` | Child entity `targetElement` | Child table |
|
|
123
|
+
| `#FIELDGROUP_REFERENCE` | Fields with `@UI.fieldGroup` matching `targetQualifier` | Grouped form |
|
|
124
|
+
| `#COLLECTION` | Sub-facets via `parentId` | Tab/section container |
|
|
125
|
+
| `#DATAPOINT_REFERENCE` | `@UI.dataPoint` matching qualifier | KPI in header |
|
|
126
|
+
| `#STATUS_INFO_REFERENCE` | Status field | Status in header |
|
|
127
|
+
|
|
128
|
+
## UI Header Info
|
|
129
|
+
|
|
130
|
+
```abap
|
|
131
|
+
@UI.headerInfo: {
|
|
132
|
+
typeName: 'Travel',
|
|
133
|
+
typeNamePlural: 'Travels',
|
|
134
|
+
title: { type: #STANDARD, value: 'Description' },
|
|
135
|
+
description: { type: #STANDARD, value: 'TravelID' },
|
|
136
|
+
imageUrl: 'ImageURL'
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Actions in Metadata Extension
|
|
141
|
+
|
|
142
|
+
```abap
|
|
143
|
+
@Metadata.layer: #CORE
|
|
144
|
+
annotate entity ZC_Travel with
|
|
145
|
+
{
|
|
146
|
+
" Action buttons in list table
|
|
147
|
+
@UI.lineItem: [
|
|
148
|
+
{ position: 10 },
|
|
149
|
+
{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept' },
|
|
150
|
+
{ type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject' }
|
|
151
|
+
]
|
|
152
|
+
TravelID;
|
|
153
|
+
|
|
154
|
+
" Action buttons on object page
|
|
155
|
+
@UI.identification: [
|
|
156
|
+
{ position: 10 },
|
|
157
|
+
{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept', position: 1 },
|
|
158
|
+
{ type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject', position: 2 }
|
|
159
|
+
]
|
|
160
|
+
TravelID;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Field Group
|
|
165
|
+
|
|
166
|
+
```abap
|
|
167
|
+
@Metadata.layer: #CORE
|
|
168
|
+
annotate entity ZC_Order with
|
|
169
|
+
{
|
|
170
|
+
@UI.fieldGroup: [{ qualifier: 'PricingGroup', position: 10 }]
|
|
171
|
+
NetAmount;
|
|
172
|
+
|
|
173
|
+
@UI.fieldGroup: [{ qualifier: 'PricingGroup', position: 20 }]
|
|
174
|
+
TaxAmount;
|
|
175
|
+
|
|
176
|
+
@UI.fieldGroup: [{ qualifier: 'PricingGroup', position: 30 }]
|
|
177
|
+
TotalAmount;
|
|
178
|
+
|
|
179
|
+
@UI.fieldGroup: [{ qualifier: 'DatesGroup', position: 10 }]
|
|
180
|
+
CreatedAt;
|
|
181
|
+
|
|
182
|
+
@UI.fieldGroup: [{ qualifier: 'DatesGroup', position: 20 }]
|
|
183
|
+
LastChangedAt;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## DataPoint (Header KPIs)
|
|
188
|
+
|
|
189
|
+
```abap
|
|
190
|
+
@UI.dataPoint: {
|
|
191
|
+
qualifier: 'StatusDP',
|
|
192
|
+
title: 'Status',
|
|
193
|
+
criticality: 'StatusCriticality'
|
|
194
|
+
}
|
|
195
|
+
OverallStatus;
|
|
196
|
+
|
|
197
|
+
@UI.dataPoint: {
|
|
198
|
+
qualifier: 'PriceDP',
|
|
199
|
+
title: 'Total Price'
|
|
200
|
+
}
|
|
201
|
+
TotalPrice;
|
|
202
|
+
|
|
203
|
+
" Reference in header facets
|
|
204
|
+
@UI.facet: [
|
|
205
|
+
{ id: 'HeaderStatus',
|
|
206
|
+
type: #DATAPOINT_REFERENCE,
|
|
207
|
+
purpose: #HEADER,
|
|
208
|
+
targetQualifier: 'StatusDP',
|
|
209
|
+
position: 10 },
|
|
210
|
+
{ id: 'HeaderPrice',
|
|
211
|
+
type: #DATAPOINT_REFERENCE,
|
|
212
|
+
purpose: #HEADER,
|
|
213
|
+
targetQualifier: 'PriceDP',
|
|
214
|
+
position: 20 }
|
|
215
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Criticality
|
|
219
|
+
|
|
220
|
+
```abap
|
|
221
|
+
" Computed field in CDS (or virtual field set in handler)
|
|
222
|
+
case overall_status
|
|
223
|
+
when 'O' then 2 " Open = yellow (Critical)
|
|
224
|
+
when 'A' then 3 " Accepted = green (Positive)
|
|
225
|
+
when 'X' then 1 " Rejected = red (Negative)
|
|
226
|
+
else 0 " grey (Neutral)
|
|
227
|
+
end as StatusCriticality,
|
|
228
|
+
|
|
229
|
+
" Referenced in annotation
|
|
230
|
+
@UI.lineItem: [{ position: 50, criticality: 'StatusCriticality' }]
|
|
231
|
+
OverallStatus;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Value | Color | Meaning |
|
|
235
|
+
|:-----:|-------|---------|
|
|
236
|
+
| 0 | Grey | Neutral |
|
|
237
|
+
| 1 | Red | Negative/Error |
|
|
238
|
+
| 2 | Yellow | Critical/Warning |
|
|
239
|
+
| 3 | Green | Positive/Success |
|
|
240
|
+
| 5 | Blue | Information |
|
|
241
|
+
|
|
242
|
+
## Complete Child Metadata Extension
|
|
243
|
+
|
|
244
|
+
```abap
|
|
245
|
+
@Metadata.layer: #CORE
|
|
246
|
+
annotate entity ZC_Booking with
|
|
247
|
+
{
|
|
248
|
+
@UI.facet: [
|
|
249
|
+
{ id: 'Booking', type: #IDENTIFICATION_REFERENCE, label: 'Booking Details', position: 10 }
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
@UI.hidden: true
|
|
253
|
+
TravelID;
|
|
254
|
+
|
|
255
|
+
@UI.hidden: true
|
|
256
|
+
BookingID;
|
|
257
|
+
|
|
258
|
+
@UI: { lineItem: [{ position: 10 }], identification: [{ position: 10 }] }
|
|
259
|
+
BookingDate;
|
|
260
|
+
|
|
261
|
+
@UI: { lineItem: [{ position: 20 }], identification: [{ position: 20 }] }
|
|
262
|
+
CarrierID;
|
|
263
|
+
|
|
264
|
+
@UI: { lineItem: [{ position: 30 }], identification: [{ position: 30 }] }
|
|
265
|
+
ConnectionID;
|
|
266
|
+
|
|
267
|
+
@UI: { lineItem: [{ position: 40 }], identification: [{ position: 40 }] }
|
|
268
|
+
FlightDate;
|
|
269
|
+
|
|
270
|
+
@UI: { lineItem: [{ position: 50 }], identification: [{ position: 50 }] }
|
|
271
|
+
FlightPrice;
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Rules
|
|
276
|
+
- CDS view MUST have `@Metadata.allowExtensions: true`
|
|
277
|
+
- Metadata Extension annotates **projection CDS** (ZC_*), not interface (ZI_*)
|
|
278
|
+
- `@Metadata.layer: #CORE` is the standard for application development
|
|
279
|
+
- Number positions by 10s for easy insertion
|
|
280
|
+
- `@UI.facet` goes on the FIRST field (or any field — it's entity-level)
|
|
281
|
+
- `parentId` links sub-facets to a `#COLLECTION` facet
|
|
282
|
+
- `targetElement` for child tables matches the association name (e.g., `_Booking`)
|
|
283
|
+
- `targetQualifier` matches the `qualifier` in `@UI.fieldGroup`
|
|
284
|
+
|
|
285
|
+
## Anti-Patterns
|
|
286
|
+
| Anti-Pattern | Correct |
|
|
287
|
+
|---|---|
|
|
288
|
+
| UI annotations in interface CDS (ZI_*) | Metadata Extension on projection (ZC_*) |
|
|
289
|
+
| Missing `@Metadata.allowExtensions` on CDS | Extension silently ignored |
|
|
290
|
+
| `@UI.facet` with `targetElement` that doesn't match association name | Use exact association name |
|
|
291
|
+
| Positions 1, 2, 3 | Use 10, 20, 30 for insertion gaps |
|
|
292
|
+
| Actions in `@UI.fieldGroup` | Actions in `@UI.lineItem` or `@UI.identification` with `type: #FOR_ACTION` |
|
|
293
|
+
| `#FIELDGROUP_REFERENCE` without matching `targetQualifier` | Qualifier must match `@UI.fieldGroup` qualifier |
|
|
294
|
+
| Multiple `@UI.facet` on different fields | Put all facets in ONE `@UI.facet` array on one field |
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# CAP Authentication — XSUAA, roles, scopes, instance-level auth
|
|
2
|
+
|
|
3
|
+
## Enable Authentication
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
// package.json
|
|
7
|
+
{
|
|
8
|
+
"cds": {
|
|
9
|
+
"requires": {
|
|
10
|
+
"auth": {
|
|
11
|
+
"kind": "xsuaa",
|
|
12
|
+
"[development]": {
|
|
13
|
+
"kind": "mocked",
|
|
14
|
+
"users": {
|
|
15
|
+
"admin": { "password": "admin", "roles": ["admin", "viewer"] },
|
|
16
|
+
"viewer": { "password": "viewer", "roles": ["viewer"] },
|
|
17
|
+
"carol": { "password": "carol", "roles": [], "attr": { "Country": "DE" } }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Service-Level Access Control
|
|
27
|
+
|
|
28
|
+
```cds
|
|
29
|
+
// Entire service requires authentication
|
|
30
|
+
service AdminService @(requires: 'admin') {
|
|
31
|
+
entity Books as projection on my.Books;
|
|
32
|
+
entity Authors as projection on my.Authors;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Public read, auth for write
|
|
36
|
+
service CatalogService @(requires: 'authenticated-user') {
|
|
37
|
+
@readonly
|
|
38
|
+
entity Books as projection on my.Books;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No auth (public API)
|
|
42
|
+
service PublicService {
|
|
43
|
+
@readonly
|
|
44
|
+
entity Products as projection on my.Products;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Entity-Level Access Control
|
|
49
|
+
|
|
50
|
+
```cds
|
|
51
|
+
service OrderService {
|
|
52
|
+
// Only admin can manage orders
|
|
53
|
+
@requires: 'admin'
|
|
54
|
+
entity Orders as projection on my.Orders;
|
|
55
|
+
|
|
56
|
+
// Multiple roles (any matches)
|
|
57
|
+
@requires: ['admin', 'support']
|
|
58
|
+
action cancelAllOrders();
|
|
59
|
+
|
|
60
|
+
// Read-only for authenticated users
|
|
61
|
+
@readonly @requires: 'authenticated-user'
|
|
62
|
+
entity Products as projection on my.Products;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Instance-Level Access (Row-Based)
|
|
67
|
+
|
|
68
|
+
```cds
|
|
69
|
+
// Restrict based on user role + attributes
|
|
70
|
+
annotate OrderService.Orders with @(restrict: [
|
|
71
|
+
// Admin: full access
|
|
72
|
+
{ grant: '*', to: 'admin' },
|
|
73
|
+
|
|
74
|
+
// Viewer: read only
|
|
75
|
+
{ grant: 'READ', to: 'viewer' },
|
|
76
|
+
|
|
77
|
+
// Support: read + update
|
|
78
|
+
{ grant: ['READ', 'UPDATE'], to: 'support' },
|
|
79
|
+
|
|
80
|
+
// Country-based: only see orders from user's country
|
|
81
|
+
{ grant: 'READ', where: 'country = $user.Country' },
|
|
82
|
+
|
|
83
|
+
// Own records: user sees only what they created
|
|
84
|
+
{ grant: 'READ', where: 'createdBy = $user' },
|
|
85
|
+
|
|
86
|
+
// Combined: role + attribute
|
|
87
|
+
{ grant: ['READ', 'UPDATE'], to: 'regional_admin',
|
|
88
|
+
where: 'country = $user.Country' }
|
|
89
|
+
]);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## xs-security.json — Scopes and Roles
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"xsappname": "myapp",
|
|
97
|
+
"tenant-mode": "dedicated",
|
|
98
|
+
"scopes": [
|
|
99
|
+
{ "name": "$XSAPPNAME.admin", "description": "Admin" },
|
|
100
|
+
{ "name": "$XSAPPNAME.viewer", "description": "Viewer" },
|
|
101
|
+
{ "name": "$XSAPPNAME.support", "description": "Support" }
|
|
102
|
+
],
|
|
103
|
+
"attributes": [
|
|
104
|
+
{ "name": "Country", "description": "Country", "valueType": "string" },
|
|
105
|
+
{ "name": "CostCenter", "description": "Cost Center", "valueType": "string" }
|
|
106
|
+
],
|
|
107
|
+
"role-templates": [
|
|
108
|
+
{
|
|
109
|
+
"name": "Admin",
|
|
110
|
+
"description": "Full access",
|
|
111
|
+
"scope-references": ["$XSAPPNAME.admin", "$XSAPPNAME.viewer"]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "Viewer",
|
|
115
|
+
"description": "Read-only",
|
|
116
|
+
"scope-references": ["$XSAPPNAME.viewer"]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "RegionalAdmin",
|
|
120
|
+
"description": "Admin for specific country",
|
|
121
|
+
"scope-references": ["$XSAPPNAME.admin"],
|
|
122
|
+
"attribute-references": ["Country"]
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"role-collections": [
|
|
126
|
+
{
|
|
127
|
+
"name": "MyApp_Admin",
|
|
128
|
+
"role-template-references": ["$XSAPPNAME.Admin"]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "MyApp_Viewer",
|
|
132
|
+
"role-template-references": ["$XSAPPNAME.Viewer"]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Accessing User Info in Handlers
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
this.before('*', (req) => {
|
|
142
|
+
const { user } = req;
|
|
143
|
+
|
|
144
|
+
// User ID
|
|
145
|
+
console.log(user.id); // 'admin@company.com'
|
|
146
|
+
|
|
147
|
+
// Role check
|
|
148
|
+
if (user.is('admin')) { /* full access */ }
|
|
149
|
+
if (user.is('viewer')) { /* read only */ }
|
|
150
|
+
|
|
151
|
+
// User attributes
|
|
152
|
+
const country = user.attr.Country; // 'DE'
|
|
153
|
+
const costCenter = user.attr.CostCenter;
|
|
154
|
+
|
|
155
|
+
// Tenant
|
|
156
|
+
console.log(req.tenant); // 't1' (multi-tenant)
|
|
157
|
+
|
|
158
|
+
// Locale
|
|
159
|
+
console.log(req.locale); // 'en'
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Custom Authorization in Handler
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
this.before('UPDATE', 'Orders', async (req) => {
|
|
167
|
+
// Only order owner or admin can update
|
|
168
|
+
const order = await SELECT.one.from('Orders', req.data.ID);
|
|
169
|
+
if (!order) return req.reject(404, 'Order not found');
|
|
170
|
+
|
|
171
|
+
if (order.createdBy !== req.user.id && !req.user.is('admin')) {
|
|
172
|
+
return req.reject(403, 'Only the order creator or admin can modify this order');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.before('DELETE', 'Orders', async (req) => {
|
|
177
|
+
// Admin only
|
|
178
|
+
if (!req.user.is('admin')) {
|
|
179
|
+
return req.reject(403, 'Only administrators can delete orders');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Attribute-based filtering
|
|
184
|
+
this.before('READ', 'Orders', (req) => {
|
|
185
|
+
if (!req.user.is('admin')) {
|
|
186
|
+
// Non-admin users only see their country's orders
|
|
187
|
+
const country = req.user.attr.Country;
|
|
188
|
+
if (country) {
|
|
189
|
+
req.query.where({ country });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Mock Users for Development
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
// package.json — mocked auth
|
|
199
|
+
{
|
|
200
|
+
"cds": {
|
|
201
|
+
"requires": {
|
|
202
|
+
"auth": {
|
|
203
|
+
"[development]": {
|
|
204
|
+
"kind": "mocked",
|
|
205
|
+
"users": {
|
|
206
|
+
"alice": {
|
|
207
|
+
"password": "alice",
|
|
208
|
+
"roles": ["admin", "viewer"],
|
|
209
|
+
"attr": { "Country": "DE" }
|
|
210
|
+
},
|
|
211
|
+
"bob": {
|
|
212
|
+
"password": "bob",
|
|
213
|
+
"roles": ["viewer"],
|
|
214
|
+
"attr": { "Country": "US" }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"[production]": {
|
|
219
|
+
"kind": "xsuaa"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Test with mock user
|
|
229
|
+
curl -u alice:alice http://localhost:4004/admin/Books
|
|
230
|
+
|
|
231
|
+
# Or in browser: login prompt with alice/alice
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Mapping CDS Roles to XSUAA Scopes
|
|
235
|
+
|
|
236
|
+
| CDS `@requires` | xs-security.json scope |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `'admin'` | `$XSAPPNAME.admin` |
|
|
239
|
+
| `'viewer'` | `$XSAPPNAME.viewer` |
|
|
240
|
+
| `'authenticated-user'` | Any valid JWT token (built-in) |
|
|
241
|
+
| `'system-user'` | Technical user (built-in) |
|
|
242
|
+
|
|
243
|
+
CAP auto-maps: `@requires: 'admin'` → checks for scope `$XSAPPNAME.admin` in JWT.
|
|
244
|
+
|
|
245
|
+
## BTP Role Collection Assignment
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# After deployment, assign users to role collections in BTP Cockpit:
|
|
249
|
+
# 1. BTP Cockpit → Subaccount → Security → Role Collections
|
|
250
|
+
# 2. Select "MyApp_Admin"
|
|
251
|
+
# 3. Add user: alice@company.com
|
|
252
|
+
# 4. Save
|
|
253
|
+
|
|
254
|
+
# Or via CLI:
|
|
255
|
+
cf create-service-key myapp-auth myapp-auth-key
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Rules
|
|
259
|
+
- `@requires: 'authenticated-user'` for any logged-in user
|
|
260
|
+
- `@requires: 'admin'` maps to scope `$XSAPPNAME.admin` in xs-security.json
|
|
261
|
+
- `@restrict` for instance-level (row-based) authorization
|
|
262
|
+
- `$user` in `where` clause resolves to `req.user.id`
|
|
263
|
+
- `$user.Country` resolves to user attribute `Country`
|
|
264
|
+
- Mock users in `[development]` profile — XSUAA in `[production]`
|
|
265
|
+
- Role collections assigned in BTP Cockpit after deployment
|
|
266
|
+
- Always test with mock users before deploying to BTP
|
|
267
|
+
|
|
268
|
+
## Anti-Patterns
|
|
269
|
+
| Anti-Pattern | Correct |
|
|
270
|
+
|---|---|
|
|
271
|
+
| Hardcoded user checks in handler | Use `@requires` + `@restrict` declaratively |
|
|
272
|
+
| `@requires` on service but sensitive entities open | Add `@requires` on entity level too |
|
|
273
|
+
| Missing `[production]` XSUAA config | Mocked auth in production = no security |
|
|
274
|
+
| `$XSAPPNAME.admin` hardcoded in handler | Use `req.user.is('admin')` |
|
|
275
|
+
| No mock users in development | Define mock users for local testing |
|
|
276
|
+
| Role collection without role template | Role collections reference role templates |
|
|
277
|
+
| Checking `req.user` without auth middleware | Ensure `"auth"` is in `cds.requires` |
|
|
278
|
+
| Missing `forwardAuthToken` in App Router | Auth token not propagated to CAP server |
|