Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tests/dynamic-constraints/fts/some-feature.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using { sap.ariba.buying.Requisitions } from '../srv/etc/requisition';

annotate Requisitions with {
buyer @assert: (buyer is null ? 'is missing' : null);
}
37 changes: 37 additions & 0 deletions tests/dynamic-constraints/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Experimental Dynamic Constraints

This example demonstrates how to use dynamic constraints in a CAP application. It includes a service definition and a test setup to validate the constraints.


### Prerequisites

You've setup the [_cap/samples_](https://github.com/capire/samples) like so:

```sh
git clone -b dynamic-constraints -q https://github.com/capire/samples cap/samples
cd cap/samples
npm install
```

### Testing

Test like that in `cds.repl` from _cap/samples_ root:

```sh
cds repl --run tests/dynamic-constraints
````

```js
await AdminService.create ('Books', {})
await AdminService.create ('Books', { title:' ', author_ID:150 })
await AdminService.create ('Books', { title:'x' })
```

```js
await cds.validate (Books.constraints, 201)
await cds.validate (Books.constraints)
```

```js
await AdminService.read `ID, title, price, fc.price from Books`
```
17 changes: 17 additions & 0 deletions tests/dynamic-constraints/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Quick and dirty implementation for cds.validate()
// using db-level constraints.
//

const cds = require('@sap/cds'); require('./validate.js')
cds.on('served', ()=> {
const { AdminService } = cds.services
AdminService.after (['CREATE','UPDATE'], (_,req) => cds.validate (req))
})



Object.defineProperties (cds.entity.prototype, {
constraints: { get() { return cds.model.definitions[this.name+'.constraints'] }},
controls: { get() { return cds.model.definitions[this.name+'.field.control'] }},
})
14 changes: 14 additions & 0 deletions tests/dynamic-constraints/srv/admin-service.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using { AdminService } from '@capire/bookshop';
namespace AdminService; //> for cds.entities

annotate AdminService with @odata.draft.enabled;
annotate AdminService with @requires: false;

extend AdminService.Authors with columns {
// null as books // to simulate the exclusion of books
}

// Should be provided by CAP ootb:
extend AdminService.Books with columns {
active : Association to AdminService.Books on active.ID = $self.ID,
}
20 changes: 20 additions & 0 deletions tests/dynamic-constraints/srv/etc/catalog.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using { cuid, Currency } from '@sap/cds/common';

context sap.ariba.catalog {

entity Product : cuid { // = ProductDescription in Ariba CG
name : String(111);
descr : String(1111);
price : Decimal(10,2);
stock : Integer;
suppliers : Association to many Suppliers;
}

entity Suppliers : cuid {
name : String(111);
contact : String(111);
address : String(1111);
currency : Currency;
}

}
31 changes: 31 additions & 0 deletions tests/dynamic-constraints/srv/etc/requisition.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

using { cuid } from '@sap/cds/common';
using { sap.ariba.catalog.Product, sap.ariba.catalog.Suppliers } from './catalog';

type Price : Decimal(10,2);

context sap.ariba.buying {

entity Requisitions : cuid {
buyer : String;
Items : Composition of many LineItems on Items.parent = $self;
totalPrice : Price;
}

entity LineItems {
key parent : Association to Requisitions;
key pos : Integer;
product : Association to Product;
supplier : Association to Suppliers; //
quantity : Integer;
// supplierCurrency : Currency;
};

// entity Product : sap.ariba.catalog.Product {
// product : Association to Product;
// }
entity Suppliers : sap.ariba.catalog.Suppliers {
product : Association to Product;
}

}
16 changes: 16 additions & 0 deletions tests/dynamic-constraints/srv/etc/some-body-else.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using { sap.ariba.buying } from './requisition';
context other {

aspect managed {
createdBy: User @assert: (createdBy is not null ? null : 'is missing');
createdAt: DateTime;
lastModifiedBy: User;
lastModifiedAt: DateTime;
}

type User : String;

extend buying.Requisitions with managed;
extend buying.Suppliers with managed;

}
36 changes: 36 additions & 0 deletions tests/dynamic-constraints/srv/etc/validation-aspects.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using { sap.capire.bookshop.Books } from '@capire/bookshop';


/**
* Validation constraints for Books
*/
@validations aspect AdminService.Books.constraints.aspect : Books {

// two-step mandatory check
check_title = case
when title is null then 'is missing'
when trim(title)='' then 'must not be empty'
end;

check_title2 = (
title is null ? 'is missing' :
trim(title)='' ? 'must not be empty' : null
);

// range check
check_stock = stock < 0 ? 'must not be negative' : null;

// range check
check_price = price < 0 ? 'must not be negative' : null;

// assert target check
// check_genre = genre is not null and not exists genre ? 'does not exist' : null;

// multiple constraints: mandatory + assert target + special
check_author = case
when author.ID is null then 'is missing' // FIXME: 1) // TODO: 2)
// when not exists author then 'Author does not exist: ' || author.ID
when count(author.books.ID) -1 > 1 then author.name || ' already wrote too many books' // TODO: 3)
when /* exists */ author.books[genre.name like '%Noire%'] then 'Author has written a Noire book'
end
}
72 changes: 72 additions & 0 deletions tests/dynamic-constraints/srv/etc/validation-asserts.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using { sap.capire.bookshop.Books } from '@capire/bookshop';

// @mandatory
// @readonly
// @hidden @visible @inapplicable

// @assert.range
// @assert.format
// @assert.target

// Following are invariant constraints declared on domain model entity
annotate Books with {

// manual two-step mandatory constraint
// title @assert.constraint: {
// not_null: { condition: (title is not null), message: 'is missing' },
// not_empty: { condition: (trim(title) != ''), message: 'must not be empty' },
// };

// manual two-step mandatory constraint
title @assert: (case
when title is null then 'is missing'
when trim(title)='' then 'must not be empty'
end);

// range check
stock @assert: (case
when stock <= 0 then 'must not a positive number'
end);

// range check
price @assert: (case
// when price is not null and not price between 0 and 500 then 'must be between 0 and 500'
when price <= 0 or price > 500 then 'must be between 0 and 500'
end);

// assert target check
// genre @assert: (case
// when genre is not null and not exists genre then 'does not exist'
// end);

genre @assert: (case
when genre is null then null // genre may be null
when not exists genre then 'does not exist'
end);

// multiple constraints: mandatory + assert target, ...
author @assert: (case
when author is null then 'is missing'
when not exists author then 'does not exist'
end);
}


// Following need to go on service-level entity, as rewriting would fail for CatalogService
annotate AdminService.Books with {

// ... + special
author @assert: (case
when sum(author.books.price) > 111 then author.name || ' already earned too much with their books'
when count(author.books.ID) -1 > 1 then author.name || ' already wrote too many books'
// FIXME: ^^^^^^^^^^^^^^^^ cqn4sql doesn't support count(author.books) yet
end);

price @mandatory: (exists author.books.genre[name = 'Drama']);

price @assert: (case
when price is null and exists author.books.genre[name = 'Drama']
then 'Price must be specified for books by drama queens'
end);

}
80 changes: 80 additions & 0 deletions tests/dynamic-constraints/srv/etc/validation-views.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using { AdminService, sap.capire.bookshop as my } from '../admin-service';

// entity Books.drafts as projection on AdminService.Books;
// @cds.api.ignore view Books.drafts.constraints as select from AdminService.Books.drafts mixin {
// before: Association to my.Books on before.ID = $self.ID;
// base: Association to my.Books on base.ID = $self.ID;
// } into { ID, // FIXME: compiler should resolve Books without AdminService prefix
// case
// when title is null then 'is missing'
// when trim(title)='' then 'must not be empty'
// end as title,
// ...
// }

/**
* Validation constraints for Books
*/
@validation view AdminService.Books.constraints as select from AdminService.Books mixin {
base: Association to my.Books on base.ID = $self.ID // Should be provided by CAP ootb
} into {
ID,

// two-step mandatory check
case
when title is null then 'is missing'
when trim(title)='' then 'must not be empty'
end as title,
// the above is equivalent to:
// title is null ? 'is missing' : trim(title)='' ? 'must not be empty' :

// range check
stock < 0 ? 'must not be negative' :
null as stock,

// range check
price < 0 ? 'must not be negative' :
null as price,

// assert target check
genre.ID is not null and not exists genre ? 'does not exist' :
null as genre,

genre.name as _genre,

// multiple constraints: mandatory + assert target + special
case
when author.ID is null then 'is missing' // FIXME: 1) // TODO: 2)
when not exists author then 'Author does not exist: ' || author.ID
when sum(base.author.books.price) > 111 then author.name || ' already earned too much' // TODO: 3)
end as author,

} group by ID; // because of the count(base.author.books) above

// 1) FIXME: expected author.ID to refer to foreign key,
// apparently that is not the case -> move one line up
// and run test to see the erroneous impact.

// 2) TODO: we should allow to write author is null instead of author.ID is null

// 3) TODO: we should support count(author.books)


/**
* Validation constraints for Authors
*/
@validation view AdminService.Authors.constraints as select from AdminService.Authors { ID, // FIXME: compiler should resolve Authors without AdminService prefix

// two-step mandatory check
name = null ? 'is missing' : trim(name)='' ? 'must not be empty' :
null as name,

// constraint related to two fields
dateOfDeath < dateOfBirth ? 'we can''t die before we are born' : null as _born_before_death, // reuse condition
$self._born_before_death as dateOfBirth,
$self._born_before_death as dateOfDeath,

}


annotate AdminService.Books.constraints with @cds.api.ignore @odata.draft.enabled: false;
11 changes: 11 additions & 0 deletions tests/dynamic-constraints/srv/field-control.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using { AdminService } from './admin-service';

@fieldcontrol view AdminService.Books.field.control as select from AdminService.Books { ID,
genre.name == 'Drama' ? 'readonly' :
null as price
}

// Make that available to Fiori clients
extend AdminService.Books with columns {
fc : Association to AdminService.Books.field.control on fc.ID = $self.ID
}
1 change: 1 addition & 0 deletions tests/dynamic-constraints/srv/validation.cds
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
using from './etc/validation-asserts';
Loading