From bd5a57189d0f09472d011ba96c5d74d66418852b Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 13:23:35 +0200 Subject: [PATCH 01/11] feat: PoC for dynamic constraints using standard cds views --- bookshop/server.js | 26 +++++++++++++++++ bookshop/srv/x-field-control.cds | 10 +++++++ bookshop/srv/x-validation.cds | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 bookshop/server.js create mode 100644 bookshop/srv/x-field-control.cds create mode 100644 bookshop/srv/x-validation.cds diff --git a/bookshop/server.js b/bookshop/server.js new file mode 100644 index 00000000..46467b15 --- /dev/null +++ b/bookshop/server.js @@ -0,0 +1,26 @@ +// +// Quick and dirty implementation for cds.validate() using db-level constraints +// + +const cds = require('@sap/cds') +cds.on('served', ()=> { + + const $ = cds.validate; cds.validate = async function (entity, key, ...columns) { + if (!entity.is_entity) return $(...arguments) + if (entity.constraints) entity = entity.constraints + if (key?.results) key = key.results[0].lastInsertRowid + const checks = await SELECT.one.from (entity, key, columns.length && columns) + const failed = {}; for (let c in checks) { + if (c in entity.keys) continue + if (c[0] == '_') continue + if (checks[c]) failed[c] = checks[c] + } + if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` + } + +}) + +Object.defineProperties (cds.entity.prototype, { + constraints: { get() { return cds.model.definitions[this.name+'.constraints'] }}, + fields: { get() { return cds.model.definitions[this.name+'.field.control'] }}, +}) diff --git a/bookshop/srv/x-field-control.cds b/bookshop/srv/x-field-control.cds new file mode 100644 index 00000000..9f96d3ad --- /dev/null +++ b/bookshop/srv/x-field-control.cds @@ -0,0 +1,10 @@ +namespace sap.capire.bookshop; +using from '../db/schema'; + +view Books.field.control as select from Books { ID, + genre.name == 'Drama' ? 'readonly' : + null as price +} +extend Books with { + fc : Association to Books.field.control on fc.ID = $self.ID +} diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds new file mode 100644 index 00000000..3ef7b696 --- /dev/null +++ b/bookshop/srv/x-validation.cds @@ -0,0 +1,50 @@ +namespace sap.capire.bookshop; +using from '../db/schema'; + + +/** + * Validation constraints for Books + */ +view Books.constraints as select from Books { ID, + + // two-step mandatory check + title = null ? 'is missing' : trim(title)='' ? 'must not be empty' : + null as title, + + // 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, + + // multiple constraints: mandatory + assert target + special + not exists author ? 'Author does not exist: ' || author.ID : + author.ID is null ? 'is missing' : // FIXME: expected author.ID to refer to foreign key, apparently that is not the case -> move one line up to see + count(author.books.ID) -1 > 1 ? author.name || ' already wrote multiple books, please choose another author' : // TODO: we should support count(author.books) + null as author, + +} group by ID; + + + +/** + * Validation constraints for Authors + */ +view Authors.constraints as select from Authors { ID, + + // two-step mandatory check + name = null ? 'is missing' : trim(name)='' ? 'must not be empty' : + null as name, + + // constraint related to two fields + dateOfDeath > dateOfBirth ? 'date of birth must be before date of death' : null as _born_before_death, + $self._born_before_death as dateOfBirth, + $self._born_before_death as dateOfDeath, + +} From b55cce3e63dd48a81f8964c1a5ea9ad356208c7e Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 13:49:45 +0200 Subject: [PATCH 02/11] case when equivalent --- bookshop/srv/x-validation.cds | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds index 3ef7b696..c5f67ce1 100644 --- a/bookshop/srv/x-validation.cds +++ b/bookshop/srv/x-validation.cds @@ -8,8 +8,12 @@ using from '../db/schema'; view Books.constraints as select from Books { ID, // two-step mandatory check - title = null ? 'is missing' : trim(title)='' ? 'must not be empty' : - null as title, + 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' : From 7cd3a6d1e327286f6c587469e93f0f829dd7c5b7 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 13:59:33 +0200 Subject: [PATCH 03/11] comments --- bookshop/srv/x-validation.cds | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds index c5f67ce1..3aeca373 100644 --- a/bookshop/srv/x-validation.cds +++ b/bookshop/srv/x-validation.cds @@ -29,12 +29,17 @@ view Books.constraints as select from Books { ID, // multiple constraints: mandatory + assert target + special not exists author ? 'Author does not exist: ' || author.ID : - author.ID is null ? 'is missing' : // FIXME: expected author.ID to refer to foreign key, apparently that is not the case -> move one line up to see - count(author.books.ID) -1 > 1 ? author.name || ' already wrote multiple books, please choose another author' : // TODO: we should support count(author.books) + author.ID is null ? 'is missing' : // FIXME: 1) + count(author.books.ID) -1 > 1 ? author.name || ' already wrote too many books' : // TODO: 2) null as author, } group by ID; +// 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 support count(author.books) /** From 4fc324c7fb09027200d8b7f48ed9f8e736e16196 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 14:08:25 +0200 Subject: [PATCH 04/11] Added instructions for ad-hoc tests in cds repl --- bookshop/server.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/bookshop/server.js b/bookshop/server.js index 46467b15..35c74e15 100644 --- a/bookshop/server.js +++ b/bookshop/server.js @@ -1,21 +1,28 @@ // // Quick and dirty implementation for cds.validate() using db-level constraints +// Test in cds.repl like that: +// await cds.run (()=> INSERT.into (Books, { title:' ', author_ID:150 }) .then (cds.validate(Books))) // const cds = require('@sap/cds') cds.on('served', ()=> { - const $ = cds.validate; cds.validate = async function (entity, key, ...columns) { + const $ = cds.validate; cds.validate = function (entity, key, ...columns) { + if (!entity.is_entity) return $(...arguments) + if (!key) return key => cds.validate(entity,key) + if (entity.constraints) entity = entity.constraints - if (key?.results) key = key.results[0].lastInsertRowid - const checks = await SELECT.one.from (entity, key, columns.length && columns) - const failed = {}; for (let c in checks) { - if (c in entity.keys) continue - if (c[0] == '_') continue - if (checks[c]) failed[c] = checks[c] - } - if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` + if (key.results) key = key.results[0].lastInsertRowid + + return SELECT.one.from (entity, key, columns.length && columns) .then (checks => { + const failed = {}; for (let c in checks) { + if (c in entity.keys) continue + if (c[0] == '_') continue + if (checks[c]) failed[c] = checks[c] + } + if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` + }) } }) From 04d659c9c24309fc674a297815786a4b6952c77e Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 17:25:32 +0200 Subject: [PATCH 05/11] more comments --- bookshop/srv/x-validation.cds | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds index 3aeca373..8326afd3 100644 --- a/bookshop/srv/x-validation.cds +++ b/bookshop/srv/x-validation.cds @@ -28,9 +28,9 @@ view Books.constraints as select from Books { ID, null as genre, // multiple constraints: mandatory + assert target + special + author.ID is null ? 'is missing' : // FIXME: 1) // TODO: 2) not exists author ? 'Author does not exist: ' || author.ID : - author.ID is null ? 'is missing' : // FIXME: 1) - count(author.books.ID) -1 > 1 ? author.name || ' already wrote too many books' : // TODO: 2) + count(author.books.ID) -1 > 1 ? author.name || ' already wrote too many books' : // TODO: 3) null as author, } group by ID; @@ -39,7 +39,9 @@ view Books.constraints as select from Books { ID, // apparently that is not the case -> move one line up // and run test to see the erroneous impact. -// 2) TODO: we should support count(author.books) +// 2) TODO: we should allow to write author is null instead of author.ID is null + +// 3) TODO: we should support count(author.books) /** From 15fa7de880e0cb907ac02d94c058c8b21a9be6a8 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 16 Jul 2025 17:27:07 +0200 Subject: [PATCH 06/11] . --- bookshop/srv/x-validation.cds | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds index 8326afd3..b92d32a0 100644 --- a/bookshop/srv/x-validation.cds +++ b/bookshop/srv/x-validation.cds @@ -54,7 +54,7 @@ view Authors.constraints as select from Authors { ID, null as name, // constraint related to two fields - dateOfDeath > dateOfBirth ? 'date of birth must be before date of death' : null as _born_before_death, + dateOfDeath > dateOfBirth ? 'we must be born before we die' : null as _born_before_death, $self._born_before_death as dateOfBirth, $self._born_before_death as dateOfDeath, From d57c29e1267e529e54486890b4de58963cb68242 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Wed, 23 Jul 2025 17:36:58 +0200 Subject: [PATCH 07/11] constraints on service level --- bookshop/server.js | 22 +++++- bookshop/srv/access-control.cds | 2 +- bookshop/srv/admin-service.cds | 2 +- bookshop/srv/x-validation.cds | 135 ++++++++++++++++++-------------- 4 files changed, 95 insertions(+), 66 deletions(-) diff --git a/bookshop/server.js b/bookshop/server.js index 35c74e15..819c55b7 100644 --- a/bookshop/server.js +++ b/bookshop/server.js @@ -1,19 +1,31 @@ // // Quick and dirty implementation for cds.validate() using db-level constraints // Test in cds.repl like that: +// {Books} = AdminService.entities // await cds.run (()=> INSERT.into (Books, { title:' ', author_ID:150 }) .then (cds.validate(Books))) +// await AdminService.create ('Books', { title:' ', author_ID:150 }) // const cds = require('@sap/cds') cds.on('served', ()=> { - const $ = cds.validate; cds.validate = function (entity, key, ...columns) { + const $ = cds.validate + cds.validate = function (entity, key, ...columns) { - if (!entity.is_entity) return $(...arguments) - if (!key) return key => cds.validate(entity,key) + if (entity?.ref) entity = { // quick and dirty + name: entity.ref[0], + constraints: { // even quicker and dirtier + name: entity.ref[0] +'.constraints', + keys: {ID:1} + } + } + else if (!entity.is_entity) return // we skip all standard validations for the experiments + else if (!entity.is_entity) return $(...arguments) // eslint-disable-line no-dupe-else-if if (entity.constraints) entity = entity.constraints - if (key.results) key = key.results[0].lastInsertRowid + if (!key) return key => cds.validate(entity,key) + if (key.results) key = key.results[0].lastInsertRowid // quick and dirty + if (key.ID) key = key.ID // quick and dirty return SELECT.one.from (entity, key, columns.length && columns) .then (checks => { const failed = {}; for (let c in checks) { @@ -25,6 +37,8 @@ cds.on('served', ()=> { }) } + const { AdminService } = cds.services + AdminService.after (['CREATE','UPDATE'], (result,req) => cds.validate (req.subject, result)) }) Object.defineProperties (cds.entity.prototype, { diff --git a/bookshop/srv/access-control.cds b/bookshop/srv/access-control.cds index 80384f6e..1cd1ac3c 100644 --- a/bookshop/srv/access-control.cds +++ b/bookshop/srv/access-control.cds @@ -1,2 +1,2 @@ using { AdminService } from './admin-service'; -annotate AdminService with @requires:'admin'; +annotate AdminService with @requires: false; //'admin'; diff --git a/bookshop/srv/admin-service.cds b/bookshop/srv/admin-service.cds index 37b02eb7..855e73d2 100644 --- a/bookshop/srv/admin-service.cds +++ b/bookshop/srv/admin-service.cds @@ -1,6 +1,6 @@ using { sap.capire.bookshop as my } from '../db/schema'; service AdminService @(path:'/admin') { - entity Authors as projection on my.Authors; + entity Authors as projection on my.Authors excluding { books}; entity Books as projection on my.Books; entity Genres as projection on my.Genres; } diff --git a/bookshop/srv/x-validation.cds b/bookshop/srv/x-validation.cds index b92d32a0..a845caf7 100644 --- a/bookshop/srv/x-validation.cds +++ b/bookshop/srv/x-validation.cds @@ -1,61 +1,76 @@ -namespace sap.capire.bookshop; -using from '../db/schema'; - - -/** - * Validation constraints for Books - */ -view Books.constraints as select from Books { 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, - - // multiple constraints: mandatory + assert target + special - author.ID is null ? 'is missing' : // FIXME: 1) // TODO: 2) - not exists author ? 'Author does not exist: ' || author.ID : - count(author.books.ID) -1 > 1 ? author.name || ' already wrote too many books' : // TODO: 3) - null as author, - -} group by ID; - -// 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 - */ -view Authors.constraints as select from Authors { ID, - - // 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 must be born before we die' : null as _born_before_death, - $self._born_before_death as dateOfBirth, - $self._born_before_death as dateOfDeath, - +using { AdminService, sap.capire.bookshop as my } from './admin-service'; + +extend service AdminService with { + + // 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 + */ + @cds.api.ignore view Books.constraints as select from AdminService.Books mixin { + base: Association to my.Books on base.ID = $self.ID; + } into { ID, // FIXME: compiler should resolve Books without AdminService prefix + + // 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, + + // multiple constraints: mandatory + assert target + special + author.ID is null ? 'is missing' : // FIXME: 1) // TODO: 2) + not exists author ? 'Author does not exist: ' || author.ID : + count(base.author.books.ID) -1 > 1 ? author.name || ' already wrote too many books' : // TODO: 3) + null as author, + + } group by ID; + + // 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 + */ + view 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 must be born before we die' : null as _born_before_death, + $self._born_before_death as dateOfBirth, + $self._born_before_death as dateOfDeath, + + } } From 65eccfad33551db246b60b0eb757aeb0d0a519be Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 24 Jul 2025 10:38:43 +0200 Subject: [PATCH 08/11] Moved dynamic constraints in a dedicated folder --- bookshop/srv/access-control.cds | 2 +- bookshop/srv/admin-service.cds | 2 +- bookshop/test/dynamic-constraints/readme.md | 26 +++++++++++++++++++ .../{ => test/dynamic-constraints}/server.js | 7 ++--- .../dynamic-constraints/srv/admin-service.cds | 5 ++++ .../srv/field-control.cds} | 2 +- .../dynamic-constraints/srv/validation.cds} | 0 7 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 bookshop/test/dynamic-constraints/readme.md rename bookshop/{ => test/dynamic-constraints}/server.js (82%) create mode 100644 bookshop/test/dynamic-constraints/srv/admin-service.cds rename bookshop/{srv/x-field-control.cds => test/dynamic-constraints/srv/field-control.cds} (88%) rename bookshop/{srv/x-validation.cds => test/dynamic-constraints/srv/validation.cds} (100%) diff --git a/bookshop/srv/access-control.cds b/bookshop/srv/access-control.cds index 1cd1ac3c..80384f6e 100644 --- a/bookshop/srv/access-control.cds +++ b/bookshop/srv/access-control.cds @@ -1,2 +1,2 @@ using { AdminService } from './admin-service'; -annotate AdminService with @requires: false; //'admin'; +annotate AdminService with @requires:'admin'; diff --git a/bookshop/srv/admin-service.cds b/bookshop/srv/admin-service.cds index 855e73d2..37b02eb7 100644 --- a/bookshop/srv/admin-service.cds +++ b/bookshop/srv/admin-service.cds @@ -1,6 +1,6 @@ using { sap.capire.bookshop as my } from '../db/schema'; service AdminService @(path:'/admin') { - entity Authors as projection on my.Authors excluding { books}; + entity Authors as projection on my.Authors; entity Books as projection on my.Books; entity Genres as projection on my.Genres; } diff --git a/bookshop/test/dynamic-constraints/readme.md b/bookshop/test/dynamic-constraints/readme.md new file mode 100644 index 00000000..0ffb974d --- /dev/null +++ b/bookshop/test/dynamic-constraints/readme.md @@ -0,0 +1,26 @@ +## 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/sap-samples/cloud-cap-samples) like so: + +```sh +git clone -q https://github.com/sap-samples/cloud-cap-samples cap/samples +cd cap/samples +npm install +``` + +### Testing + +Test like that in `cds.repl` from _cap/samples_ root: + +```sh +cds repl --run bookshop/test/dynamic-constraints +```` + +```javascript +await AdminService.create ('Books', { title:' ', author_ID:150 }) +``` diff --git a/bookshop/server.js b/bookshop/test/dynamic-constraints/server.js similarity index 82% rename from bookshop/server.js rename to bookshop/test/dynamic-constraints/server.js index 819c55b7..d7d62fbb 100644 --- a/bookshop/server.js +++ b/bookshop/test/dynamic-constraints/server.js @@ -1,9 +1,6 @@ // -// Quick and dirty implementation for cds.validate() using db-level constraints -// Test in cds.repl like that: -// {Books} = AdminService.entities -// await cds.run (()=> INSERT.into (Books, { title:' ', author_ID:150 }) .then (cds.validate(Books))) -// await AdminService.create ('Books', { title:' ', author_ID:150 }) +// Quick and dirty implementation for cds.validate() +// using db-level constraints. // const cds = require('@sap/cds') diff --git a/bookshop/test/dynamic-constraints/srv/admin-service.cds b/bookshop/test/dynamic-constraints/srv/admin-service.cds new file mode 100644 index 00000000..373501e5 --- /dev/null +++ b/bookshop/test/dynamic-constraints/srv/admin-service.cds @@ -0,0 +1,5 @@ +using { AdminService } from '../../../srv/admin-service'; +annotate AdminService with @requires: false; +extend AdminService.Authors with columns { + null as books // to simulate the exclusion of books +} diff --git a/bookshop/srv/x-field-control.cds b/bookshop/test/dynamic-constraints/srv/field-control.cds similarity index 88% rename from bookshop/srv/x-field-control.cds rename to bookshop/test/dynamic-constraints/srv/field-control.cds index 9f96d3ad..3b48b7a3 100644 --- a/bookshop/srv/x-field-control.cds +++ b/bookshop/test/dynamic-constraints/srv/field-control.cds @@ -1,5 +1,5 @@ namespace sap.capire.bookshop; -using from '../db/schema'; +using from './admin-service'; view Books.field.control as select from Books { ID, genre.name == 'Drama' ? 'readonly' : diff --git a/bookshop/srv/x-validation.cds b/bookshop/test/dynamic-constraints/srv/validation.cds similarity index 100% rename from bookshop/srv/x-validation.cds rename to bookshop/test/dynamic-constraints/srv/validation.cds From 6dc1da2964325100793fff6d9b9c02a95be32e9a Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 24 Jul 2025 12:24:11 +0200 Subject: [PATCH 09/11] Reverting removal of cds.service.providers --- inspectr/srv/data-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inspectr/srv/data-service.js b/inspectr/srv/data-service.js index e0a5c776..a9bfc355 100644 --- a/inspectr/srv/data-service.js +++ b/inspectr/srv/data-service.js @@ -52,7 +52,7 @@ module.exports = { DataService } /** @returns {cds.Service} */ function findDataSource(dataSourceName, entityName) { - for (let srv of Object.values(cds.services)) { // all connected services + for (let srv of cds.service.providers) { // all connected services if (!srv.name) continue // FIXME intermediate/pending in cds.services ? if (dataSourceName === srv.name || entityName.startsWith(srv.name+'.')) { log._debug && log.debug(`using ${srv.name} as data source`) From 991e890ba976d51426f34509bb3f27073a4a328f Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 24 Jul 2025 13:31:45 +0200 Subject: [PATCH 10/11] cleanup impls --- bookshop/test/dynamic-constraints/server.js | 33 ++----------------- .../dynamic-constraints/srv/validation.cds | 2 +- bookshop/test/dynamic-constraints/validate.js | 33 +++++++++++++++++++ 3 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 bookshop/test/dynamic-constraints/validate.js diff --git a/bookshop/test/dynamic-constraints/server.js b/bookshop/test/dynamic-constraints/server.js index d7d62fbb..9c230857 100644 --- a/bookshop/test/dynamic-constraints/server.js +++ b/bookshop/test/dynamic-constraints/server.js @@ -3,41 +3,14 @@ // using db-level constraints. // -const cds = require('@sap/cds') +const cds = require('@sap/cds'); require('./validate.js') cds.on('served', ()=> { - - const $ = cds.validate - cds.validate = function (entity, key, ...columns) { - - if (entity?.ref) entity = { // quick and dirty - name: entity.ref[0], - constraints: { // even quicker and dirtier - name: entity.ref[0] +'.constraints', - keys: {ID:1} - } - } - else if (!entity.is_entity) return // we skip all standard validations for the experiments - else if (!entity.is_entity) return $(...arguments) // eslint-disable-line no-dupe-else-if - - if (entity.constraints) entity = entity.constraints - if (!key) return key => cds.validate(entity,key) - if (key.results) key = key.results[0].lastInsertRowid // quick and dirty - if (key.ID) key = key.ID // quick and dirty - - return SELECT.one.from (entity, key, columns.length && columns) .then (checks => { - const failed = {}; for (let c in checks) { - if (c in entity.keys) continue - if (c[0] == '_') continue - if (checks[c]) failed[c] = checks[c] - } - if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` - }) - } - const { AdminService } = cds.services AdminService.after (['CREATE','UPDATE'], (result,req) => cds.validate (req.subject, result)) }) + + Object.defineProperties (cds.entity.prototype, { constraints: { get() { return cds.model.definitions[this.name+'.constraints'] }}, fields: { get() { return cds.model.definitions[this.name+'.field.control'] }}, diff --git a/bookshop/test/dynamic-constraints/srv/validation.cds b/bookshop/test/dynamic-constraints/srv/validation.cds index a845caf7..4d733947 100644 --- a/bookshop/test/dynamic-constraints/srv/validation.cds +++ b/bookshop/test/dynamic-constraints/srv/validation.cds @@ -68,7 +68,7 @@ extend service AdminService with { null as name, // constraint related to two fields - dateOfDeath > dateOfBirth ? 'we must be born before we die' : null as _born_before_death, + dateOfDeath < dateOfBirth ? 'we can''t die before we are born' : null as _born_before_death, $self._born_before_death as dateOfBirth, $self._born_before_death as dateOfDeath, diff --git a/bookshop/test/dynamic-constraints/validate.js b/bookshop/test/dynamic-constraints/validate.js new file mode 100644 index 00000000..8a93f111 --- /dev/null +++ b/bookshop/test/dynamic-constraints/validate.js @@ -0,0 +1,33 @@ +const cds = require('@sap/cds') +const $super = { validate: cds.validate, skip(){} } + + +/** + * Quick and dirty implementation for cds.validate() using db-level constraints. + */ +cds.validate = function (x, pk, ...columns) { + + // Delegate to base impl of cds.validate() for standard input validation + if (!_is_constraints(x)) return $super.skip (...arguments) + + // Support subject refs to base entities as arguments + if (x?.ref) [ x, pk ] = [ x.ref +'.constraints', pk.ID||pk ] + + // Run the constraints check query + const constraints = cds.model.definitions[x] || cds.error `No such constraints view: ${x}` + return SELECT.one.from (constraints, pk, columns.length && columns) + + // Collect and throw errors, if any + .then (checks => { + const failed = {}; for (let c in checks) { + if (c in constraints.keys) continue + if (c[0] == '_') continue + if (checks[c]) failed[c] = checks[c] + } + if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` + }) +} + + +// Helpers +const _is_constraints = x => x.ref || x.is_entity || typeof x === 'string' From 4e76d71defeccf1159b9e859db9cb705b4c1aae3 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 24 Jul 2025 13:52:01 +0200 Subject: [PATCH 11/11] polishing --- bookshop/test/dynamic-constraints/readme.md | 4 ++++ bookshop/test/dynamic-constraints/srv/admin-service.cds | 2 ++ bookshop/test/dynamic-constraints/validate.js | 9 +++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/bookshop/test/dynamic-constraints/readme.md b/bookshop/test/dynamic-constraints/readme.md index 0ffb974d..6660d649 100644 --- a/bookshop/test/dynamic-constraints/readme.md +++ b/bookshop/test/dynamic-constraints/readme.md @@ -22,5 +22,9 @@ cds repl --run bookshop/test/dynamic-constraints ```` ```javascript +await AdminService.create ('Books', {}) await AdminService.create ('Books', { title:' ', author_ID:150 }) +await AdminService.create ('Books', { title:'x' }) +await cds.validate (Books.constraints, 201) +await cds.validate (Books.constraints) ``` diff --git a/bookshop/test/dynamic-constraints/srv/admin-service.cds b/bookshop/test/dynamic-constraints/srv/admin-service.cds index 373501e5..fd771d06 100644 --- a/bookshop/test/dynamic-constraints/srv/admin-service.cds +++ b/bookshop/test/dynamic-constraints/srv/admin-service.cds @@ -1,3 +1,5 @@ +namespace AdminService; //> for cds.entities + using { AdminService } from '../../../srv/admin-service'; annotate AdminService with @requires: false; extend AdminService.Authors with columns { diff --git a/bookshop/test/dynamic-constraints/validate.js b/bookshop/test/dynamic-constraints/validate.js index 8a93f111..22907005 100644 --- a/bookshop/test/dynamic-constraints/validate.js +++ b/bookshop/test/dynamic-constraints/validate.js @@ -14,18 +14,19 @@ cds.validate = function (x, pk, ...columns) { if (x?.ref) [ x, pk ] = [ x.ref +'.constraints', pk.ID||pk ] // Run the constraints check query - const constraints = cds.model.definitions[x] || cds.error `No such constraints view: ${x}` - return SELECT.one.from (constraints, pk, columns.length && columns) + const constraints = typeof x === 'string' ? cds.model.definitions[x] || cds.error `No such constraints view: ${x}` : x + return SELECT.from (constraints, pk, columns.length && columns) // Collect and throw errors, if any - .then (checks => { + .then (rows => (rows.map ? rows : [rows]).map (checks => { const failed = {}; for (let c in checks) { if (c in constraints.keys) continue if (c[0] == '_') continue if (checks[c]) failed[c] = checks[c] } if (Object.keys(failed).length) throw cds.error `Invalid input: ${failed}` - }) + return checks + })) }