diff --git a/.vitepress/config.js b/.vitepress/config.js index 863c6faf1d..7b39a57aef 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -106,8 +106,8 @@ config.rewrites = rewrites // Add custom capire info to the theme config config.themeConfig.capire = { versions: { - java_services: '4.5.0', - java_cds4j: '4.5.0' + java_services: '4.6.0', + java_cds4j: '4.6.0' }, gotoLinks: [] } diff --git a/.vitepress/theme/components/Alpha.vue b/.vitepress/theme/components/Alpha.vue index 598d845384..c1b17bfd3f 100644 --- a/.vitepress/theme/components/Alpha.vue +++ b/.vitepress/theme/components/Alpha.vue @@ -1,7 +1,7 @@ diff --git a/.vitepress/theme/components/Beta.vue b/.vitepress/theme/components/Beta.vue index b76d53cfd9..426e16889b 100644 --- a/.vitepress/theme/components/Beta.vue +++ b/.vitepress/theme/components/Beta.vue @@ -1,7 +1,7 @@ diff --git a/.vitepress/theme/components/Concept.vue b/.vitepress/theme/components/Concept.vue index 06c9e3c028..07a331b80a 100644 --- a/.vitepress/theme/components/Concept.vue +++ b/.vitepress/theme/components/Concept.vue @@ -1,7 +1,7 @@ diff --git a/.vitepress/theme/components/Gamma.vue b/.vitepress/theme/components/Gamma.vue new file mode 100644 index 0000000000..ecac65c55c --- /dev/null +++ b/.vitepress/theme/components/Gamma.vue @@ -0,0 +1,7 @@ + diff --git a/.vitepress/theme/components/Since.vue b/.vitepress/theme/components/Since.vue index 1790249c43..8fe8e381ee 100644 --- a/.vitepress/theme/components/Since.vue +++ b/.vitepress/theme/components/Since.vue @@ -1,5 +1,9 @@ \ No newline at end of file diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 10cb7a185c..aab94a8784 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -3,8 +3,10 @@ import { EnhanceAppContext } from 'vitepress'; import Layout from './Layout.vue'; import IndexList from './components/IndexList.vue'; import ImplVariantsHint from './components/implvariants/ImpVariantsHint.vue'; +import StatusBadge from './components/StatusBadge.vue'; import Alpha from './components/Alpha.vue'; import Beta from './components/Beta.vue'; +import Gamma from './components/Gamma.vue'; import Concept from './components/Concept.vue' import Since from './components/Since.vue'; import UnderConstruction from './components/UnderConstruction.vue'; @@ -25,8 +27,10 @@ export default { ctx.app.component('Config', CfgInspect) ctx.app.component('IndexList', IndexList) ctx.app.component('ImplVariantsHint', ImplVariantsHint) + ctx.app.component('StatusBadge', StatusBadge) ctx.app.component('Alpha', Alpha) ctx.app.component('Beta', Beta) + ctx.app.component('Gamma', Gamma) ctx.app.component('Concept', Concept) ctx.app.component('Since', Since) ctx.app.component('UnderConstruction', UnderConstruction) diff --git a/.vitepress/theme/styles.scss b/.vitepress/theme/styles.scss index e4e2722169..bbb344c3f5 100644 --- a/.vitepress/theme/styles.scss +++ b/.vitepress/theme/styles.scss @@ -535,7 +535,19 @@ a.learn-more, p.learn-more, .learn-more { } } -.VPBadge { white-space: nowrap; } +.VPBadge { + &.tip { + background-color: #ced; color: #031; .dark & { + background-color: #031; color: #9ca; + } + } + margin-top: 3px !important; + padding: 0 0.7em !important; + font-size: 80%; + line-height: 1.5em; + border-radius: 1.5em; + white-space: nowrap; +} html.java { & .node { diff --git a/about/features.md b/about/features.md index fb0f76820c..e9b320bb16 100644 --- a/about/features.md +++ b/about/features.md @@ -115,7 +115,7 @@ Following is an index of the features currently covered by CAP, with status and | [Streaming & Media Types](../guides/providing-services#serving-media-data) | | | | | [Conflict Detection through _ETags_](../guides/providing-services#etag) | | | | | [Authentication via JWT](../guides/security/authorization#prerequisite-authentication) | | | | -| [Basic Authentication](../guides/security/authorization#prerequisite-authentication) | | | | +| [Mocked Authentication](../guides/security/authorization#prerequisite-authentication) | | | |
diff --git a/advanced/assets/hierarchical-tree-view.png b/advanced/assets/hierarchical-tree-view.png new file mode 100644 index 0000000000..aaf46c64e4 Binary files /dev/null and b/advanced/assets/hierarchical-tree-view.png differ diff --git a/advanced/fiori.md b/advanced/fiori.md index 1c96d54e1e..1f42cc9e1d 100644 --- a/advanced/fiori.md +++ b/advanced/fiori.md @@ -745,9 +745,9 @@ Cache Control feature is currently supported on the Java runtime only. ## Hierarchical Tree Views -Recursive hierarchies are parent-child hierarchies, where each entity references its parent and through that defines the hierarchical structure. A common example is a company organization structure or HR reporting, where each employee entity references another employee as a direct report or manager. +Recursive hierarchies are parent-child related structures: each entity references its parent and through that defines the hierarchical structure. A common example is a company organization structure or HR reporting, where each employee entity references another employee as a direct report or manager. -A generic hierarchy implementation for hierarchies is available on all relational datases supported by the CAP runtimes. +A generic hierarchy implementation for hierarchies is available on all relational databases supported by the CAP runtimes. ::: warning On H2, only small hierarchies should be used for performance reasons. @@ -761,6 +761,7 @@ Let's assume we have the following domain model and its projection in a service: namespace my.bookshop; entity Genres { //... + ID : UUID; parent : Association to Genres; } ``` @@ -774,19 +775,67 @@ service AdminService { ``` ::: +In this example, there is a managed to-one association `parent` that defines the parent-child hierarchy +based on a single key element. In such a situation you can define the Tree View via the annotation `@hierarchy`: -Annotate/extend the entity in the service as follows: +```cds +annotate AdminService.Genres with @hierarchy : parent; +``` + +If the entity contains only one such association, you can even omit the value: + +```cds +annotate AdminService.Genres with @hierarchy; +``` + +Configure the TreeTable in UI5's _manifest.json_ file: + +```jsonc + "sap.ui5": { ... + "routing": { ... + "targets": { ... + "GenresList": { ... + "options": { + "settings": { ... + "controlConfiguration": { + "@com.sap.vocabularies.UI.v1.LineItem": { + "tableSettings": { + "hierarchyQualifier": "GenresHierarchy", // [!code focus] + "type": "TreeTable" // [!code focus] + } + } + } + } + } + }, + }, + }, +``` + +> Note: construct the `hierarchyQualifier` with the following pattern:
+> `Hierarchy` + +You can now start the server with `cds watch` and see the hierarchical tree view in action in the [_Browse Genres_](http://localhost:4004/fiori-apps.html#Genres-display) app. + +![Fiori UI with hierarchical tree view.](assets/hierarchical-tree-view.png) {style="filter: drop-shadow(0 2px 5px rgba(0,0,0,.40));"} + +The compiler automatically expands the shortcut annotation `@hierarchy` to the +following `annotate` and `extend` statements. + +### Manual Approach + +The following documents what happens behind the scenes, done by the compiler as described before. You can also use it, if you cannot use the `@hierarchy` annotation, for example, because you only have an unmanaged parent association. ```cds // declare a hierarchy with the qualifier "GenresHierarchy" -annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy : { +annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy: { NodeProperty : ID, // identifies a node, usually the key ParentNavigationProperty : parent // navigates to a node's parent }; extend AdminService.Genres with @( // The computed properties expected by Fiori to be present in hierarchy entities - Hierarchy.RecursiveHierarchy #GenresHierarchy : { + Hierarchy.RecursiveHierarchy #GenresHierarchy: { LimitedDescendantCount : LimitedDescendantCount, DistanceFromRoot : DistanceFromRoot, DrillState : DrillState, @@ -797,7 +846,7 @@ extend AdminService.Genres with @( 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ], // Disallow sorting on these properties from Fiori UIs - Capabilities.SortRestrictions.NonSortableProperties : [ + Capabilities.SortRestrictions.NonSortableProperties: [ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ], ) columns { // Ensure we can query these columns from the database @@ -811,31 +860,5 @@ extend AdminService.Genres with @( > Note: When naming the hierarchy qualifier, use the following pattern:
> `Hierarchy` -Configure the TreeTable in UI5's _manifest.json_ file: - -```jsonc - "sap.ui5": { ... - "routing": { ... - "targets": { ... - "GenresList": { ... - "options": { - "settings": { ... - "controlConfiguration": { - "@com.sap.vocabularies.UI.v1.LineItem": { - "tableSettings": { - "hierarchyQualifier": "GenresHierarchy", // [!code focus] - "type": "TreeTable" // [!code focus] - } - } - } - } - } - }, - }, - }, -``` - -> Note: use the `hierarchyQualifier` declared earlier -
diff --git a/advanced/publishing-apis/openapi.md b/advanced/publishing-apis/openapi.md index 3d535a07cf..b2aa7ed5a1 100644 --- a/advanced/publishing-apis/openapi.md +++ b/advanced/publishing-apis/openapi.md @@ -97,11 +97,15 @@ See [Frequently Asked Questions](#faq) for examples on how to use these annotati | `Description` | Action, ActionImport, Function, FunctionImport | `summary` of Operation Object | | `Description` | EntitySet, Singleton | `description` of Tag Object | | `Description` | EntityType | `title` of Request Body Object | -| `Description` | ComplexType, EntityType, EnumerationType, Parameter, Property, TypeDefinition | `title` of Schema Object | +| `Description` | ComplexType, EntityType, EnumerationType, TypeDefinition | `title` of Schema Object | +| `Description` | Parameter | `description` of Parameter Object (fallback if `LongDescription` not present) | +| `Description` | Property | `description` of Schema Object (fallback if `LongDescription` not present) | | `Description` | Schema, EntityContainer | `info.title` | | `Example` | Property | `example` of Schema Object | | `Immutable` | Property | omit from Update structure | | `LongDescription` | Action, ActionImport, Function, FunctionImport | `description` of Operation Object | +| `LongDescription` | Parameter | `description` of Parameter Object | +| `LongDescription` | Property | `description` of Schema Object | | `LongDescription` | Schema, EntityContainer | `info.description` | | `Permissions:Read` | Property | omit read-only properties from Create and Update structures | | `SchemaVersion` | Schema | `info.version` | diff --git a/cds/annotations.md b/cds/annotations.md index 5903310a28..7a5c6a0be0 100644 --- a/cds/annotations.md +++ b/cds/annotations.md @@ -36,11 +36,11 @@ uacp: Used as link target from Help Portal at https://help.sap.com/products/BTP/ | Annotation | Description | |---------------------|----------------------------------------------------------------------| -| `@readonly ` | see [Input Validation](../guides/providing-services#readonly) | -| `@mandatory` | see [Input Validation](../guides/providing-services#mandatory) | -| `@assert.target` | see [Input Validation](../guides/providing-services#assert-target) | -| `@assert.format` | see [Input Validation](../guides/providing-services#assert-format) | -| `@assert.range` | see [Input Validation](../guides/providing-services#assert-range) | +| `@readonly ` | see [Input Validation](../guides/services/constraints#readonly) | +| `@mandatory` | see [Input Validation](../guides/services/constraints#mandatory) | +| `@assert.target` | see [Input Validation](../guides/services/constraints#assert-target) | +| `@assert.format` | see [Input Validation](../guides/services/constraints#assert-format) | +| `@assert.range` | see [Input Validation](../guides/services/constraints#assert-range) | @@ -91,8 +91,8 @@ Intrinsically supported OData Annotations: | Annotation | Description | |------------------------|------------------------------------------------------------------| -| `@Core.Computed` | see [Providing Services](../guides/providing-services#readonly) | -| `@Core.Immutable` | see [Providing Services](../guides/providing-services#readonly) | +| `@Core.Computed` | see [Providing Services](../guides/services/constraints#readonly) | +| `@Core.Immutable` | see [Providing Services](../guides/services/constraints#readonly) | | `@Core.MediaType` | see [Media Data](../guides/providing-services#serving-media-data) | | `@Core.IsMediaType` | see [Media Data](../guides/providing-services#serving-media-data) | | `@Core.IsUrl` | see [Media Data](../guides/providing-services#serving-media-data) | diff --git a/cds/cdl.md b/cds/cdl.md index 39fb01b314..68b3a587a9 100644 --- a/cds/cdl.md +++ b/cds/cdl.md @@ -482,7 +482,7 @@ entity Bar { An element definition can be prefixed with modifier keyword `virtual`. This keyword indicates that this element isn't added to persistent artifacts, that is, tables or views in SQL databases. Virtual elements are part of OData metadata. -By default, virtual elements are annotated with `@Core.Computed: true`, not writable for the client and will be [silently ignored](../guides/providing-services#readonly). This means also, that they are not accessible in custom event handlers. If you want to make virtual elements writable for the client, you explicitly need to annotate these elements with `@Core.Computed: false`. Still those elements are not persisted and therefore, for example, not sortable or filterable. Further, during read requests, you need to provide values for all virtual elements. You can do this by using post-processing in an `after` handler. +By default, virtual elements are annotated with `@Core.Computed: true`, not writable for the client and will be [silently ignored](../guides/services/constraints#readonly). This means also, that they are not accessible in custom event handlers. If you want to make virtual elements writable for the client, you explicitly need to annotate these elements with `@Core.Computed: false`. Still those elements are not persisted and therefore, for example, not sortable or filterable. Further, during read requests, you need to provide values for all virtual elements. You can do this by using post-processing in an `after` handler. ```cds entity Employees { @@ -639,7 +639,7 @@ type Complex { If the element has an enum type, you can use the enum symbol instead of a literal value: ```cds type Status : String enum {open; closed;} -entity Order { +entity Orders { status : Status default #open; } ``` @@ -650,7 +650,7 @@ entity Order { If you want to base an element's type on another element of the same structure, you can use the `type of` operator. ```cds -entity Author { +entity Authors { firstname : String(100); lastname : type of firstname; // has type "String(100)" } @@ -685,7 +685,7 @@ For string types, declaration of actual values is optional; if omitted, the actu ```cds type Gender : String enum { male; female; non_binary = 'non-binary'; } -entity Order { +entity Orders { status : Integer enum { submitted = 1; fulfilled = 2; @@ -695,7 +695,7 @@ entity Order { } ``` -To enforce your _enum_ values during runtime, use the [`@assert.range` annotation](../guides/providing-services#assert-range). +To enforce your _enum_ values during runtime, use the [`@assert.range` annotation](../guides/services/constraints#assert-range). For localization of enum values, model them as [code list](./common#adding-own-code-lists).
@@ -856,7 +856,51 @@ Result result = service.run(Select.from("UsingView"), params); [Learn more about how to expose views with parameters in **Services - Exposed Entities**.](#exposed-entities){ .learn-more} [Learn more about views with parameters for existing HANA artifacts in **Native SAP HANA Artifacts**.](../advanced/hana){ .learn-more} +### Runtime Views { #runtimeviews } + +To add or update CDS views without redeploying the database schema, annotate them with [@cds.persistence.skip](../guides/databases#cds-persistence-skip). This advises the CDS compiler to skip generating database views for these CDS views. Instead, CAP resolves them *at runtime* on each request. + +Runtime views must be simple [projections](#as-projection-on), not using *aggregations*, *join*, *union* or *subqueries* in the *from* clause, but may have a *where* condition if they are only used to read. + +In CAP Java, runtime views are enabled by default, in Node.js enable them via cds.features.runtime_views: true. +[Learn more about runtime views in CAP Java.](../java/working-with-cql/query-execution#runtimeviews) {.learn-more} + +By default, runtime views are translated into _Common Table Expressions_ (CTEs) and sent with the query to the database. + +For example, given the following CDS model and query: + +```cds +entity Books { + key ID : UUID; + title : String; + stock : Integer; + author : Association to one Authors; +} +@cds.persistence.skip +entity BooksWithLowStock as projection on Books { + ID, title, author.name as author +} where stock < 10; // makes the view read only +``` +```sql +SELECT from BooksWithLowStock where author = 'Kafka' +``` + +The runtime translates the view definition into a _Common Table Expression_ (CTE) and sends it with the query to the database. + +```sql +WITH BOOKSWITHLOWSTOCK_CTE AS ( + SELECT B.ID, + B.TITLE, + A.NAME AS "AUTHOR" + FROM BOOKS B + LEFT OUTER JOIN AUTHOR A ON B.AUTHOR_ID = A.ID + WHERE B.STOCK < 10 +) +SELECT ID, TITLE, AUTHOR AS "author" + FROM BOOKSWITHLOWSTOCK_CTE + WHERE A.NAME = ? +``` ## Associations @@ -951,6 +995,7 @@ entity Emp2Addr { ``` [Learn more about **Managed Compositions for Many-to-many Relationships**.](#for-many-to-many-relationships){.learn-more} +[Watch a short video by DJ Adams to see an example of how a link entity can be used.](https://www.youtube.com/shorts/yGg3YD1weIA){.learn-more}
@@ -979,13 +1024,14 @@ entity Orders.Items { ``` :::info Contained-in relationship -Essentially, Compositions are the same as _[associations](#associations)_, just with the additional information that this association represents a _contained-in_ relationship so the same syntax and rules apply in their base form. +Essentially, Compositions are the same as _[associations](#associations)_, just with the additional information that this association represents a _contained-in_ relationship; so the same syntax and rules apply in their base form. ::: ::: warning Limitations of Compositions of one Using compositions of one for entities is discouraged. There is often no added value of using them as the information can be placed in the root entity. Compositions of one have limitations as follow: - Very limited Draft support. Fiori elements does not support compositions of one unless you take care of their creation in a custom handler. - No extensive support for modifications over paths if compositions of one are involved. You must fill in foreign keys manually in a custom handler. +See the [Keep it Simple, Stupid](/guides/domain-modeling#keep-it-simple-stupid) best practice, especially the [Prefer Flat Models](/guides/domain-modeling#prefer-flat-models) section. ::: ### Managed Compositions of Aspects {#managed-compositions} @@ -1038,7 +1084,7 @@ aspect OrderItems { #### Default Target Cardinality -If not otherwise specified, a managed composition of an aspect has the default target cardinality *to-one*. +If not otherwise specified, a managed composition of an aspect has the default target cardinality *to-one* for the backlink. #### For Many-to-many Relationships @@ -1870,14 +1916,14 @@ exposing entities. ```cds service CatalogService { - entity Product as projection on data.Products { + entity Products as projection on data.Products { *, created.at as since } excluding { created }; } service MyOrders { //> $user only implemented for SAP HANA - entity Order as select from data.Orders { * } where buyer=$user.id; - entity Product as projection on CatalogService.Product; + entity Orders as select from data.Orders { * } where buyer=$user.id; + entity Products as projection on CatalogService.Products; } ``` @@ -2050,7 +2096,7 @@ Within service definitions, you can additionally specify `actions` and `function ```cds service MyOrders { - entity Order { /*...*/ }; + entity Orders { /*...*/ }; // unbound actions / functions type cancelOrderRet { acknowledge: String enum { succeeded; failed; }; diff --git a/cds/cxl.md b/cds/cxl.md new file mode 100644 index 0000000000..f9734ff3d9 --- /dev/null +++ b/cds/cxl.md @@ -0,0 +1,12 @@ +--- +status: released +--- + +# CDS Expression Language (CXL) + +This document provides an overview of the CDS Expression Language (CXL) used in CDS models and queries. +CXL is essentially a standard subset of SQL expressions enhanced by [Path Expressions](../cds/cql#path-expressions) with [Infix Filters](../cds/cql#with-infix-filters). + +The documentation is still **under construction** and will be released soon. + +For the time being, please refer to the existing reference docs about [CDS Query Language (CQL)](../cds/cql), and to the [CDS Expression Notation (CXN)](../cds/cxn) documentation. diff --git a/cds/types.md b/cds/types.md index 12d16b5604..2148b31280 100644 --- a/cds/types.md +++ b/cds/types.md @@ -41,7 +41,7 @@ These types are used to define the structure of entities and services, and are m | `Timestamp` | _µs_ precision, with up to 7 fractional digits | _TIMESTAMP_ | | `String` (`length`) | Default *length*: 255; on HANA: 5000 (4)(5) | _NVARCHAR_ | | `Binary` (`length`) | Default *length*: 255; on HANA: 5000 (4)(6) | _VARBINARY_ | -| `LargeBinary` | Unlimited data, usually streamed at runtime | _BLOB_ | +| `LargeBinary` | Unlimited data, usually streamed at runtime
[Prefer using Attachments plugin for large files](../plugins/index.md#attachments) | _BLOB_ | | `LargeString` | Unlimited data, usually streamed at runtime | _NCLOB_ | | `Map` | Mapped to *NCLOB* for HANA. | *JSON* type | | `Vector` (`dimension `) | Requires SAP HANA Cloud QRC 1/2024, or later | _REAL_VECTOR_ | diff --git a/get-started/learning-sources.md b/get-started/learning-sources.md index f809590a63..aec2e6e110 100644 --- a/get-started/learning-sources.md +++ b/get-started/learning-sources.md @@ -24,28 +24,19 @@ It's organized as follows: | [Plugins](../plugins/) | **Curated list of plugins** that extend the capabilities of the CAP framework. | | [Releases](../releases/) | The place where you can stay up to date with the most recent information about new features and changes in CAP. | - -### Node/Java Toggles - - ### Feature Status Badges Within the docs, you find badges that indicate the status of a feature, or API. Here's a list of the badges and their meanings: | Badge | Description | -|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | | The marked feature is available with the given version or higher | +| | Concept features are ideas for potential future enhancements and an opportunity for you to give feedback. This is not a commitment to implement the feature though | | | Alpha features are experimental. They may never be generally available. If released subsequently, the APIs and behavior might change | | | Beta features are planned to be generally available in subsequent releases, however, APIs and their behavior are not final and may change in the general release | -| | Concept features are ideas for potential future enhancements and an opportunity for you to give feedback. This is not a commitment to implement the feature though | -| | SAP specific features, processes, or infrastructure. Examples are _Deploy with Confidence_, _SAP product standards_, or _xMake_ | - - -### CAP Notebooks Integration - - - +| | Gamma features are finalized and ready to use, stable, and supported long term. Yet, as they have a broad scope and range, you should ensure to test them thoroughly. | +| | SAP specific features, processes, or infrastructure. | ## Sample Projects diff --git a/guides/assets/flows/xtravels-flow-extend.svg b/guides/assets/flows/xtravels-flow-extend.svg deleted file mode 100644 index 55fde8c076..0000000000 --- a/guides/assets/flows/xtravels-flow-extend.svg +++ /dev/null @@ -1,4 +0,0 @@ - - -OpenAcceptedWithdrawnacceptwithdrawCanceledreject \ No newline at end of file diff --git a/guides/assets/flows/xtravels-flow-previous.svg b/guides/assets/flows/xtravels-flow-previous.svg index ad2a583c85..7416a412c9 100644 --- a/guides/assets/flows/xtravels-flow-previous.svg +++ b/guides/assets/flows/xtravels-flow-previous.svg @@ -1,4 +1,4 @@ -OpenInReviewAcceptedCanceled→ to previous statereviewacceptblockreopenBlockedrejectblockunblock \ No newline at end of file +OpenInReviewAcceptedRejected→ to previous statereviewacceptblockreopenBlockedrejectblockunblock \ No newline at end of file diff --git a/guides/assets/flows/xtravels-flow-simple.svg b/guides/assets/flows/xtravels-flow-simple.svg index 24b2c46de4..80806ca3ae 100644 --- a/guides/assets/flows/xtravels-flow-simple.svg +++ b/guides/assets/flows/xtravels-flow-simple.svg @@ -1,4 +1,4 @@ -OpenAcceptedCanceledacceptreject \ No newline at end of file +OpenAcceptedRejectedacceptreject \ No newline at end of file diff --git a/guides/databases.md b/guides/databases.md index 784069076a..1c18cbbb4a 100644 --- a/guides/databases.md +++ b/guides/databases.md @@ -916,7 +916,7 @@ CREATE TABLE Books_texts ( ::: warning Database constraints aren't intended for checking user input Instead, they protect the integrity of your data in the database layer against programming errors. If a constraint violation occurs, the error messages coming from the database aren't standardized by the runtimes but presented as-is. -→ Use [`@assert.target`](providing-services#assert-target) for corresponding input validations. +→ Use [`@assert.target`](services/constraints#assert-target) for corresponding input validations. ::: ## Standard Database Functions diff --git a/guides/deployment/microservices.md b/guides/deployment/microservices.md index 60bdae144c..75a2b746ec 100644 --- a/guides/deployment/microservices.md +++ b/guides/deployment/microservices.md @@ -571,17 +571,17 @@ npm i @sap-cloud-sdk/resilience --workspace bookstore ``` ::: -### Approuter +### App Router -Add [approuter configuration](../deployment/to-cf#add-app-router) using the command: +Add _App Router_ configuration using the command: ```shell cds add approuter ``` -The approuter serves the UIs and acts as a proxy for requests toward the different apps. +The App Router serves the UIs and acts as a proxy for requests toward the different apps. -Since the approuter folder is only necessary for deployment, we move it into a `.deploy` folder. +Since the App Router folder is only necessary for deployment, we move it into a `.deploy` folder. ```shell mkdir .deploy @@ -602,7 +602,7 @@ modules: #### Static Content -The approuter can serve static content. Since our UIs are located in different NPM workspaces, we create symbolic links to them as an easy way to deploy them as part of the approuter. +The App Router can serve static content. Since our UIs are located in different NPM workspaces, we create symbolic links to them as an easy way to deploy them as part of the App Router. ```shell mkdir .deploy/app-router/resources @@ -614,7 +614,7 @@ cd ../../.. ``` ::: warning Simplified Setup -This is a simplified setup which deploys the static content as part of the approuter. +This is a simplified setup which deploys the static content as part of the App Router. See [Deploy to Cloud Foundry](./to-cf#add-ui) for a productive UI setup. ::: @@ -753,7 +753,7 @@ cds up [Learn more about `cds up`.](./to-cf#build-and-deploy){.learn-more} -Once the app is deployed, you can get the url of the approuter via +Once the app is deployed, you can get the url of the App Router via ```shell cf apps # [!code focus] diff --git a/guides/deployment/to-cf.md b/guides/deployment/to-cf.md index 746d9aa672..481b83470a 100644 --- a/guides/deployment/to-cf.md +++ b/guides/deployment/to-cf.md @@ -166,7 +166,7 @@ The roles/scopes are derived from authorization-related annotations in your CDS ### 3. MTA-Based Deployment { #add-mta-yaml} -We'll be using the [Cloud MTA Build Tool](https://sap.github.io/cloud-mta-build-tool/) to execute the deployment. The modules and services are configured in an `mta.yaml` deployment descriptor file, which we generate with: +We'll be using the [Cloud MTA Build Tool](https://sap.github.io/cloud-mta-build-tool/) to execute the deployment. The modules and services are configured in an _mta.yaml_ deployment descriptor: ```sh cds add mta @@ -174,41 +174,56 @@ cds add mta [Learn more about MTA-based deployment.](https://help.sap.com/products/BTP/65de2977205c403bbc107264b8eccf4b/d04fc0e2ad894545aebfd7126384307c.html?locale=en-US){.learn-more} -### 4. App Router as Gateway { #add-app-router} +### 4. User Interfaces { #add-ui } -The _App Router_ acts as a single point-of-entry gateway to route requests to. In particular, it ensures user login and authentication in combination with XSUAA. +#### Option A: SAP Cloud Portal -Two deployment options are available: +If you intend to deploy **multitenant** applications with a UI, we recommend to set up the [HTML5 Application Repository](https://discovery-center.cloud.sap/serviceCatalog/html5-application-repository-service) in combination with the [SAP Cloud Portal service](https://discovery-center.cloud.sap/serviceCatalog/cloud-portal-service): -- **Managed App Router**: for SAP Build Work Zone, the Managed App Router provided by SAP Fiori Launchpad is available. -- **Custom App Router**: for custom scenarios without SAP Fiori Launchpad, the App Router needs to be deployed along with your application. - In this case, use the following command to enhance the application configuration: +```sh +cds add portal +``` - ```sh - cds add approuter - ``` +::: tip `cds add portal` adds an _App Router_ configuration to your project +The App Router acts as a single point-of-entry gateway to route requests to. +In particular, it ensures user login and authentication in combination with XSUAA or IAS. +::: -[Learn more about the SAP BTP Application Router.](https://help.sap.com/products/BTP/65de2977205c403bbc107264b8eccf4b/01c5f9ba7d6847aaaf069d153b981b51.html?locale=en-US){.learn-more} +#### Option B: SAP BTP Application Frontend -### 5. User Interfaces { #add-ui } +For **single-tenant** applications, you can use the new [SAP BTP Application Frontend](https://help.sap.com/docs/application-frontend-service) service: -#### Option A: SAP Cloud Portal +```sh +cds add app-frontend +``` +[Enable the service for consumption in your subaccount](https://help.sap.com/docs/application-frontend-service/application-frontend-service/enabling-service?locale=en-US){.learn-more} -If you intend to deploy **multi-tenant** user interface applications, you also need to set up the [HTML5 Application Repository](https://discovery-center.cloud.sap/serviceCatalog/html5-application-repository-service) in combination with the [SAP Cloud Portal service](https://discovery-center.cloud.sap/serviceCatalog/cloud-portal-service): +::: details Other deployment variants... + +For **single-tenant** applications, you can integrate with SAP Build Work Zone, standard edition: ```sh -cds add portal +cds add workzone ``` -#### Option B: SAP BTP Application Frontend +This approach uses the **managed App Router** provided by SAP Fiori Launchpad — you don't need to deploy your own. Instead, destinations are configured. -For **single-tenant** applications, you can use the new [SAP BTP Application Frontend](https://help.sap.com/docs/application-frontend-service) service: +
+ +You might also use a custom App Router setup without SAP BTP Cloud Portal service: ```sh -cds add app-front +cds add approuter ``` +[Learn more about the SAP BTP Application Router.](https://help.sap.com/products/BTP/65de2977205c403bbc107264b8eccf4b/01c5f9ba7d6847aaaf069d153b981b51.html?locale=en-US){.learn-more} +
+However, in this case, you need to create symlinks from your _app_ folders to make them visible to the deployed App Router. +The [samples _modulith_](https://github.com/capire/samples) project uses this setup for serving a static _index.html_ consuming Vue.js via CDN. + +[Find the symlink directory in the App Router's _resources_ folder](https://github.com/capire/samples/tree/main/.deploy/app-router/resources){.learn-more} +::: -### 6. Optional: Multitenancy { #add-multitenancy } +### 5. Optional: Multitenancy { #add-multitenancy } To enable multitenancy for production, run the following command: diff --git a/guides/domain-modeling.md b/guides/domain-modeling.md index e0b578a014..7f7b016afe 100644 --- a/guides/domain-modeling.md +++ b/guides/domain-modeling.md @@ -88,8 +88,7 @@ The more we succeeded in capturing intent over imperative implementations, the m We use CDS as our ubiquitous modelling language, with CDS Aspects giving us the means to separate core domain aspects from generic aspects. CDS's human-readable nature fosters collaboration of developers and domain experts. -As CDS models are used to fuel generic providers — the database as well as application services — we ensure the models are applied in the implementation. And as coding is minimized we can more easily refine and revise our models, without having to refactor large boilerplate code based. - +As CDS models are used to fuel generic providers — the database as well as application services — we ensure the models are applied in the implementation. And as coding is minimized we can more easily refine and revise our models, without having to refactor large boilerplate codebases. @@ -230,9 +229,9 @@ Note: - **Namespaces are optional** — use namespaces if your models might be reused in other projects; otherwise, you can go without namespaces. - The **reverse domain name** approach works well for choosing namespaces. -::: warning +::: warning Avoid names that could change -Avoid short-lived ingredients in namespaces, or names in general, such as your current organization's name, or project code names. +Don't use short-lived ingredients in namespaces, or names in general, such as your current organization's name, or project code names. ::: @@ -341,7 +340,7 @@ It is an unfortunate anti pattern to validate UUIDs, such as for compliance to [ On the same note, converting UUID values obtained as strings from the database into binary representations such as `java.lang.UUID`, only to render them back to strings in responses to HTTP requests, is useless overhead. -::: warning +::: warning In summary: * Avoid unnecessary assumptions, for example, about uppercase or lowercase * Avoid useless conversions, for example, from strings to binary and back @@ -614,9 +613,9 @@ entity Books : NamedAspect { ... } [Learn more about the usage of aspects in the _Aspect-oriented Modeling_ section.](../cds/aspects).{ .learn-more} -::: tip +::: tip Consumers always see effective models -Consumers always see the merged effective models, with the separation into aspects fully transparent to them. +The separation into aspects is fully transparent to consumers. ::: @@ -756,7 +755,7 @@ entity Books.texts { Essentially, this is also what CAP generates behind the scenes, plus many more things to ease working with localized data and serving it out of the box. -::: tip +::: tip `localized` keeps models comprehensible By generating `.texts` entities and associations behind the scenes, CAP's **out-of-the-box support** for `localized` data avoids polluting your models with doubled numbers of entities, and detrimental effects on comprehensibility. ::: @@ -816,7 +815,6 @@ Managed data fields are filled in automatically and are write-protected for exte In case of `UPSERT` operations, the handlers for `@cds.on.update` are executed, but not the ones for `@cds.on.insert`. ::: -Domain Modeling > Managed Data {style="margin-bottom: 0; font-size:70%; font-style: italic;"} ### Aspect _`managed`_ {style="margin-top: 0;"} You can also use the [pre-defined aspect `managed`](../cds/common#aspect-managed) from [@sap/cds/common](../cds/common) to get the very same as by the definition above: diff --git a/guides/flows.md b/guides/flows.md deleted file mode 100644 index 3d32fd547c..0000000000 --- a/guides/flows.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -synopsis: > - Learn how to define and manage status-transition flows in your CDS models using annotations, without writing custom handlers. -#status: released ---- - -# Status-Transition Flows - -[...] - -## Extending Flows - -[...] - -### Example Use Case - -Consider a requirement where customers can withdraw from travel — for example, due to sickness — but only up to 24 hours before travel begins. This requires custom validation logic. - -The status transition diagram below shows the new state and transitions: -![](./assets/flows/xtravels-flow-extend.svg) - -First, add the `Withdrawn` status and the `withdrawTravel` action to the model: - -```cds -// db/schema.cds -entity TravelStatus : sap.common.CodeList { - key code : String(1) enum { - Open = 'O'; - Accepted = 'A'; - Canceled = 'X'; - Withdrawn = 'W'; // [!code highlight] - } -} - -// srv/travel-service.cds -service TravelService { - - // Define entity and actions - entity Travels as projection on db.Travels - actions { - action rejectTravel(); - action acceptTravel(); - action withdrawTravel(); // [!code highlight] - action deductDiscount( percent: Percentage not null ) returns Travels; - }; - - // Define flow through actions - annotate Travels with @flow.status: Status actions { - rejectTravel @from: #Open @to: #Canceled; - acceptTravel @from: #Open @to: #Accepted; - withdrawTravel @from: [#Open, #Accepted]; // [!code highlight] - deductDiscount @from: #Open; - }; - -} -``` - -Note that `withdrawTravel` has no `@to` annotation; you implement the transition in a custom handler. - - -### In Java - -Here is a custom Java implementation that enforces the 24-hour rule: - -```java -@Component -@ServiceName(TravelService_.CDS_NAME) -public class WithdrawTravelHandler implements EventHandler { - - private final PersistenceService persistenceService; - - public WithdrawTravelHandler(PersistenceService persistenceService) { - this.persistenceService = persistenceService; - } - - @Before(entity = Travel_.CDS_NAME) - public void check24HoursBeforeTravel(final TravelWithdrawTravelContext context, CqnStructuredTypeRef travelRef) { - Travel travel = ((ApplicationService) context.getService()).run( - Select.from(travelRef).columns(Travel_.BEGIN_DATE)).first(Travel.class) - .orElseThrow(() -> new ServiceException(ErrorStatuses.BAD_REQUEST, "TRAVEL_NOT_FOUND")); - - if (travel.beginDate().isBefore(LocalDate.now().minusDays(1))) { - context.getMessages().error("Travel can only be withdrawn up to 24 hours before travel begins."); - } - } - - @On(entity = Travel_.CDS_NAME) - public void onWithdrawTravel(final TravelWithdrawTravelContext context, CqnStructuredTypeRef travelRef) { - boolean isDraftTarget =DraftUtils.isDraftTarget( - travelRef, - context.getModel().findEntity(travelRef.targetSegment().id()).get(), - context.getModel()); - boolean isDraftEnabled = DraftUtils.isDraftEnabled(context.getTarget()); - var travel = Travel.create(); - travel.travelStatusCode(TravelStatusCode.WITHDRAWN); - if (isDraftTarget) { - ((DraftService) context.getService()).patchDraft(Update.entity(travelRef).data(travel)); - } else { - AnalysisResult analysis = CqnAnalyzer.create(context.getModel()).analyze(travelRef); - Map keys = analysis.targetKeyValues(); - if (isDraftEnabled) { - keys.remove(Drafts.IS_ACTIVE_ENTITY); - } - persistenceService.run(Update.entity(context.getTarget()).matching(keys).data(travel)); - } - context.setCompleted(); - } - -} -``` - -The custom `before` handler reads the travel's `BeginDate` and validates that withdrawal occurs within the allowed timeframe. The custom `on` handler updates the travel status to `Withdrawn` and marks the action as completed. - - - -The custom `on` handler updates the travel status to `Withdrawn` and marks the action as completed. - -::: warning TODO: we should actually do the following! --> `withdrawTravel` should only have an additional before check. -::: - -While you could use the `@to` annotation with the default handler, omitting it signals that you implemented custom transition logic. - - -### In Node.js - -TODO diff --git a/guides/messaging/index.md b/guides/messaging/index.md index 01bf0dfc40..374483cb0f 100644 --- a/guides/messaging/index.md +++ b/guides/messaging/index.md @@ -644,3 +644,17 @@ S4Bupa.on ('BusinessPartner.Changed', msg => {...}) Find more detailed information specific to receiving events from SAP S/4HANA in this separate guide: [→ **_Receiving Events from SAP S/4HANA_**](./s4) ::: + + +## [Task Queues](task-queues) + +The _Outbox Pattern_ is a reliable strategy used in distributed systems to ensure that messages or events are consistently recorded and delivered, even in the face of failures. +This pattern, however, can not only be applied to outbound messages, but to inbound messages and server-internal background tasks as well. +The core principle remains the same: +1. Persist the message (or _task_) in the database -- using the same transaction as the triggering action, if applicable +2. Process it asynchronously afterwards -- incl. retries, if necessary + +::: tip Read the guide +Find additional information about modeling task queues in this guide: +[→ **_Task Queues_**](task-queues) +::: \ No newline at end of file diff --git a/guides/multitenancy/index.md b/guides/multitenancy/index.md index 7eb9cfaddb..3b6782710f 100644 --- a/guides/multitenancy/index.md +++ b/guides/multitenancy/index.md @@ -472,6 +472,16 @@ In the third terminal, subscribe to two tenants using one of the following metho ::: + ::: tip Username defaults to `yves` on localhost + When subscribing to tenants on localhost, you can omit the `-u` option. The command automatically uses `yves` (the default mock user) with an empty password: + + ```sh + cds subscribe t1 --to http://localhost:4005 + ``` + + If you've logged in with `cds login`, that user takes precedence. To override, explicitly specify `-u `. + ::: + > Run `cds help subscribe` to see all available options. -```cds -@assert.range: [(0),100] // 0 < input ≤ 100 -@assert.range: [0,(100)] // 0 ≤ input < 100 -@assert.range: [(0),(100)] // 0 < input < 100 -``` -In addition, you can use an underscore `_` to represent *Infinity* like that: - -```cds -@assert.range: [(0),_] // positive numbers only, _ means +Infinity here -@assert.range: [_,(0)] // negative number only, _ means -Infinity here -``` -> Basically values wrapped in parentheses _`(x)`_ can be read as _excluding `x`_ for *min* or *max*. Note that the underscore `_` doesn't have to be wrapped into parentheses, as by definition no number can be equal to *Infinity* . - -Support for open intervals and infinity is available for CAP Node.js since `@sap/cds` version **8.5** and in CAP Java since version **3.5.0**. - - - -### `@assert .target` - -Annotate a [managed to-one association](../cds/cdl#managed-associations) of a CDS model entity definition with the -`@assert.target` annotation to check whether the target entity referenced by the association (the reference's target) -exists. In other words, use this annotation to check whether a non-null foreign key input in a table has a corresponding -primary key in the associated/referenced target table. - -You can check whether multiple targets exist in the same transaction. For example, in the `Books` entity, you could -annotate one or more managed to-one associations with the `@assert.target` annotation. However, it is assumed that -dependent values were inserted before the current transaction. For example, in a deep create scenario, when creating a -book, checking whether an associated author exists that was created as part of the same deep create transaction isn't -supported, in this case, you will get an error. - -The `@assert.target` check constraint is meant to **validate user input** and not to ensure referential integrity. -Therefore only `CREATE`, and `UPDATE` events are supported (`DELETE` events are not supported). To ensure that every -non-null foreign key in a table has a corresponding primary key in the associated/referenced target table -(ensure referential integrity), the [`@assert.integrity`](databases#database-constraints) constraint must be used instead. - -If the reference's target doesn't exist, an HTTP response -(error message) is provided to HTTP client applications and logged to stdout in debug mode. The HTTP response body's -content adheres to the standard OData specification for an error -[response body](https://docs.oasis-open.org/odata/odata-json-format/v4.01/cs01/odata-json-format-v4.01-cs01.html#sec_ErrorResponse). - -#### Example - -Add `@assert.target` annotation to the service definition as previously mentioned: - -```cds -entity Books { - key ID : UUID; - title : String; - author : Association to Authors @assert.target; -} - -entity Authors { - key ID : UUID; - name : String; - books : Association to many Books on books.author = $self; -} -``` - -**HTTP Request** — *assume that an author with the ID `"796e274a-c3de-4584-9de2-3ffd7d42d646"` doesn't exist in the database* - -```http -POST Books HTTP/1.1 -Accept: application/json;odata.metadata=minimal -Prefer: return=minimal -Content-Type: application/json;charset=UTF-8 - -{"author_ID": "796e274a-c3de-4584-9de2-3ffd7d42d646"} -``` - -**HTTP Response** - -```http -HTTP/1.1 400 Bad Request -odata-version: 4.0 -content-type: application/json;odata.metadata=minimal - -{"error": { - "@Common.numericSeverity": 4, - "code": "400", - "message": "Value doesn't exist", - "target": "author_ID" -}} -``` -::: tip -In contrast to the `@assert.integrity` constraint, whose check is performed on the underlying database layer, -the `@assert.target` check constraint is performed on the application service layer before the custom application handlers are called. -::: -::: warning -Cross-service checks are not supported. It is expected that the associated entities are defined in the same service. -::: -::: warning -The `@assert.target` check constraint relies on database locks to ensure accurate results in concurrent scenarios. However, locking is a database-specific feature, and some databases don't permit to lock certain kinds of objects. On SAP HANA, for example, views with joins or unions can't be locked. Do not use `@assert.target` on such artifacts/entities. -::: - - -### Custom Error Messages - -The annotations `@assert.range`, `@assert.format`, and `@mandatory` also support custom error messages. Use the annotation `@.message` with an error text or [text bundle key](../guides/i18n#externalizing-texts-bundles) to specify a custom error message: +## Custom Logic -```cds -entity Person : cuid { - name : String; +As most standard tasks and use cases are covered by [generic service providers](#generic-providers), the need to add service implementation code is greatly reduced and minified, and hence the quantity of individual boilerplate coding. - @assert.format: '/^\S+@\S+\.\S+$/' - @assert.format.message: 'Provide a valid email address' - email : String; +The remaining cases that need custom handlers, reduce to real custom logic, specific to your domain and application, such as: - @assert.range: [(0),_] - @assert.range.message: '{i18n>person-age}' - age : Int16; -} -``` +- Domain-specific programmatic [Validations](#input-validation) +- Augmenting result sets, for example to add computed fields for frontends +- Programmatic [Authorization Enforcements](/guides/security/authorization#enforcement) +- Triggering follow-up actions, for example calling other services or emitting outbound events in response to inbound events +- And more... In general, all the things not (yet) covered by generic handlers -Note: The above can also be written like that: -```cds -entity Person : cuid { - name : String; - @assert.format: { - $value: '/^\S+@\S+\.\S+$/', message: 'Provide a valid email address' - } - email : String; +### Declarative Custom Logic - @assert.range: { - $value: [(0),_], message: '{i18n>person-age}' - } - age : Int16; -} -``` +CAP supports various declarative techniques to express custom logic without coding, in particular for input validation and status-transition flows. +#### Status Transition Flows -### Database Constraints +- [Status-Transition Flows](./services/status-flows.md) ensure transitions are explicitly modeled, validated, and executed in a controlled and reliable way, thereby eliminating the need for extensive custom coding. -Next to input validation, you can add [database constraints](databases#database-constraints) to prevent invalid data from being persisted. -## Custom Logic +#### Input Validation +- [Declarative Constraints](./services/constraints) allow to annotate your models and have the respective checks still be executed and enforced by generic runtimes, with the following annotations: + - [`@assert`](./services/constraints#assert-constraint), incl. derivates: + - [`@assert.format`](./services/constraints#assert-format) + - [`@assert.range`](./services/constraints#assert-range) + - [`@assert.target`](./services/constraints#assert-target) + - [`@mandatory`](./services/constraints#mandatory) + - [`@readonly`](./services/constraints#readonly) -As most standard tasks and use cases are covered by [generic service providers](#generic-providers), the need to add service implementation code is greatly reduced and minified, and hence the quantity of individual boilerplate coding. -The remaining cases that need custom handlers, reduce to real custom logic, specific to your domain and application, such as: +> [!tip] +> Prefer declarative constraints over programmatic validations wherever possible, as they require no implementation coding and are automatically served by CAP runtimes in optimized ways. -- Domain-specific programmatic [Validations](#input-validation) -- Augmenting result sets, for example to add computed fields for frontends -- Programmatic [Authorization Enforcements](/guides/security/authorization#enforcement) -- Triggering follow-up actions, for example calling other services or emitting outbound events in response to inbound events -- And more... In general, all the things not (yet) covered by generic handlers +### Custom Service Providers **In Node.js**, the easiest way to add custom implementations for services is through equally named _.js_ files placed next to a service definition's _.cds_ file: @@ -1235,230 +1053,6 @@ Programmatic usage would look like this for Node.js: > Note: Even with that typed APIs, always pass the target entity name as second argument for bound actions/functions. - -## Status-Transition Flows - -The flow feature makes it easy to define and manage state transitions in your CDS models. -It ensures transitions are explicitly modeled, validated, and executed in a controlled and reliable way. -For more complex requirements, you can extend flows with custom event handlers. - - -### Enabling Flows - -Status-transition flows are supported by both CAP runtimes. - -In CAP Node.js support for flows is part of the CAP Node.js core (`@sap/cds`). - -For CAP Java, support for flows is provided by the feature [cds-feature-flow](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-flow). Enable it by adding this dependency to your _srv/pom.xml_ file: - -```xml - - com.sap.cds - cds-feature-flow - runtime - -``` - -### Modeling Flows - -The following example, taken from [@capire/xtravels](https://github.com/capire/xtravels), shows the simplest way to model a flow. -The annotations in the service model are sufficient to define and use the flow. - -![A flow diagram showing three status states connected by arrows. The leftmost oval contains the word Open. An arrow labeled accept points from Open to an oval containing Accepted at the top right. Another arrow labeled reject points from Open to an oval containing Canceled at the bottom right. The diagram illustrates a simple state transition workflow where items can move from an open state to either accepted or canceled states.](./assets/flows/xtravels-flow-simple.svg) - -The following is an extract of the relevant parts of the domain model: - -::: details `db/schema.cds` -```cds [db/schema.cds] -// db/schema.cds -namespace sap.capire.travels; - -entity Travels : managed { - // [...] - Status : Association to TravelStatus default 'O'; - // [...] -} - -entity TravelStatus : sap.common.CodeList { - key code : String(1) enum { - Open = 'O'; - Accepted = 'A'; - Canceled = 'X'; - } -} -``` -::: - -```cds [srv/travel-service.cds] -// srv/travel-service.cds -service TravelService { - - // Define entity and actions - entity Travels as projection on db.Travels - actions { - action rejectTravel(); - action acceptTravel(); - action deductDiscount( percent: Percentage not null ) returns Travels; - }; - - // Define flow through actions (+ status check for "deductDiscount") - annotate Travels with @flow.status: Status actions { // [!code highlight] - rejectTravel @from: #Open @to: #Canceled; // [!code highlight] - acceptTravel @from: #Open @to: #Accepted; // [!code highlight] - deductDiscount @from: #Open; // [!code highlight] - }; // [!code highlight] - -} -``` - -No custom action handlers are needed for simple transitions—the flow feature's default handlers validate that the entry state is `Open` and transition the status to `Accepted` or `Canceled` accordingly. -For more complex scenarios, you can add custom handlers as explained later. - - -### Flow Annotations - -Flows consist of a _status element_ and a set of _flow actions_ that define transitions between states. - -#### Declare Flow Using `@flow.status` - -To model a flow, one of the entity fields needs to be annotated with `@flow.status`. -This field must be one of the following: - -- A String or Integer enum consisting of keys and values -- A String enum with only symbols -- A Codelist entity with the key `code` if localization is needed (`code` must be one of the two above) - -::: tip The status field should be `@readonly` and have a default value. -We recommend to always use `@flow.status` in combination with `@readonly`. -This ensures that the status element is immutable from the client side, giving the service provider full control over all state transitions. -As no initial state can be provided on `CREATE`, there should be a default value. -::: - -When you annotate `@flow.status: ` at the entity level (as in the example above), the annotation is propagated to the respective element, which is also automatically annotated with `@readonly`. - -**About the `@flow.status` annotation:** -- This annotation is **mandatory**. -- The annotated element must be either an enum or an association to a code list. -- Only one status element per entity is supported. -- Draft-enabled entities are supported, however flows are only applied to the active version. -- `null` is **not** a valid state—model your empty state explicitly. - -::: warning Only simple projections are supported -The entity must be _writable_, and renaming the status element is currently not supported. -::: - -After declaring `@flow.status`, use the following annotations on bound actions to model transitions: - -#### Model Transitions Using `@from` and `to` - -Both annotations are optional, but at least one is required to mark an action as a flow action. Use either one or both depending on your needs. When you use both, no custom handlers are needed—generic handlers are registered automatically. - -**`@from`** - -- Defines valid entry states for the action. -- Validates whether the entity is in a valid entry state before executing the action (the current state of the entity must be included in the states defined here). -- Can be a single value or an array of values (each element must be a value from the status enum). -- UI annotations to allow/disallow buttons and to refresh the page are automatically generated for UI5. - - Can be deactivated via cds.features.annotate_for_flows: false. - -**`@to`** - -- Defines the desired target state of the entity after executing the action. -- Changes the state of the entity to the value defined in this annotation after executing the action. -- Must be a single value from the status enum. - - - -### Generic Handlers - -Generic handlers are registered automatically, so no custom implementations are required for basic flows. - -#### `before` - -Based on the `@from` annotation, a handler validates that the entity is in a valid entry state - the current state must match one of the states specified in `@from`. -If validation fails, the request returns a `409 Conflict` HTTP status code with an appropriate error message. - -#### `on` - -In case of a `@to` declaration and if no custom handler is provided, an empty handler is registered that completes the action for void return types, ensuring the request passes through the generic handler stack. -This is an exception to the rule that actions must be implemented by the application. - -#### `after` - -Based on the `@to` annotation, a handler automatically updates the entity's status to the target state. -For example, if the current state is `Open` and the target state is `Accepted`, the handler updates the status to `Accepted` after action execution. -This ensures consistent state transitions without custom logic. - -::: tip Generic handlers are not executed for draft entities -For example, if you call `acceptTravel()` on a `Travels` entity that is currently being edited (in _inactive_ state), the call has no effect. -::: - - -### Reverting to Previous State - -You can use the target state `$flow.previous` to restore the previous state in a workflow. -The following example introduces a `Blocked` state with two possible previous states (`Open` and `InReview`) and an action `unblockTravel` that restores the previous state. -For instance, if `Blocked` was transitioned to from `Open`, calling `unblockTravel` transitions back to `Open`. The same applies for `InReview`. - -![The graphic is explained in the accompanying text.](./assets/flows/xtravels-flow-previous.svg) - -```cds [srv/travel-service.cds] -// srv/travel-service.cds -service TravelService { - - // Define entity and actions - entity Travels as projection on db.Travels - actions { - action reviewTravel(); - action reopenTravel(); - action blockTravel(); - action unblockTravel(); - action rejectTravel(); - action acceptTravel(); - action deductDiscount( percent: Percentage not null ) returns Travels; - }; - - // Define flow incl. "unblockTravel" that transitions to the previous state - annotate Travels with @flow.status: Status actions { - reviewTravel @from: #Open @to: #InReview; // [!code highlight] - reopenTravel @from: #InReview @to: #Open; // [!code highlight] - blockTravel @from: [#Open, #InReview] @to: #Blocked; // [!code highlight] - unblockTravel @from: #Blocked @to: $flow.previous; // [!code highlight] - rejectTravel @from: #InReview @to: #Canceled; - acceptTravel @from: #InReview @to: #Accepted; - deductDiscount @from: #Open; - }; - -} -``` - -Entities with flows that include at least one transition to `$flow.previous` are automatically extended with the `sap.common.FlowHistory` aspect, which includes `transitions_` composition that captures the history of state transitions. - -::: details The `transitions_` composition -The `transitions_` composition is meant as a technical artifact to implement transitioning to the previous state and not for exposing the transition history to business users, etc. -For such use cases, check out the [Change Tracking plugin](../plugins/index.md#change-tracking). - -The automatic entity extending described above can be deactivated via cds.features.history_for_flows: false. -If you do so, you need to add aspect `sap.common.FlowHistory` manually in order to use `@to: $flow.previous`! - -Automatic history capturing can, as an experimental feature, also be enabled for all entities with a flow definition via cds.features.history_for_flows: 'all'. - -The `transitions_` composition automatically appended to the base entity is also automatically excluded from all projections. -::: - - -### Extending Flows - -Flow annotations work well for basic flows. For more complex scenarios, implement custom event handlers. - -Common use cases for custom handlers: -- **Additional validation:** Implement a custom `before` handler when entry state validation depends on extra conditions -- **Non-void return types:** Implement a custom `on` handler when the action returns data -- **Conditional target states:** Implement a custom `on` or `after` handler (without `@to` annotation) when multiple target states depend on conditions - - - - ## Serving Media Data CAP provides out-of-the-box support for serving media and other binary data. diff --git a/guides/security/authorization.md b/guides/security/authorization.md index 1a53ce52c8..d1cb7578ad 100644 --- a/guides/security/authorization.md +++ b/guides/security/authorization.md @@ -433,7 +433,7 @@ So, the authorization for the requests in the example is delegated as follows: | Request Target | Authorization Entity | |--------------------------------------------------------|:--------------------------------------:| -| `IssuesService.Components` | 1 | +| `IssuesService.Components` | `IssuesService.Components`3 | | `IssuesService.Issues` | 1 | | `IssuesService.Categories` | `IssuesService.Categories`2 | | `IssuesService.Components[].issues` | `IssuesService.Components`3 | @@ -830,19 +830,16 @@ service CustomerService { ::: code-group ```cds [services-auth.cds] -service ReviewsService @(requires: 'authenticated-user'){ - /*...*/ -} - -service CustomerService @(requires: 'authenticated-user'){ - entity Orders @(restrict: [ - { grant: ['READ','WRITE'], to: 'admin' }, - { grant: 'READ', where: 'buyer = $user' }, - ]){/*...*/} - entity Approval @(restrict: [ - { grant: 'WRITE', where: '$user.level > 2' } - ]){/*...*/} -} +annotate ReviewsService with @(requires: 'authenticated-user'); + +annotate CustomerService with @(requires: 'authenticated-user'); +annotate CustomerService.Orders with @(restrict: [ + { grant: ['READ','WRITE'], to: 'admin' }, + { grant: 'READ', where: 'buyer = $user' }, +]); +annotate CustomerService.Approval with @(restrict: [ + { grant: 'WRITE', where: '$user.level > 2' } +]); ``` ::: diff --git a/guides/services/assets/constraints/fiori-errors.png b/guides/services/assets/constraints/fiori-errors.png new file mode 100644 index 0000000000..3e104ae7d9 Binary files /dev/null and b/guides/services/assets/constraints/fiori-errors.png differ diff --git a/guides/services/constraints.md b/guides/services/constraints.md new file mode 100644 index 0000000000..5e5eee18d6 --- /dev/null +++ b/guides/services/constraints.md @@ -0,0 +1,573 @@ +--- +synopsis: > + Declarative constraints allow you to express conditions using CXL expressions that are validated automatically whenever data is written, greatly reducing the need for extensive custom code for input validation. +status: released +--- + +# Declarative Constraints + +Declarative constraints allow you to express conditions using [CDS Expression Language (CXL)](/cds/cxl) that are validated automatically whenever data is written. This greatly reduces the need for extensive custom code for input validation. + +> [!note] +> Don't confuse declarative constraints as discussed in here with [database constraints](../databases#database-constraints). Declarative constraints are meant for domain-specific input validation with error messages meant to be shown to end users, while database constraints are meant to prevent data corruption due to programming error, with error messages not intended for end users. + + + +[[toc]] + + + +## Introduction + +Use annotations like `@assert` and `@mandatory` to declaratively add constraints for the primary purpose of input validation. Add them to the elements of the entities exposed by respective services, which accept input to be validated. + +### Constraints Annotations + +Following is an excerpt from the [`@capire/xtravels`](https:/github.com/capire/xtravels/tree/main/srv/travel-constraints.cds) sample: + +::: code-group + +```cds [srv/travel-constraints.cds] +using { TravelService } from './travel-service'; +annotate TravelService.Travels with { + + Description @assert: (case + when length(Description) < 3 then 'Description too short' + end); + + Agency @mandatory @assert: (case + when not exists Agency then 'Agency does not exist' + end); + + Customer @assert: (case + when Customer is null then 'Customer must be specified' + when not exists Customer then 'Customer does not exist' + end); + + BeginDate @mandatory @assert: (case + when BeginDate > EndDate then 'ASSERT_BEGINDATE_BEFORE_ENDDATE' + when exists Bookings [Flight.date < Travel.BeginDate] + then 'ASSERT_BOOKINGS_IN_TRAVEL_PERIOD' + end); + + BookingFee @assert: (case + when BookingFee < 0 then 'ASSERT_BOOKING_FEE_NON_NEGATIVE' + end); + +} +``` + +::: + +> [!tip] BEST PRACTICES +> +> **Separation of Concerns** – always put secondary concerns, such as constraints in this case, into separate files as in the example, instead of polluting your core service definitions. +> +> **Concise and comprehensible** – in contrast to imperative coding, constraints expressed in expression languages as shown here are easy to read and understand. +> +> **Fueling AI** – Not the least, this also fuels AI-based approaches: AIs can easily generate such constraints, and you as a developer using such AIs can easily validate what was generated. + + + +### Served Out-of-the-Box + + + +The constraints are enforced automatically by the CAP runtimes on any input, and if failures occur, the request is ultimately rejected and the transaction rolled back. + +Some of the checks, e.g. the static `@mandatory` checks, are validated directly on the input data, while the ones specified with `@assert:(\)` are collected into a query and **pushed down to the database** for execution. This in turn means, that first the respective `INSERT`s and `UPDATE`s are sent to the database, followed by the validation query. + + + +::: details Behind the scenes... + +The automatically compiled and executed validation query would look like that (in [CQL](/cds/cql)) for the constraints from the sample above: + +```sql +SELECT from TravelService.Travels { + + (case + when length(Description) < 3 then 'Description too short' + end) as Description, + + (case + when not exists Agency then 'Agency does not exist' + end) as Agency, + + (case + when Customer is null then 'Customer must be specified' + when not exists Customer then 'Customer does not exist' + end) as Customer, + + (case + when BeginDate > EndDate then 'ASSERT_BEGINDATE_BEFORE_ENDDATE' + when exists Bookings [Flight.date < Travel.BeginDate] + then 'ASSERT_BOOKINGS_IN_TRAVEL_PERIOD' + end) as BeginDate, + + (case + when BookingFee < 0 then 'ASSERT_BOOKING_FEE_NON_NEGATIVE' + end) as BookingFee, + +} +``` + +::: + + + +> [!tip] BEST PRACTICES +> +> **Push down to the database** is a general principle applied in CAP. Applied to input validation with declarative constraints it means that instead of reading a lot of related data into the service layer to do the checks there, we push down the respective checks to where the data is (in the database). +> +> **What, not how!** – This in turn boils down to the even more general principle that we share with functional programming: tell us *what* to do (= *intentional*), not how (= *imperative*), because then generic runtimes can apply advanced optimized ways to execute things, which is impossible with imperative code. + + + + + +### Served to Fiori UIs + +For Fiori UIs as clients the error messages will be automatically be equiped with relevant `target` properties to attach them to the respective fields on the UIs. For example a Fiori UI for the sample above, would display returned errors like that: + +![image-20251219115646302](./assets/constraints/fiori-errors.png) + +::: details Behind the scenes ... + +A sample response for such errors displayed in Fiori UIs would look like that: + +```json +{ + "@odata.context": "$metadata#Travels/$entity", + "ID": 4132, + "DraftMessages": [ + { + "target": "/Travels(ID=4132,IsActiveEntity=false)/EndDate", // [!code focus] + "numericSeverity": 4, + "@Common.numericSeverity": 4, + "message": "Alle Buchungen müssen innerhalb des Reisezeitraums liegen", + "code": "ASSERT_BOOKINGS_IN_TRAVEL_PERIOD" + }, + { + "target": "/Travels(ID=4132,IsActiveEntity=false)/Customer_ID", // [!code focus] + "numericSeverity": 4, + "@Common.numericSeverity": 4, + "message": "Customer does not exist", + "code": "400" + }, + { + "target": "/Travels(ID=4132,IsActiveEntity=false)/Bookings(Travel_ID=4132,Pos=1,IsActiveEntity=false)/Flight_date", // [!code focus] + "numericSeverity": 4, + "@Common.numericSeverity": 4, + "message": "Das Flugdatum dieser Buchung liegt nicht innerhalb des Reisezeitraums", + "code": "ASSERT_BOOKING_IN_TRAVEL_PERIOD" + } + ], + "IsActiveEntity": false +} +``` + +::: + + + + + +## Input Validation + + + +Use annotations like `@assert` and `@mandatory` to declaratively add constraints for the primary purpose of input validation. Add them to the elements of the entities exposed by respective services, which accept input to be validated. + + + +### `@assert:` *(constraint)* + +Annotate an element with `@assert: ()` to specify checks to be applied on respective input and errors to be raised if they fail. The `` are standard SQL `case` expressions with one or more `when` branches, as shown in this example: + +```cds +annotate TravelService.Travels with { + + Description @assert: (case // [!code focus] + when Description then 'Description must be specified' // [!code focus] + when trim(Description) = '' then 'Description must not be empty' // [!code focus] + when length(Description) < 3 then 'Description too short' // [!code focus] + end); // [!code focus] + +} +``` + +[Refer to _Expressions as Annotation Values_ for details on syntax.](../../cds/cdl.md#expressions-as-annotation-values) {.learn-more} + + +Conditions can also **refer to other data elements** in the same entity as shown in this example which validated input for `BeginDate` with the related `EndDate`: + +```cds +annotate TravelService.Travels with { + + BeginDate @assert: (case // [!code focus] + when BeginDate > EndDate then 'Begin date must be before end date' // [!code focus] + end); // [!code focus] + +} +``` + +We can also use **path expressions** to compare with data from **associated** entities. For example, this one is from anoter annotation on `TravelService.Bookings` in the [`@capire/xtravels`](https:/github.com/capire/xtravels/tree/main/srv/travel-constraints.cds) sample, that checks if all currencies specified in the list of bookings match the currency chosen in the travel header, refered to by the `Travel` association: + +```cds +annotate TravelService.Bookings with { + + Currency @assert: (case // [!code focus] + when Currency != Travel.Currency then 'Currencies must match' // [!code focus] + end); // [!code focus] + +} + +``` + +We can also do checks with sets of related data using path expressions which navigate along **to-many associations** or compositions, combined with SQL's `exists` quantifier, and optional [infix filters](../..//cds/cql#with-infix-filters), as shown in this example: + +```cds +annotate TravelService.Travels with { + + BeginDate @assert: (case // [!code focus] + when exists Bookings [Flight.date < Travel.BeginDate] // [!code focus] + then 'All bookings must be within travel period' // [!code focus] + end); // [!code focus] + +} +``` + + + + + + +### `@assert.format` + +Allows you to specify a regular expression string (in ECMA 262 format in CAP Node.js and java.util.regex.Pattern format in CAP Java) that all string input must match. + +```cds +entity Foo { + bar : String @assert.format: '[a-z]ear'; +} +``` + + +### `@assert.range` + +Allows you to specify `[ min, max ]` ranges for elements with ordinal types — that is, numeric or date/time types. For `enum` elements, `true` can be specified to restrict all input to the defined enum values. + +```cds +entity Foo { + bar : Integer @assert.range: [ 0, 3 ]; + boo : Decimal @assert.range: [ 2.1, 10.25 ]; + car : DateTime @assert.range: ['2018-10-31', '2019-01-15']; + zoo : String @assert.range enum { high; medium; low; }; +} +``` + +By default, specified `[min,max]` ranges are interpreted as closed intervals, that means, the performed checks are `min ≤ input ≤ max`. You can also specify open intervals by wrapping the *min* and/or *max* values into parentheses like that: + + +```cds +@assert.range: [(0),100] // 0 < input ≤ 100 +@assert.range: [0,(100)] // 0 ≤ input < 100 +@assert.range: [(0),(100)] // 0 < input < 100 +``` +In addition, you can use an underscore `_` to represent *Infinity* like that: + +```cds +@assert.range: [(0),_] // positive numbers only, _ means +Infinity here +@assert.range: [_,(0)] // negative number only, _ means -Infinity here +``` +> Basically values wrapped in parentheses _`(x)`_ can be read as _excluding `x`_ for *min* or *max*. Note that the underscore `_` doesn't have to be wrapped into parentheses, as by definition no number can be equal to *Infinity* . + +Support for open intervals and infinity is available for CAP Node.js since `@sap/cds` version **8.5** and in CAP Java since version **3.5.0**. + + + +### `@assert.target` + +Annotate a [managed to-one association](../../cds/cdl#managed-associations) with `@assert.target` to check whether the target entity referenced by the association (the reference's target) exists for a given input. + +```cds +entity Books { + key ID : UUID; + title : String; + author : Association to Authors @assert.target; +} + +entity Authors { + key ID : UUID; + name : String; + books : Association to many Books on books.author = $self; +} +``` + +You can check whether multiple targets exist in the same transaction. For example, in the `Books` entity, you could +annotate one or more managed to-one associations with the `@assert.target` annotation. However, it is assumed that +dependent values were inserted before the current transaction. For example, in a deep create scenario, when creating a book, checking whether an associated author exists that was created as part of the same deep create transaction isn't supported, in this case, you will get an error. + +The `@assert.target` check constraint is meant to **validate user input** and not to ensure referential integrity. +Therefore only `CREATE`, and `UPDATE` events are supported (`DELETE` events are not supported). To ensure that every +non-null foreign key in a table has a corresponding primary key in the associated/referenced target table +(ensure referential integrity), the [`@assert.integrity`](../databases#database-constraints) constraint must be used instead. + +If the reference's target doesn't exist, an HTTP response +(error message) is provided to HTTP client applications and logged to stdout in debug mode. The HTTP response body's +content adheres to the standard OData specification for an error +[response body](https://docs.oasis-open.org/odata/odata-json-format/v4.01/cs01/odata-json-format-v4.01-cs01.html#sec_ErrorResponse). + +```http +POST Books HTTP/1.1 +Accept: application/json;odata.metadata=minimal +Prefer: return=minimal +Content-Type: application/json;charset=UTF-8 + +{"author_ID": "796e274a-c3de-4584-9de2-3ffd7d42d646"} +``` + +**HTTP Response** + +```http +HTTP/1.1 400 Bad Request +odata-version: 4.0 +content-type: application/json;odata.metadata=minimal + +{"error": { + "@Common.numericSeverity": 4, + "code": "400", + "message": "Value doesn't exist", + "target": "author_ID" +}} +``` +::: tip +In contrast to the `@assert.integrity` constraint, whose check is performed on the underlying database layer, +the `@assert.target` check constraint is performed on the application service layer before the custom application handlers are called. +::: +::: warning +Cross-service checks are not supported. It is expected that the associated entities are defined in the same service. +::: +::: warning +The `@assert.target` check constraint relies on database locks to ensure accurate results in concurrent scenarios. However, locking is a database-specific feature, and some databases don't permit to lock certain kinds of objects. On SAP HANA, for example, views with joins or unions can't be locked. Do not use `@assert.target` on such artifacts/entities. +::: + + + + +### `@mandatory` + +Elements marked with `@mandatory` are checked for missing and empty input and respective requests are rejected. + +```cds +service Sue { + entity Books { + key ID : UUID; + title : String @mandatory; + } +} +``` + +In addition to server-side input validation as introduced above, this adds a corresponding `@FieldControl` annotation to the EDMX so that OData / Fiori clients would enforce a valid entry, thereby avoiding unnecessary request roundtrips: + +```xml + + + +``` + +
+ + + +### `@readonly` + +Elements annotated with `@readonly`, as well as [_calculated elements_](../../cds/cdl#calculated-elements), are protected against write operations. That is, if a CREATE or UPDATE operation specifies values for such fields, these values are **silently ignored**. + +By default [`virtual` elements](../../cds/cdl#virtual-elements) are also _calculated_. +::: tip +The same applies for fields with the [OData Annotations](../../advanced/odata#annotations) `@FieldControl.ReadOnly` (static), `@Core.Computed`, or `@Core.Immutable` (the latter only on UPDATEs). +::: + +::: warning Not allowed on keys +Do not use the `@readonly` annotation on keys in all variants. +::: + +
+ + +## Error Messages + +### Custom Messages + +For `@assert: ()` annotations you always specify custom error messages, specific to the individual checks: + +```cds +annotate TravelService.Travels with { + + Description @assert: (case // [!code focus] + when Description then 'Description must be specified' // [!code focus] + when trim(Description) = '' then 'Description must not be empty' // [!code focus] + when length(Description) < 3 then 'Description too short' // [!code focus] + end); // [!code focus] + +} +``` + + + +The annotations `@assert.range`, `@assert.format`, and `@mandatory` also support custom error messages, just not as elegant, as the above: Use the annotation `@.message` to specify a custom error message: + +```cds +entity Person : cuid { + name : String; + + @assert.format: '/^\S+@\S+\.\S+$/' + @assert.format.message: 'Provide a valid email address' + email : String; + + @assert.range: [(0),_] + @assert.range.message: '{i18n>person-age}' + age : Int16; +} +``` + +Note: The above can also be written like that: + +```cds +entity Person : cuid { + name : String; + + @assert.format: { + $value: '/^\S+@\S+\.\S+$/', message: 'Provide a valid email address' + } + email : String; + + @assert.range: { + $value: [(0),_], message: '{i18n>person-age}' + } + age : Int16; +} +``` + + + +### Localized Messages + +Whenever you specify an error message with the annotations above, i.e., in the `then` part of an `@assert: ()` or in `@mandatory.message`, `@assert.format.message`, or `@assert.range.message`, you can either specify a plain text, or a [I18n text bundle key](../i18n#externalizing-texts-bundles). + +Actually, we saw this already in the [sample in the introduction](#introduction): + +::: code-group + +```cds [srv/travel-constraints.cds] +using { TravelService } from './travel-service'; +annotate TravelService.Travels with { + + Description @assert: (case + when length(Description) < 3 + then 'Description too short' // [!code focus] + end); + + Agency @mandatory @assert: (case + when not exists Agency + then 'Agency does not exist' // [!code focus] + end); + + BeginDate @mandatory @assert: (case + when BeginDate > EndDate + then 'ASSERT_BEGINDATE_BEFORE_ENDDATE' // [!code focus] + when exists Bookings [Flight.date < Travel.BeginDate] + then 'ASSERT_BOOKINGS_IN_TRAVEL_PERIOD' // [!code focus] + end); + + BookingFee @assert: (case + when BookingFee < 0 + then 'ASSERT_BOOKING_FEE_NON_NEGATIVE' // [!code focus] + end); + +} +``` + +::: + +If you use a message key, the message is automatically looked up in the message bundle of the service with the current user's preferred locale. + +[Learn more about localized messages.](../i18n){.learn-more} + + + +## Field Control + +Declarative constraints can also be used to do field control in Fiori UIs, i.e. to add visual indicators to mandatory or readonly fields, or to hide fields. In particular, CAP automatically adds respective OData annotations to generated EDMX $metadata documents for the CDS listed below. + + +### `@mandatory` + +Currently only static `@mandatory` annotations are supported for field control in Fiori UIs. They result in the addition of the following OData annotation to the EDMX $metadata: + +```xml + + + +``` + + + +### `@readonly` + +Currently only static `@readonly` annotations are supported for field control in Fiori UIs. They result in the addition of the following OData annotation to the EDMX $metadata: + +```xml + + + +``` + + +### `@UI.Hidden` + +Use the `@UI.Hidden` annotation to hide fields in Fiori UIs. You can also use it with expressions as values, for example like that: + +```cds +@UI.Hidden: (status <> 'visible') +``` + +[Learn more about that in the *OData guide*](/advanced/odata#expression-annotations) {.learn-more} + + + +## Invariant Constraints + + + +Annotations in general are propagated from underlying entities to views on top. This also applies to the annotations like `@assert` and `@mandatory` introduced in here, which can be used to declare invariant constraints on base entities, which are then inherited to and hence enforced on all interface views on top. + +Picking up the [sample from the introduction](#introduction) again, we could extract some of the constraints and add them to the `sap.capire.travels.Travels` entity from the domain model, with is the underlying entity of `TravelService.Travels`: + +::: code-group + +```cds [srv/travel-invariants.cds] +using { sap.capire.travels.Travels } from '../db/schema'; +annotate Travels with { + + Description @assert: (case + when length(Description) < 3 then 'Description too short' + end); + + Customer @assert: (case + when Customer is null then 'Customer must be specified' + when not exists Customer then 'Customer does not exist' + end); + +} +``` + +::: + +And this works fine for these constraints in this example. However, it may be dangerous if you do that for constraints which refer to other fields, as views on top might not expose these fields. This would immediately lead to compiler errors. Note also, that even though you might think you know all your views, and ensure all related fields are included in all views, somebody that you never meet, builds a new view on top of one of your entity. Hence always **adhere to this strict rule**: + +> [!danger] +> +> Only add invariant constraints to underlying entities that **do not refer to other elements**! diff --git a/guides/services/status-flows.md b/guides/services/status-flows.md new file mode 100644 index 0000000000..9f62684e67 --- /dev/null +++ b/guides/services/status-flows.md @@ -0,0 +1,220 @@ +--- +synopsis: > + Status-transition flows ensure transitions are explicitly modeled, validated, and executed in a controlled and reliable way, thereby eliminating the need for extensive custom coding. +status: released +--- + +# Status-Transition Flows + + +Status-transition flows ensure transitions are explicitly modeled, validated, and executed in a controlled and reliable way, thereby eliminating the need for extensive custom coding. + +> [!info] Status: + +::: details Enabling it for CAP Java + +In CAP Node.js support for flows is built-in and available out of the box. For CAP Java, it is provided by the feature [cds-feature-flow](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-flow). Enable it by adding this dependency to your _srv/pom.xml_ file: + +```xml + + com.sap.cds + cds-feature-flow + runtime + +``` + +::: + +[[toc]] + + +## Modeling Status Flows + +The following example is taken from the [@capire/xtravels](https://github.com/capire/xtravels) sample application, in which we want to model a status flow for travel requests as depicted below: + +![A flow diagram showing three status states connected by arrows. The leftmost oval contains the word Open. An arrow labeled accept points from Open to an oval containing Accepted at the top right. Another arrow labeled reject points from Open to an oval containing Canceled at the bottom right.](../assets/flows/xtravels-flow-simple.svg) + +We can easily model this flow in CDS as follows: + +::: code-group + +```cds [srv/travel-flows.cds] +using { TravelService } from './travel-service'; +annotate TravelService.Travels with @flow.status: Status actions { + acceptTravel @from: [ #Open ] @to: #Accepted; + rejectTravel @from: [ #Open ] @to: #Rejected; + deductDiscount @from: [ #Open ]; // restricted to #Open travels +} +``` +[See full source code](https://github.com/capire/xtravels/tree/main/srv/travel-flows.cds){.learn-more} + +::: + +In essence we model status flows using three annotations: + +- `@flow.status` designates the **status** element for an entity to be flow-controlled. +- `@from` and `@to` define valid entry states and target states for **transitions**, + which are implemented by **bound** actions. + + + + +### @flow.status: element + +Annotation `@flow.status` is an entity-level annotation that identifies the status element for which to establish a status-transition flow. + +This designated status element is expected to be an `enum`, with enum symbols representing the various states of the entity. For example: + +```cds +entity Travels { // ... + @readonly Status : TravelStatusCode default 'O'; +} +``` +```cds +type TravelStatusCode : String enum { + Open = 'O'; + Accepted = 'A'; + Rejected = 'X'; +}; +``` + +Alternatively, the status element can also be an association to a code list entity with a _single_ enum element named `code`, which in turn is an `enum` as outlined above: + +```cds +entity Travels { // ... + @readonly Status : Association to TravelStatus default 'O'; +} +``` +```cds +entity TravelStatus { + key code : TravelStatusCode; // name has to be 'code' + description : localized String; +} +``` + +> [!tip] Combine with @readonly and default +> +> Consider making the status element `@readonly` to prevent clients from setting or modifying them in unmanaged ways. In addition, a `default` value can be specified for new entries. + + + + +### @from: entry state + +In an entity with a designated `@flow.status` element, add the `@from` annotation to a bound action of that entity to define the valid entry states for that action, either as a single value or an array of values: + +```cds +@from: [#Open] action acceptTravel(); +@from: #Open action acceptTravel(); // equivalent +``` + +Use the enum symbols defined with the designated status elements such as `#Open` in the example above. You can also use the raw values such as `'O'`, but using enum symbols is recommended for better readability. + +When the action is invoked, the current state of the entity is validated against the states defined in `@from`. If the current state does not match any of the defined states, the action execution is rejected. + + + +### @to: target state + +Add the `@to` annotation to a bound action of a flow-controlled entity to define the target state for that action, either as a single value or the special value `$flow.previous`: + +```cds +@from: [#Open] @to: #Accepted action acceptTravel(); +``` + +Use the enum symbols defined with the designated status elements such as `#Open` in the example above. You can also use the raw values such as `'O'`, but using enum symbols is recommended for better readability. + +At runtime, after the action execution, the status element of the entity is automatically updated to the target state defined in `@to`. + + + + + +### @to: $flow.previous + +Use the target state `$flow.previous` to return a previous state from a current state that can be reached via different routes. If present in a flow CAP framework will automatically track the sequence of states entered. + +The following example introduces a `Blocked` state with two possible previous states, `Open` and `InReview`, and an `unblock` action that restores the previous state. + +![The graphic is explained in the accompanying text.](../assets/flows/xtravels-flow-previous.svg) + +::: code-group +```cds [srv/flow-previous.cds] +annotate TravelService.Travels with @flow.status: Status actions { + acceptTravel @from: #InReview @to: #Accepted; + rejectTravel @from: #InReview @to: #Rejected; + deductDiscount @from: #Open; + reviewTravel @from: #Open @to: #InReview; // [!code highlight] + reopenTravel @from: #InReview @to: #Open; // [!code highlight] + blockTravel @from: [#Open, #InReview] @to: #Blocked; // [!code highlight] + unblockTravel @from: #Blocked @to: $flow.previous; // [!code highlight] +} +``` +::: + +[See sample in _@capire/xtravels_.](https://github.com/capire/xtravels/tree/main/xmpls/flow) {.learn-more} + + + + +## Served Out-of-the-Box + + + +### By Generic Handlers + +The need for custom code is greatly reduced when using status-transition flows, as CAP automatically provides generic handlers for the common flow operations: validation of entry states before action execution, and updating the status to the target state after action execution. + +- Based on the `@from` annotation, a generic handler validates that the entity is in a valid entry state - the current state must match one of the states specified in `@from`. +If validation fails, the request returns a `409 Conflict` HTTP status code with an appropriate error message. + +- Based on the `@to` annotation, a generic handler automatically updates the entity's status to the target state. + + + +### To Fiori UIs + +When using SAP Fiori elements, status-transition flows are automatically recognized and supported in the generated UIs. + +UI annotations to enable/disable respective buttons and to refresh displayed data are automatically generated for UI5 as shown below: + + +```xml + + + + + in/Status_code + + + + +``` +```xml + + + in/Status_code + O + + +``` + + + + +## Adding Custom Handlers + +While many use cases are covered by the generic handlers, you can add custom handlers for the actions, as usual. For example: + +- Add `before` handlers for additional validations before entering a transition. +- Add `after` handlers for conditional target states. + + + +## Current Limitations + +Following are are some current limitations of status-transition flows, which we plan to address in future releases: + +1. Status-transition flows also work with draft-enabled entities, however when in draft state all actions are disabled. Thus, status transitions can only be performed on active entities. + +2. CRUD and DRAFT operations can't be restricted by status-transition flows today. Only bound actions can be flow-controlled. diff --git a/guides/temporal-data.md b/guides/temporal-data.md index 6c91060341..1def8f7ba7 100644 --- a/guides/temporal-data.md +++ b/guides/temporal-data.md @@ -163,12 +163,12 @@ GET Employees? $expand=jobs($select=role&$expand=dept($select=name)) ``` -The values of `$at`, and so also the respective session variables, would be set to, for example: +The values of `$valid`, and so also the respective session variables, would be set to, for example: | | | | |--------------|----------------------------------|----------------------------| -| `$at.from` = | _session_context('valid-from')_= | _2019-03-08T22:11:00Z_ | -| `$at.to` = | _session_context('valid-to')_ = | _2019-03-08T22:11:00.001Z_ | +| `$valid.from` = | _session_context('valid-from')_= | _2019-03-08T22:11:00Z_ | +| `$valid.to` = | _session_context('valid-to')_ = | _2019-03-08T22:11:00.001Z_ | The result set would be: @@ -193,12 +193,12 @@ GET Employees?sap-valid-at=date'2017-01-01' $expand=jobs($select=role&$expand=dept($select=name)) ``` -The values of `$at` and hence the respective session variables would be set to, for example: +The values of `$valid` and hence the respective session variables would be set to, for example: | | | | |--------------|----------------------------------|----------------------------| -| `$at.from` = | _session_context('valid-from')_= | _2017-01-01T00:00:00Z_ | -| `$at.to` = | _session_context('valid-to')_ = | _2017-01-01T00:00:00.001Z_ | +| `$valid.from` = | _session_context('valid-from')_= | _2017-01-01T00:00:00Z_ | +| `$valid.to` = | _session_context('valid-to')_ = | _2017-01-01T00:00:00.001Z_ | The result set would be: diff --git a/index.md b/index.md index e14dee3470..cb6ce5f369 100644 --- a/index.md +++ b/index.md @@ -31,41 +31,41 @@ hero: features: -- title: Focus on Domain - icon: ⭕️ - details: - •  Capture intent ⇒ What, not how!
- •  Minimized boilerplate coding
- •  Minimized technical debt
- link: about/ - linkText: Read the Primer - - title: Rapid Development - icon: 🌀 + icon: ⭕️ details: - •  Jumpstart with minimal setup
- •  Fast inner loop dev & tests
+ •  Jumpstart w/ minimal setup
+ •  Fast inner loops
•  Grow as you go...
link: get-started/in-a-nutshell linkText: Get Started in a Nutshell - title: Proven Best Practices - icon: 🧩 + icon: 🏆 details: •  Enterprise-grade solutions
- •  Proven in SAP products
+ •  Battle tested in SAP products
•  Served out of the box
link: about/best-practices linkText: Key Concepts & Rationales -- title: Cloud Native +- title: Focus on Domain! + icon: 🍀 + details: + •  Capture intent → What, not how!
+ •  Separation of concerns
+ •  Minimized technical debts
+ link: about/ + linkText: Read the Primer + +- title: Cloud Native by Design icon: 💯 details: - •  Multitenancy, Extensibility, ...
- •  Resilience, Scalability, ...
- •  Intrinsically taken care of
+ •  Multitenancy, Scalability, ...
+ •  Intrinsic Extensibility
+ •  Evolution w/o disruption
link: about/#cloud-native-by-design - linkText: Intrinsic & by Design + linkText: Intrinsic Cloud Qualities --- diff --git a/java/change-tracking.md b/java/change-tracking.md index cdf2ef4b6c..cfbed58a14 100644 --- a/java/change-tracking.md +++ b/java/change-tracking.md @@ -213,7 +213,7 @@ Elements from the `@changelog` annotation value must always be prefixed by the a :::warning Validation required If the target of the association is missing, for example, when an entity is updated with the ID for a customer that does not exist, the changelog entry is not created. You need to validate -such cases in the custom code or use annotations, for example, [`@assert.target`](/guides/providing-services#assert-target). +such cases in the custom code or use annotations, for example, [`@assert.target`](/guides/services/constraints#assert-target). ::: ### Caveats of Identifiers diff --git a/java/cqn-services/remote-services.md b/java/cqn-services/remote-services.md index 26054b2105..5bf9119991 100644 --- a/java/cqn-services/remote-services.md +++ b/java/cqn-services/remote-services.md @@ -234,8 +234,8 @@ The destination or service binding configuration provides the base URL to the OD The full service URL however is built from three parts: 1. The URL provided by the destination or the service binding configuration. -1. An optional URL suffix provided in the _Remote Service_ http configuration under the `suffix` property. -1. The name of the service, either obtained from the optional `service` configuration property or the fully qualified name of the CDS service definition. +1. An optional URL suffix provided in the _Remote Service_ http configuration under the `http.suffix` property. +1. The name of the service, either obtained from the optional `http.service` configuration property or the fully qualified name of the CDS service definition. Consider this example: diff --git a/java/developing-applications/properties.json b/java/developing-applications/properties.json index b9966611ab..84dd5ce67e 100644 --- a/java/developing-applications/properties.json +++ b/java/developing-applications/properties.json @@ -949,7 +949,7 @@ "header": false, "name": "cds.multiTenancy.serviceManager.cacheRefreshInterval", "type": "Duration", - "default": "PT2M", + "default": "PT20M", "doc": "The cache refresh interval (as Duration)." }, { @@ -975,7 +975,7 @@ "header": false, "name": "cds.multiTenancy.hanaMtService.hanaTenantPrefix", "type": "String", - "doc": "Optional prefix for HANA tenant id calculation as hash from _id>" + "doc": "Optional prefix for HANA tenant id calculation as hash from -id>" }, { "header": true, @@ -1805,6 +1805,70 @@ "default": "true", "doc": "Determines, if it is enabled." }, + { + "header": true, + "name": "cds.outbox.persistent.statusLock", + "doc": "Properties to configure the locking strategy using a status column. By default row-level
database locks are used. Only enable status locks, if all deployed instances are already
using @sap/cds-dk >= 9 and CAP Java >= 4.5.0 and have already set
`cds.outbox.persistent.scheduler.enabled` to `true`" + }, + { + "header": false, + "name": "cds.outbox.persistent.statusLock.timeout", + "type": "Duration", + "default": "PT1H", + "doc": "The timeout, after which a message with status PROCESSING is considered for processing by
other instances again." + }, + { + "header": false, + "name": "cds.outbox.persistent.statusLock.enabled", + "type": "boolean", + "default": "false", + "doc": "Determines, if it is enabled." + }, + { + "header": true, + "name": "cds.outbox.persistent.scheduler", + "doc": "Properties for the task-based outbox collector." + }, + { + "header": false, + "name": "cds.outbox.persistent.scheduler.enabled", + "type": "boolean", + "default": "true", + "doc": "Determines, if it is enabled." + }, + { + "header": true, + "name": "cds.outbox.persistent.scheduler.allTenantsTask", + "doc": "Properties to configure regular iterations over outbox tables of all tenants." + }, + { + "header": false, + "name": "cds.outbox.persistent.scheduler.allTenantsTask.startDelay", + "type": "Duration", + "default": "PT30S", + "doc": "The time to wait after startup, before triggering this task for the first time." + }, + { + "header": false, + "name": "cds.outbox.persistent.scheduler.allTenantsTask.interval", + "type": "Duration", + "default": "PT2H", + "doc": "The time to wait between executions of this task." + }, + { + "header": false, + "name": "cds.outbox.persistent.scheduler.allTenantsTask.spreadTime", + "type": "Duration", + "default": "PT15M", + "doc": "The time span over which checks of individual tenants are randomly distributed." + }, + { + "header": false, + "name": "cds.outbox.persistent.scheduler.allTenantsTask.enabled", + "type": "boolean", + "default": "false", + "doc": "Determines, if it is enabled." + }, { "header": true, "name": "cds.outbox.services", @@ -1823,6 +1887,13 @@ "default": "10", "doc": "Specifies the maximum number of attempts to emit a message stored in the Outbox. Messages
that have reached the maximum number of attempts are ignored by the Outbox and need to be
handled by the application." }, + { + "header": false, + "name": "cds.outbox.services..threads", + "type": "int", + "default": "2", + "doc": "Determines the size of the thread pool, used to process outbox tasks. This configuration
only applies to outbox instances configured as `ordered: false`. For outbox instances
configured as `ordered: true` only one thread is used." + }, { "header": false, "name": "cds.outbox.services..ordered", @@ -1851,18 +1922,6 @@ "default": "true", "doc": "Determines, if it is enabled." }, - { - "header": true, - "name": "cds.taskScheduler", - "doc": "Properties for the Task Scheduler." - }, - { - "header": false, - "name": "cds.taskScheduler.enabled", - "type": "boolean", - "default": "false", - "doc": "Determines, if it is enabled." - }, { "header": true, "name": "cds.ucl", diff --git a/java/event-handlers/indicating-errors.md b/java/event-handlers/indicating-errors.md index c7abfdfe8c..475355b0c7 100644 --- a/java/event-handlers/indicating-errors.md +++ b/java/event-handlers/indicating-errors.md @@ -149,8 +149,7 @@ To know which error codes and messages are available by default, you can have a ## Target When SAP Fiori interprets messages it can handle an additional `target` property, which, for example, specifies which element of an entity the message refers to. SAP Fiori can use this information to display the message along the corresponding field on the UI. -When specifying messages in the `sap-messages` HTTP header, SAP Fiori mostly ignores the `target` value. -Therefore, specifying the `target` can only correctly be used when throwing a `ServiceException` as SAP Fiori correctly handles the `target` property in OData V4 error responses. +SAP Fiori interprets `target` property in OData V4 error messages and [draft state messages](../../advanced/fiori#validating-drafts). You can specify the `target` when throwing a `ServiceException` or when setting a validation error using the [Messages API](#messages). A message target is always relative to an input parameter in the event context. For CRUD-based events this is always the `cqn` parameter, which represents and carries the payload of the request. @@ -162,7 +161,7 @@ By default a message target always refers to the CQN statement of the event. In As CRUD event handlers are often called from within bound actions or functions (e.g. `draftActivate`), CAP's OData adapter adds a parameter prefix to a message target referring to the `cqn` parameter only when required. ::: info -When using the `target(String)` API, which specifices the full target as a `String`, no additional parameter prefixes are added by CAP's OData adapter. The `target` value is used as specified. +When using the `target(String)` API, which specifies the full target as a `String`, no additional parameter prefixes are added by CAP's OData adapter. When using this API, [draft state messages](../../advanced/fiori#validating-drafts) can't be invalidated automatically on `PATCH`. ::: Let's illustrate this with the following example: @@ -208,63 +207,39 @@ Here, we have a `CatalogService` that exposes et al. the `Books` entity and a `B ### CRUD Events -Within a `Before` handler that triggers on inserts of new books a message target can only refer to the `cqn` parameter: +Within a `Before` handler that triggers on inserts of new books, a message target can only refer to the `cqn` parameter. You can use generic `String`-based APIs or a typed API backed by the interfaces generated from the CDS model: -``` java +```java @Before public void validateTitle(CdsCreateEventContext context, Books book) { - // ... + Messages messages = context.getMessages(); // event context contains the "cqn" key + // target implicitly refers to cqn + messages.error("No title specified").target(b -> b.get("title")); - // implicitly referring to cqn - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget(b -> b.get("title")); - - // which is equivalent to explicitly referring to cqn - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget("cqn", b -> b.get("title")); + // which is equivalent to a target explicitly referring to cqn + messages.error("No title specified").target("cqn", b -> b.get("title")); - // which is the same as using plain string - // assuming direct POST request - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget("title"); - - // which is the same as using plain string - // assuming surrounding bound action request with binding parameter "in", - // e.g. draftActivate - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget("in/title"); + // using interfaces generated from the CDS model + messages.error("No title specified").target(Books_.class, b -> b.title()); } ``` -Instead of using the generic API for creating the relative message target path, CAP Java SDK also provides a typed API backed by the CDS model: - -``` java -@Before -public void validateTitle(CdsCreateEventContext context, Books book) { - // ... - - // implicitly referring to cqn - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget(Books_.class, b -> b.title()); -} -``` +Depending on the OData request context, the resulting target in this example is either `title` (assuming a `POST` or `PATCH` request) or `in/title` (assuming a bound action like `draftActivate`). -This also works for nested paths with associations: +You can also specify targets that point to elements within associations: -``` java +```java @Before public void validateAuthorName(CdsCreateEventContext context, Books book) { - // ... + Messages messages = context.getMessages(); - // using un-typed API - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No title specified") - .messageTarget(b -> b.to("author").get("name")); + // using untyped API + messages.error("No title specified").target(b -> b.to("author").get("name")); // using typed API - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "No author name specified") - .messageTarget(Books_.class, b -> b.author().name()); + messages.error("No title specified").target(Books_.class, b -> b.author().name()); } ``` @@ -276,30 +251,25 @@ The same applies to message targets that refer to an action or function input pa ``` java @Before public void validateReview(BooksAddReviewContext context) { - // ... - + Messages messages = context.getMessages(); // event context contains the keys "reviewer", "rating", "title", "text", // which are the input parameters of the action "addReview" // referring to action parameter "reviewer", targeting "firstName" - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid reviewer first name") - .messageTarget("reviewer", r -> r.get("firstName")); + messages.error("Invalid reviewer first name").target("reviewer", r -> r.get("firstName")); // which is equivalent to using the typed API - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid reviewer first name") - .messageTarget(BooksAddReviewContext.REVIEWER, Reviewer_.class, r -> r.firstName()); + messages.error("Invalid reviewer first name") + .target(BooksAddReviewContext.REVIEWER, Reviewer_.class, r -> r.firstName()); // targeting "rating" - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review rating") - .messageTarget("rating"); + messages.error("Invalid review rating").target("rating"); // targeting "title" - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review title") - .messageTarget("title"); + messages.error("Invalid review title").target("title"); // targeting "text" - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid review text") - .messageTarget("text"); + messages.error("Invalid review text").target("text"); } ``` @@ -310,27 +280,21 @@ For the `addReview` action that is the `Books` entity, as in the following examp ``` java @Before public void validateReview(BooksAddReviewContext context) { - // ... - + Messages messages = context.getMessages(); // referring to the bound entity `Books` - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description") - .messageTarget(b -> b.get("descr")); - - // or (using the typed API, referring to "cqn" implicitly) - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description") - .messageTarget(Books_.class, b -> b.descr()); + messages.error("Invalid book description").target(b -> b.get("descr")); - // which is the same as using plain string - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Invalid book description") - .messageTarget("in/descr"); + // or using the typed API + messages.error("Invalid book description").target(Books_.class, b -> b.descr()); } ``` +In the case of OData the resulting target is `in/descr`, assuming the default name `in` is used for the binding parameter name. + ::: tip -The previous examples showcase the target creation with the `ServiceException` API, but the same can be done with the `Message` API and the respective `target(...)` methods. +The previous examples showcase the target creation with the `Messages` API, but the same can be done with the `ServiceException` API and the respective `messageTarget(...)` methods. ::: - ## Error Handler { #errorhandler} An [exception](#exceptions) thrown in an event handler will stop the processing of the request. As part of that, protocol adapters trigger the `ERROR_RESPONSE` event of the [Application Lifecycle Service](https://www.javadoc.io/doc/com.sap.cds/cds-services-api/latest/com/sap/cds/services/application/ApplicationLifecycleService.html). By default, this event combines the thrown exception and the [messages](#messages) from the `RequestContext` in a list to produce the error response. OData V4 and V2 protocol adapters will use this list to create an OData error response with the first entry being the main error and the remaining entries in the details section. diff --git a/java/reflection-api.md b/java/reflection-api.md index 864965c074..c2a9be2697 100644 --- a/java/reflection-api.md +++ b/java/reflection-api.md @@ -259,7 +259,7 @@ cds: ::: #### Custom Implementation -Applications can implement a custom [`FeatureTogglesInfoProvider`](https://javadoc.io/doc/com.sap.cds/cds-services-api/latest/com/sap/cds/services/runtime/FeatureTogglesInfoProvider.html) that computes a `FeatureTogglesInfo` based on the request's [`UserInfo`](https://www.javadoc.io/static/com.sap.cds/cds-services-api/latest/com/sap/cds/services/request/UserInfo.html) and [`ParameterInfo`](https://www.javadoc.io/static/com.sap.cds/cds-services-api/latest/com/sap/cds/services/request/ParameterInfo.html). +Applications can implement a custom [`FeatureTogglesInfoProvider`](https://javadoc.io/doc/com.sap.cds/cds-services-api/latest/com/sap/cds/services/runtime/FeatureTogglesInfoProvider.html) that computes a `FeatureTogglesInfo` based on the request's [`UserInfo`](https://www.javadoc.io/doc/com.sap.cds/cds-services-api/latest/com/sap/cds/services/request/UserInfo.html) and [`ParameterInfo`](https://www.javadoc.io/doc/com.sap.cds/cds-services-api/latest/com/sap/cds/services/request/ParameterInfo.html). The following example demonstrates a feature toggles info provider that enables the feature `isbn` if the user has the `expert` role: diff --git a/java/working-with-cql/query-execution.md b/java/working-with-cql/query-execution.md index 3b6b3c652c..0d952c4a3b 100644 --- a/java/working-with-cql/query-execution.md +++ b/java/working-with-cql/query-execution.md @@ -283,11 +283,11 @@ UPDATE entity OrderView2 - [Path expressions](../../cds/cql#path-expressions) over compositions *of one* (*header.status*) are writable. For [inserts](./query-api#insert), the view must expose all *not null* elements of the target entity and the data must include values for all of them. In the example above, the order header must have a generated key to support inserting new orders with a value for *headerStatus*. ::: warning Handling Compositions and Aliased Paths in Projections - For projections that include *to-one* compositions (*header*) and aliased paths over these compositions (*headerStatus*), write structured data using the composition and make the aliased path [@readonly](../../guides/providing-services#readonly). Do not use data for the aliased path along with structured data for the composition in the same statement. + For projections that include *to-one* compositions (*header*) and aliased paths over these compositions (*headerStatus*), write structured data using the composition and make the aliased path [@readonly](../../guides/services/constraints#readonly). Do not use data for the aliased path along with structured data for the composition in the same statement. ::: ::: warning Path Expressions over Associations - Path expressions navigating *associations* (*header.customer.name*) are [not writable](#cascading-over-associations) by default. To avoid issues on write, annotate them with [@readonly](../../guides/providing-services#readonly). + Path expressions navigating *associations* (*header.customer.name*) are [not writable](#cascading-over-associations) by default. To avoid issues on write, annotate them with [@readonly](../../guides/services/constraints#readonly). ::: ### Delete through Views { #delete-via-view } @@ -321,13 +321,12 @@ The delete operation is resolved to the underlying `Order` entity with ID *42* a ### Runtime Views { #runtimeviews } -To add or update CDS views without redeploying the database schema, annotate them with [@cds.persistence.skip](../../guides/databases#cds-persistence-skip). This advises the CDS compiler to skip generating database views for these CDS views. Instead, CAP Java resolves them *at runtime* on each request. -Runtime views must be simple [projections](../../cds/cdl#as-projection-on), not using *aggregations*, *join*, *union* or *subqueries* in the *from* clause, but may have a *where* condition if they are only used to read. On write, the restrictions for [write through views](#updatable-views) apply in the same way as for standard CDS views. However, if a runtime view cannot be resolved, a fallback to database views is not possible, and the statement fails with an error. +CAP Java provides two modes for resolving [runtime views](../../cds/cdl#runtimeviews) during read operations: [cte](#rtview-cte) and [resolve](#rtview-resolve). -CAP Java provides two modes for resolving runtime views during read operations: [cte](#rtview-cte) and [resolve](#rtview-resolve). +On write, the restrictions for [write through views](#updatable-views) apply in the same way as for standard CDS views. However, if a runtime view cannot be resolved, a fallback to database views is not possible, and the statement fails with an error. -::: details Changing the runtime view mode +::: details Changing the runtime view read mode To globally set the runtime view mode, use the property `cds.sql.runtimeView.mode` with value `cte` (the default) or `resolve` in the *application.yml*. To set the mode for a specific runtime view, annotate it with `@cds.java.runtimeView.mode: cte|resolve`. To set the mode for a specific query, use a [hint](#hana-hints): @@ -337,42 +336,9 @@ Select.from(BooksWithLowStock).hint("cds.sql.runtimeView.mode", "resolve"); ``` ::: -The next two sections introduce both modes using the following CDS model and query: - -```cds -entity Books { - key ID : UUID; - title : String; - stock : Integer; - author : Association to one Authors; -} -@cds.persistence.skip -entity BooksWithLowStock as projection on Books { - ID, title, author.name as author -} where stock < 10; // makes the view read only -``` -```sql -SELECT from BooksWithLowStock where author = 'Kafka' -``` - - #### Read in `cte` mode { #rtview-cte } -This is the default mode since CAP Java `4.x`. The runtime translates the [view definition](#runtimeviews) into a _Common Table Expression_ (CTE) and sends it with the query to the database. - -```sql -WITH BOOKSWITHLOWSTOCK_CTE AS ( - SELECT B.ID, - B.TITLE, - A.NAME AS "AUTHOR" - FROM BOOKS B - LEFT OUTER JOIN AUTHOR A ON B.AUTHOR_ID = A.ID - WHERE B.STOCK < 10 -) -SELECT ID, TITLE, AUTHOR AS "author" - FROM BOOKSWITHLOWSTOCK_CTE - WHERE A.NAME = ? -``` +In [cte mode](../../cds/cdl#runtimeviews), the runtime translates the view definition into a _Common Table Expression_ (CTE) and sends it with the query to the database. This is the default mode since CAP Java `4.x`. ::: tip CAP Java 3.10 Enable *cte* mode with *cds.sql.runtimeView.mode: cte* @@ -380,7 +346,9 @@ Enable *cte* mode with *cds.sql.runtimeView.mode: cte* #### Read in `resolve` mode { #rtview-resolve } -The runtime _resolves_ the [view definition](#runtimeviews) to the underlying persistence entities and executes the query directly against the corresponding tables. +In `resolve` mode, the runtime _resolves_ the view definition to the underlying persistence entities and executes the query directly against the corresponding tables. + +For example, the [view definition](../../cds/cdl#runtimeviews) is resolved into the following SQL statement: ```sql SELECT B.ID, B.TITLE, A.NAME AS "author" diff --git a/menu.md b/menu.md index 53a289d38e..556c39adfb 100644 --- a/menu.md +++ b/menu.md @@ -22,10 +22,11 @@ ## [Providing Services](guides/providing-services) - ### [Intro: Core Concepts](guides/providing-services#introduction) + ### [Core Concepts](guides/providing-services#introduction) ### [Service Definitions](guides/providing-services#service-definitions) - ### [Generic Providers](guides/providing-services#generic-providers) - ### [Input Validation](guides/providing-services#input-validation) + ### [Served Out-of-the-Box](guides/providing-services#generic-providers) + ### [Status Flows](guides/services/status-flows) + ### [Constraints](guides/services/constraints) ### [Custom Logic](guides/providing-services#custom-logic) ### [Actions & Functions](guides/providing-services#actions-functions) ### [Status-Transition Flows](guides/providing-services#status-transition-flows) @@ -126,7 +127,8 @@ ## [Schema Notation (CSN)](cds/csn) ## [Query Language (CQL)](cds/cql) ## [Query Notation (CQN)](cds/cqn) -## [Expressions (CXN)](cds/cxn) +## [Expression Language (CXL)](cds/cxl) +## [Expression Notation (CXN)](cds/cxn) ## [Core / Built-in Types](cds/types) ## [Common Reuse Types](cds/common) ## [Common Annotations](cds/annotations) diff --git a/node.js/authentication.md b/node.js/authentication.md index 99e253f179..338219f766 100644 --- a/node.js/authentication.md +++ b/node.js/authentication.md @@ -188,6 +188,9 @@ This strategy creates a user that passes all authorization checks. It's meant fo This authentication strategy uses basic authentication with pre-defined mock users during development. +::: warning Mocked authentication is not suitable for production! +::: + > **Note:** When testing different users in the browser, it's best to use an incognito window, because logon information might otherwise be reused. **Configuration:** Choose this strategy as follows: @@ -254,48 +257,10 @@ If you want to restrict these additional logins, you need to overwrite the defau } ``` - -### Basic Authentication {#basic } - -This authentication strategy uses basic authentication to use mock users during development. - -> **Note:** When testing different users in the browser, it's best to use an incognito window, because logon information might otherwise be reused. - -**Configuration:** Choose this strategy as follows: - -::: code-group -```json [package.json] -"cds": { - "requires": { - "auth": "basic" - } -} -``` +::: tip +The pre-defined mock users can be deactivated by using kind `basic` instead of `mocked`. In that case configure users yourself, as described previously. ::: -You can optionally configure users as follows: - -::: code-group -```json [package.json] -"cds": { - "requires": { - "auth": { - "kind": "basic", - "users": { - "": { - "password": "", - "roles": [ "", ... ], - "attr": { ... } - } - } - } - } -} -``` -::: - -In contrast to [mocked authentication](#mocked), no default users are automatically added to the configuration. - ### JWT-based Authentication { #jwt } diff --git a/node.js/best-practices.md b/node.js/best-practices.md index 218708d51e..536f2e7db0 100644 --- a/node.js/best-practices.md +++ b/node.js/best-practices.md @@ -201,7 +201,7 @@ If a CSRF token is cached, it can potentially be reused in multiple requests, de #### Using App Router -The _App Router_ is configured to require a _CSRF_ token by default for all protected routes and all HTTP requests methods except _HEAD_ and _GET_. Thus, by adding the _App Router_ as described in the [Deployment Guide: Using App Router as Gateway](../guides/deployment/to-cf#add-app-router), endpoints are CSRF protected. +The _App Router_ is configured to require a _CSRF_ token by default for all protected routes and all HTTP requests methods except _HEAD_ and _GET_. Thus, by using an _App Router_ as described in the [_Deployment_ guide](../guides/deployment/to-cf#add-ui), endpoints are CSRF protected. [Learn more about CSRF protection with the **App Router**](https://help.sap.com/docs/BTP/65de2977205c403bbc107264b8eccf4b/c19f165084d742e096c5d1625cecd2d4.html?q=csrf#loioc19f165084d742e096c5d1625cecd2d4__section_xj4_pcg_2z){.learn-more} @@ -278,7 +278,7 @@ cds.on('bootstrap', app => app.use ((req, res, next) => { #### Configuring CORS in App Router -The _App Router_ has full support for CORS. Thus, by adding the _App Router_ as described in the [Deployment Guide: Using App Router as Gateway](../guides/deployment/to-cf#add-app-router), CORS can be configured in the _App Router_ configuration. +The _App Router_ has full support for CORS. Thus, by adding the _App Router_ as described in the [_Deployment_ guide](../guides/deployment/to-cf#add-ui), CORS can be configured in the _App Router_ configuration. [Learn more about CORS handling with the **App Router**](https://help.sap.com/docs/BTP/65de2977205c403bbc107264b8eccf4b/ba527058dc4d423a9e0a69ecc67f4593.html?q=allowedOrigin#loioba527058dc4d423a9e0a69ecc67f4593__section_nt3_t4k_sz){.learn-more} diff --git a/node.js/cds-connect.md b/node.js/cds-connect.md index 99d39053ff..1b0a817cb8 100644 --- a/node.js/cds-connect.md +++ b/node.js/cds-connect.md @@ -46,7 +46,8 @@ async function cds.connect.to ( Argument `name` is used to look up connect options from [configured services](#cds-env-requires), which are defined in the `cds.requires` section of your _package.json_ or _.cdsrc.json_ or _.yaml_ files. -Argument `options` also allows to pass additional options such as `credentials` programmatically, and thus create services without configurations and [service bindings](#service-bindings), for example, you could connect to a local SQLite database in your tests like this: +Argument `options` also allows to pass additional options programmatically. The available and supported properties of options depend on the selected `kind`. +Each `kind` defines its own set of expected configuration properties (for example, `credentials`, `model`, `service`). This allows creating services without configurations and [service bindings](#service-bindings). For example, you could connect to a local SQLite database in your tests like this: ```js const db2 = await cds.connect.to ({ diff --git a/node.js/cds-reflect.md b/node.js/cds-reflect.md index b364f571ff..5ed61f23fa 100644 --- a/node.js/cds-reflect.md +++ b/node.js/cds-reflect.md @@ -108,15 +108,22 @@ Example: ```js let m = cds.linked` namespace my.bookshop; - entity Books {...} - entity Authors {...} + entity Books {} + entity Authors {} + service CatalogService { + entity Books as projection on my.bookshop.Books; + entity Authors as projection on my.bookshop.Authors; + } ` + // Function nature let { Books, Authors } = m.entities ('my.bookshop') - -// Object nature (uses the model's top-level namespace) -let { Books, Authors } = m.entities +// Object nature +let { + 'my.bookshop.Books': Books, + 'my.bookshop.Authors': Authors +} = m.entities // Array nature for (let each of m.entities) console.log (each.name) diff --git a/node.js/core-services.md b/node.js/core-services.md index cac08dc897..0dd8836796 100644 --- a/node.js/core-services.md +++ b/node.js/core-services.md @@ -396,19 +396,23 @@ var srv.options : { //> from cds.requires config -### . entities {.property alt="The following documentation on operations also applies to entities. "} +### . entities {.property alt="The following documentation on actions also applies to entities. "} -### . events {.property alt="The following documentation on operations also applies to events. "} +### . events {.property alt="The following documentation on actions also applies to events. "} -### . operations {.property} +### . operations {.property .deprecated alt="The following documentation on actions also applies to operations. "} + +Use [`.actions`](#actions) instead. + +### . actions {.property} ```tsx -var srv.entities/events/operations : Iterable <{ +var srv.entities/events/actions : Iterable <{ name : CSN definition }> ``` -These properties provide convenient access to the CSN definitions of the *entities*, *events* and operations — that is *actions* and *functions* — exposed by this service. +These properties provide convenient access to the CSN definitions of the *entities*, *events* and *actions* (incl. *functions*) exposed by this service. They are *iterable* objects, which means you can use them in all of these ways: @@ -423,7 +427,6 @@ for (let d of this.entities) //... d is a CSN definition - ### srv. init() {.method} ```tsx diff --git a/node.js/events.md b/node.js/events.md index 650f4998dc..4add035ad7 100644 --- a/node.js/events.md +++ b/node.js/events.md @@ -431,7 +431,7 @@ this.on('CREATE', Books, req => { ``` ::: details **Best Practice:**{.good} Use the `@mandatory` annotation instead. -The sample above is just for illustration. Instead, use the [`@mandatory`](../guides/providing-services.md#mandatory) +The sample above is just for illustration. Instead, use the [`@mandatory`](../guides/services/constraints#mandatory) annotation in your CDS model to define mandatory inputs like that: ```cds diff --git a/node.js/fiori.md b/node.js/fiori.md index 4dbb46664e..77a81782f0 100644 --- a/node.js/fiori.md +++ b/node.js/fiori.md @@ -10,15 +10,19 @@ See [Cookbook > Serving UIs > Draft Support](../advanced/fiori#draft-support) fo [[toc]] + + + + ## Draft Entities {#draft-support} Draft-enabled entities have corresponding CSN entities for drafts: @@ -135,6 +139,7 @@ srv.on('someAction', [ MyEntity, MyEntity.drafts ], /*...*/) ``` + ## Draft Locks To prevent inconsistency, the entities with draft are locked for modifications by other users. The lock is released when the draft is saved, canceled or a timeout is hit. The default timeout is 15 minutes. You can configure this timeout by the following application configuration property: @@ -153,6 +158,7 @@ If the `draft_lock_timeout` has been reached, every user can delete other users' ::: + ## Draft Timeouts Inactive drafts are deleted automatically after the default timeout of 30 days. You can configure or deactivate this timeout by the following configuration: @@ -178,43 +184,42 @@ It can occur that inactive drafts are still in the database after the configured ::: -## Bypassing Drafts -Creating or modifying active instances directly is possible without creating drafts. This comes in handy when technical services without a UI interact with each other. -To enable this feature, set this feature flag in your configuration: +## Bypassing Drafts {.deprecated} + +Use [Direct CRUD](#direct-crud) instead. + +Until the next major release (`cds10`), you can still activate the draft bypass without also allowing direct CRUD via cds.fiori.bypass_draft:true. -```json -{ - "cds": { - "fiori": { - "bypass_draft": true - } - } -} -``` -You can then create active instances directly: + +## Direct CRUD + +With cds.fiori.direct_crud:true, creating or modifying active instances directly is possible without creating drafts. +This comes in handy when technical services without a UI interact with each other. + +That is, you can then create and modify active instances directly: ```http POST /Books { - "ID": 123, - "IsActiveEntity": true + "ID": 123 } ``` -You can modify them directly: - ```http -PATCH /Books(ID=123,IsActiveEntity=true) +PUT /Books(ID=123) { "title": "How to be more active" } ``` -This feature is required to enable [SAP Fiori Elements Mass Edit](https://sapui5.hana.ondemand.com/sdk/#/topic/965ef5b2895641bc9b6cd44f1bd0eb4d.html), allowing users to change multiple objects with the +For this, the default draft creation behavior by SAP Fiori Elements is redirected to a collection-bound action via annotation `@Common.DraftRoot.NewAction`. +The thereby freed `POST` request to draft roots without specifying `IsActiveEntity` leads to the creation of an active instance (as it would without draft enablement). + +The feature is required to enable [SAP Fiori Elements Mass Edit](https://sapui5.hana.ondemand.com/sdk/#/topic/965ef5b2895641bc9b6cd44f1bd0eb4d.html), allowing users to change multiple objects with the same editable properties without creating drafts for each row. :::warning Additional entry point @@ -223,6 +228,7 @@ payloads rather than the complete business object. ::: + ## Programmatic APIs You can programmatically invoke draft actions with the following APIs: diff --git a/package-lock.json b/package-lock.json index 346c4660bc..ecc8bf27d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1562,9 +1562,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -2343,15 +2343,15 @@ ] }, "node_modules/@sap/cds": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.5.1.tgz", - "integrity": "sha512-rMvDSRytjqYQolB0pg8tiBlpS9kKGcleRhpZmBGUmSncbbwnotKYTKoDyMCWkflS8P9/Jq9YfY1qhK+fduHCVA==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.6.1.tgz", + "integrity": "sha512-Bx6asOBhYXBE+jB6FtmyHmAjoPi11MYb/v/AYpnp0jqL/+kAXXWo6YOxJHqpx1k5Um5FS1r6gM/5tVAo/ta7bw==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { "@sap/cds-compiler": "^6.3", "@sap/cds-fiori": "^2", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.1" }, "bin": { "cds-deploy": "bin/deploy.js", @@ -2825,16 +2825,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "engines": { @@ -2850,14 +2850,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "engines": { @@ -2872,14 +2872,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2890,9 +2890,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", "dev": true, "license": "MIT", "engines": { @@ -2907,9 +2907,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -2921,16 +2921,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -2949,13 +2949,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3415,6 +3415,22 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4330,9 +4346,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -4342,7 +4358,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4451,22 +4467,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4932,6 +4932,19 @@ "node": ">=20" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5056,9 +5069,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5627,9 +5640,9 @@ } }, "node_modules/markdownlint": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.39.0.tgz", - "integrity": "sha512-Xt/oY7bAiHwukL1iru2np5LIkhwD19Y7frlsiDILK62v3jucXCD6JXlZlwMG12HZOR+roHIVuJZrfCkOhp6k3g==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", + "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", "dev": true, "license": "MIT", "dependencies": { @@ -5640,7 +5653,8 @@ "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.2" + "micromark-util-types": "2.0.2", + "string-width": "8.1.0" }, "engines": { "node": ">=20" @@ -5650,9 +5664,9 @@ } }, "node_modules/markdownlint-cli": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.46.0.tgz", - "integrity": "sha512-4gxTNzPjpLnY7ftrEZD4flPY0QBkQLiqezb6KURFSkV+vPHFOsYw8OMtY6fu82Yt8ghtSrWegpYdq1ix25VFLQ==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.47.0.tgz", + "integrity": "sha512-HOcxeKFAdDoldvoYDofd85vI8LgNWy8vmYpCwnlLV46PJcodmGzD7COSSBlhHwsfT4o9KrAStGodImVBus31Bg==", "dev": true, "license": "MIT", "dependencies": { @@ -5663,7 +5677,7 @@ "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", "markdown-it": "~14.1.0", - "markdownlint": "~0.39.0", + "markdownlint": "~0.40.0", "minimatch": "~10.1.1", "run-con": "~1.3.2", "smol-toml": "~1.5.2", @@ -7268,9 +7282,9 @@ "peer": true }, "node_modules/sass": { - "version": "1.94.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", - "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", + "version": "1.97.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.0.tgz", + "integrity": "sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7612,6 +7626,23 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7627,6 +7658,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", diff --git a/plugins/index.md b/plugins/index.md index 7754fb3a6d..912373ae17 100644 --- a/plugins/index.md +++ b/plugins/index.md @@ -168,7 +168,12 @@ Available for: ## Attachments -The Attachments plugin provides out-of-the-box support for attachment handling. On Node.js, attachments are stored on AWS S/3 through [SAP BTP's Object Store service](https://discovery-center.cloud.sap/serviceCatalog/object-store). For Java: When using the package [`cds-feature-attachments-oss`](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-attachments-oss), depending on your cloud environment, attachments are stored on AWS S/3, Microsoft Azure, or the Google object store through [SAP BTP's Object Store service](https://discovery-center.cloud.sap/serviceCatalog/object-store). When using the package [`cds-feature-attachments`](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-attachments), attachments are stored on the SAP HANA database. +The Attachments plugin enables efficient management of file attachments within your applications. By default, attachments are stored in the SAP HANA database. + +For Java, use the package [`cds-feature-attachments`](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-attachments). For Node.js, this is supported by the standard plugin. + +To integrate with cloud storage solutions such as `AWS S3`, `Azure Blob Storage`, or `Google Cloud Storage` through [SAP BTP's Object Store service](https://discovery-center.cloud.sap/serviceCatalog/object-store), use the [`cds-feature-attachments-oss`](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-attachments-oss) package for Java or the [`@cap-js/attachments`](https://www.npmjs.com/package/@cap-js/attachments) package for Node.js. + To use the Attachments plugin, simply add a composition of the predefined aspect `Attachments` like so: ```cds @@ -184,7 +189,7 @@ That's all we need to automatically add an interactive list of attachments to yo Features: -- Pre-defined type `Attachment` to use in entity definitions +- Pre-defined type `Attachments` to use in entity definitions - Automatic handling of all upload and download operations - Automatic malware scanning for uploaded files - (Automatic) Fiori Annotations for Upload Controls diff --git a/tools/assets/cds-export.png b/tools/assets/cds-export.png new file mode 100644 index 0000000000..a6d2848880 Binary files /dev/null and b/tools/assets/cds-export.png differ diff --git a/tools/assets/help/cds-version-md.out.md b/tools/assets/help/cds-version-md.out.md index 6341e11d9b..7653e5bdec 100644 --- a/tools/assets/help/cds-version-md.out.md +++ b/tools/assets/help/cds-version-md.out.md @@ -4,14 +4,14 @@ | your-project | https://github.com/<your/repo> | | ---------------------- | --------------------------------------- | -| @sap/cds | 9.5.1 | -| @sap/cds-compiler | 6.5.0 | +| @sap/cds | 9.6.0 | +| @sap/cds-compiler | 6.6.0 | | @sap/cds-dk (global) | 9.5.0 | | @sap/cds-fiori | 2.1.1 | | @sap/cds-mtxs | 3.5.0 | | @cap-js/asyncapi | 1.0.3 | -| @cap-js/db-service | 2.7.0 | +| @cap-js/db-service | 2.8.0 | | @cap-js/openapi | 1.2.3 | -| @cap-js/sqlite | 2.1.0 | +| @cap-js/sqlite | 2.1.1 | | Node.js | v22.21.1 | diff --git a/tools/assets/help/cds-version.out.md b/tools/assets/help/cds-version.out.md index 9d04283d0c..872d6ece56 100644 --- a/tools/assets/help/cds-version.out.md +++ b/tools/assets/help/cds-version.out.md @@ -2,14 +2,14 @@
 > cds version
 
-@sap/cds: 9.5.1
-@sap/cds-compiler: 6.5.0
+@sap/cds: 9.6.0
+@sap/cds-compiler: 6.6.0
 @sap/cds-dk (global): 9.5.0
 @sap/cds-fiori: 2.1.1
 @sap/cds-mtxs: 3.5.0
 @cap-js/asyncapi: 1.0.3
-@cap-js/db-service: 2.7.0
+@cap-js/db-service: 2.8.0
 @cap-js/openapi: 1.2.3
-@cap-js/sqlite: 2.1.0
+@cap-js/sqlite: 2.1.1
 Node.js: v22.21.1
 
diff --git a/tools/cds-cli.md b/tools/cds-cli.md index d4e986bd99..68370ccdc8 100644 --- a/tools/cds-cli.md +++ b/tools/cds-cli.md @@ -259,7 +259,7 @@ The result could look like this for a typical _Books_ entity from the _Bookshop_ - `author.ID` refers to a key from the _...Authors.json_ file that is created at the same time. If the _Authors_ entity is excluded, though, no such foreign key would be created, which cuts the association off. - Data for _compositions_, like the `texts` composition to `Books.texts`, is always created. - A random unique number for each record, _29894036_ here, is added to each string property, to help you correlate properties more easily. -- Data for elements annotated with a regular expression using [`assert.format`](../guides/providing-services#assert-format) can be generated using the NPM package [randexp](https://www.npmjs.com/package/randexp), which you need to installed manually. +- Data for elements annotated with a regular expression using [`assert.format`](../guides/services/constraints#assert-format) can be generated using the NPM package [randexp](https://www.npmjs.com/package/randexp), which you need to installed manually. - Other constraints like [type formats](../cds/types), [enums](../cds/cdl#enums), and [validation constraints](../guides/providing-services#input-validation) are respected as well, in a best effort way. ::: @@ -468,6 +468,70 @@ To customize the diagram layout, use these settings in the _Cds > Preview_ categ - [Diagram: Namespaces](vscode://settings/cds.preview.diagram.namespaces) - [Diagram: Queries](vscode://settings/cds.preview.diagram.queries) + +## cds export + +With `cds export` you create an API client package to be used +for data exchange via CAP-level Service integration ("Calesi"). + +Define data provider services in your CDS model that serve as an interface to your data, placing each data provider service in a separate file. + +For the [xflights](https://github.com/capire/xflights) sample app, +an API that provides information about flights, airports, and airlines +could look like this: + +::: code-group + +```cds [srv/data-service.cds] +using { sap.capire.flights as my } from '../db/schema'; + +@data.product @hcql @rest @odata +service sap.capire.flights.data { + @readonly entity Flights as projection on my.Flights; + @readonly entity Airlines as projection on my.Airlines; + @readonly entity Airports as projection on my.Airports; +} +``` + +::: + +Then create an API client package for this service: + +```sh +cds export srv/data-service.cds +``` + +The command generates the API client package into a new folder _apis/data-service_. + +![The screenshot is described in the accompanying text.](assets/cds-export.png) {style="filter: drop-shadow(0 2px 5px rgba(0,0,0,.40));"} + +The `service.csn` contains only the interface defined in the service, removing the query part of the entities and all the underlying model. +In addition, there are i18n bundles with the localized metadata relevant +for the interface, and a _data_ folder with test data +that exactly matches the structure of the entities in the API. + +`cds export` also adds a _package.json_. The package name combines the application name (from the main _package.json_) with the file name of the data service. In our example, this results in `@capire/xflights-data-service`. +You can change this name as appropriate. + +You can then publish the generated package, for example, via `npm publish`. + +To consume the API in another CAP application: +1. Import the API package with `npm add` +2. Define consumption views on the imported entities +3. Use them in your model as if they were local entities +4. Add custom code to access the data in the provider app via any of the offered protocols + +Have a look at the [xtravels](https://github.com/capire/xtravels) sample app for an +example of using an API client package. + +:::warning Do not use EDMX to exchange API information +Prefer exporting and importing API packages via `cds export` and `npm add`. +**Do not use** EDMX (or OpenAPI) as intermediate format for exchanging API information +between CAP applications, as you might loose information. +::: + + + ## cds watch Use `cds watch` to watch for changed files, restarting your Node.js server. @@ -660,7 +724,7 @@ Make sure the port matches to what the debug tunnel uses (see the message in the > [!NOTE] SapMachine is required > SapMachine is required as Java runtime environment for this feature to work.
-> There is nothing to do if you set up your MTA deployment descriptors with [`cds mta`](../guides/deployment/to-cf#add-mta-yaml) or CAP project wizards. +> There is nothing to do if you set up your MTA deployment descriptors with [`cds add mta`](../guides/deployment/to-cf#add-mta-yaml) or CAP project wizards. > See the [documentation of SapMachine](https://help.sap.com/docs/btp/sap-business-technology-platform/sapmachine) for how to configure this manually. #### Local Applications