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'; +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 Books - */ -view Books.constraints as select from Books { ID, + /** + * 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 - 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' : + // two-step mandatory check + name = null ? 'is missing' : trim(name)='' ? 'must not be empty' : + null as name, - // 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, + // 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, + } }