diff --git a/.eslintrc b/.eslintrc index 93f9297b..7f083921 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,6 +17,7 @@ "globals": { "SELECT": true, "INSERT": true, + "UPSERT": true, "UPDATE": true, "DELETE": true, "CREATE": true, diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..58fede7a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: This channel is CLOSED. + about: Use SAP community instead + url: https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce diff --git a/.github/ISSUE_TEMPLATE/question--feedback-or-bug-.md b/.github/ISSUE_TEMPLATE/question--feedback-or-bug-.md deleted file mode 100644 index f4db679b..00000000 --- a/.github/ISSUE_TEMPLATE/question--feedback-or-bug-.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: This channel is CLOSED. -about: Use our community at https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce -title: '' -labels: '' -assignees: '' - ---- - -Please use our community on https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce diff --git a/bookstore/srv/mashup.cds b/bookstore/srv/mashup.cds index 061418c0..7344ff1a 100644 --- a/bookstore/srv/mashup.cds +++ b/bookstore/srv/mashup.cds @@ -12,7 +12,11 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop'; using { ReviewsService.Reviews } from '@capire/reviews'; extend Books with { reviews : Composition of many Reviews on reviews.subject = $self.ID; + + @Common.Label : '{i18n>Rating}' rating : Decimal; + + @Common.Label : '{i18n>NumberOfReviews}' numberOfReviews : Integer; } diff --git a/fiori/app/_i18n/i18n.properties b/fiori/app/_i18n/i18n.properties index 83681bcf..c6e0d27d 100644 --- a/fiori/app/_i18n/i18n.properties +++ b/fiori/app/_i18n/i18n.properties @@ -6,16 +6,33 @@ Author = Author AuthorID = Author ID Stock = Stock Name = Name +Description = Description +Image = Image AuthorName = Author's Name DateOfBirth = Date of Birth DateOfDeath = Date of Death PlaceOfBirth = Place of Birth PlaceOfDeath = Place of Death Age = Age +Lifetime = Lifetime Authors = Authors + Order = Order Orders = Orders +OrderNo = Order Number +OrderItems = Order Items +Customer = Customer +Product = Product +ProductID = Product ID +ProductTitle = Product Title +UnitPrice = Unit Price +Quantity = Quantity + Price = Price +Currency = Currency +Date = Date +Rating = Rating +NumberOfReviews = Number of Reviews Genre = Genre Genres = Genres diff --git a/fiori/app/admin-authors/fiori-service.cds b/fiori/app/admin-authors/fiori-service.cds index 2309eae6..9ca69224 100644 --- a/fiori/app/admin-authors/fiori-service.cds +++ b/fiori/app/admin-authors/fiori-service.cds @@ -43,5 +43,10 @@ extend sap.capire.bookshop.Authors with { virtual lifetime : String; } +annotate AdminService.Authors with { + age @Common.Label : '{i18n>Age}'; + lifetime @Common.Label : '{i18n>Lifetime}' +} + // Workaround for Fiori popup for asking user to enter a new UUID on Create annotate AdminService.Authors with { ID @Core.Computed; } diff --git a/fiori/app/admin-books/fiori-service.cds b/fiori/app/admin-books/fiori-service.cds index d7b1cd57..668e2548 100644 --- a/fiori/app/admin-books/fiori-service.cds +++ b/fiori/app/admin-books/fiori-service.cds @@ -62,6 +62,11 @@ annotate AdminService.Books.texts with @( } ); +annotate AdminService.Books.texts with { + ID @UI.Hidden; + ID_texts @UI.Hidden; +}; + // Add Value Help for Locales annotate AdminService.Books.texts { locale @( diff --git a/fiori/app/browse/fiori-service.cds b/fiori/app/browse/fiori-service.cds index def17d31..03ff198c 100644 --- a/fiori/app/browse/fiori-service.cds +++ b/fiori/app/browse/fiori-service.cds @@ -52,9 +52,6 @@ annotate CatalogService.Books with @(UI : { }, {Value : genre.name}, {Value : price}, - { - Value : currency.symbol, - Label : ' ' - }, + {Value : currency.symbol}, ] }, ); diff --git a/fiori/app/common.cds b/fiori/app/common.cds index 1f396d8d..3e5f748a 100644 --- a/fiori/app/common.cds +++ b/fiori/app/common.cds @@ -4,6 +4,7 @@ using { sap.capire.bookshop as my } from '@capire/bookstore'; using { sap.common } from '@capire/common'; +using { sap.common.Currencies } from '@sap/cds/common'; //////////////////////////////////////////////////////////////////////////// // @@ -25,7 +26,7 @@ annotate my.Books with @( { Value: genre.name }, { Value: stock }, { Value: price }, - { Value: currency.symbol, Label: ' ' }, + { Value: currency.symbol }, ] } ) { @@ -37,6 +38,10 @@ annotate my.Books with @( author @ValueList.entity : 'Authors'; }; +annotate Currencies with { + symbol @Common.Label : '{i18n>Currency}'; +} + //////////////////////////////////////////////////////////////////////////// // // Books Details @@ -60,7 +65,8 @@ annotate my.Books with { author @title: '{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly }; price @title: '{i18n>Price}' @Measures.ISOCurrency : currency_code; stock @title: '{i18n>Stock}'; - descr @UI.MultiLineText; + descr @title: '{i18n>Description}' @UI.MultiLineText; + image @title: '{i18n>Image}'; } //////////////////////////////////////////////////////////////////////////// @@ -81,6 +87,10 @@ annotate my.Genres with @( } ); +annotate my.Genres with { + ID @Common.Text : name @Common.TextArrangement : #TextOnly; +} + //////////////////////////////////////////////////////////////////////////// // // Genre Details diff --git a/hello/srv/world.js b/hello/srv/world.js index ff1a370e..c5cd2495 100644 --- a/hello/srv/world.js +++ b/hello/srv/world.js @@ -1,3 +1,7 @@ module.exports = class say { - hello(req) { return `Hello ${req.data.to}!` } + hello(req) { + let {to} = req.data + if (to === 'me') to = require('os').userInfo().username + return `Hello ${to}!` + } } diff --git a/loggers/app/loggers.html b/loggers/app/loggers.html new file mode 100644 index 00000000..2d588e7b --- /dev/null +++ b/loggers/app/loggers.html @@ -0,0 +1,76 @@ + + + + + cds.log + + + + + + + +
+

Log Levels

+ + + + + + + + + + +
Module ID Log Level
{{ each.id }} +
+

Log Format:

+ [ + | + | + | + | + ] - log message ... +
+ + + + + diff --git a/loggers/package.json b/loggers/package.json new file mode 100644 index 00000000..4f76f7f2 --- /dev/null +++ b/loggers/package.json @@ -0,0 +1,22 @@ +{ + "name": "@capire/loggers", + "version": "1.0.0", + "description": "Simple sample on how to dynamically set cds.log levels and formats.", + "files": [ + "app", + "srv" + ], + "dependencies": { + "@sap/cds": ">=5.9", + "express": "^4.17.1" + }, + "scripts": { + "start": "cds run", + "watch": "cds watch" + }, + "cds": { + "requires": { + "db": "sql" + } + } +} diff --git a/loggers/readme.md b/loggers/readme.md new file mode 100644 index 00000000..c030f889 --- /dev/null +++ b/loggers/readme.md @@ -0,0 +1,11 @@ +# Dynamically Set `cds.log` Levels and Formats + +### Run + +```sh +cds watch +``` + +### Test + +Either using the UI through http://localhost:4004/loggers.html, or try the requests in `test/requests.http` diff --git a/loggers/srv/dummy.cds b/loggers/srv/dummy.cds new file mode 100644 index 00000000..7ed8e09d --- /dev/null +++ b/loggers/srv/dummy.cds @@ -0,0 +1,3 @@ +service Sue { + entity Dummy { key ID: UUID; title: String; } +} diff --git a/loggers/srv/loggers.cds b/loggers/srv/loggers.cds new file mode 100644 index 00000000..280ca33b --- /dev/null +++ b/loggers/srv/loggers.cds @@ -0,0 +1,20 @@ +@rest service LogService { + + @readonly entity Loggers : Logger {}; + entity Logger { + key id : String; + level : String; + } + + action format ( + timestamp : Boolean, + level : Boolean, + tenant : Boolean, + reqid : Boolean, + id : Boolean, + ); + + action debug (logger : String) returns Logger; + action reset (logger : String) returns Logger; + +} diff --git a/loggers/srv/loggers.js b/loggers/srv/loggers.js new file mode 100644 index 00000000..5fcd7226 --- /dev/null +++ b/loggers/srv/loggers.js @@ -0,0 +1,56 @@ +const cds = require ('@sap/cds/lib') +const LOG = cds.log('cds.log') + +module.exports = class LogService extends cds.Service { + init(){ + + this.on('GET','Loggers', (req)=>{ + let loggers = Object.values(cds.log.loggers).map (_logger) + let {$search} = req._.req.query + if ($search) { + const re = RegExp($search,'i') + loggers = loggers.filter (l => re.test(l.id) || re.test(l.level)) + } + return loggers.sort ((a,b) => a.id < b.id ? -1 : 1) + }) + + this.on('PUT','Logger', (req)=>{ + const {id} = req.params[0] || req.data + if (!id) return req.reject('No logger id specified in request') + return _logger (cds.log (id, req.data)) + }) + + this.on('debug', (req)=>{ + const {logger:id} = req.params[0] || req.data + if (!id) return req.reject('No logger id specified in request') + return _logger (cds.log (id, {level:'debug'})) + }) + + this.on('reset', (req)=>{ + const {logger:id} = req.params[0] || req.data + if (!id) return req.reject('No logger id specified in request') + return _logger (cds.log (id, {level:'info'})) + }) + + this.on('format', (req)=>{ + const $ = req.data; LOG.info('format:',$) + // Set format for new loggers constructed subsequently + cds.log.format = (id, level, ...args) => { + const fmt = [] + if ($.timestamp) fmt.push ('|', (new Date).toISOString()) + if ($.level) fmt.push ('|', _levels[level].padEnd(5)) + if ($.tenant) fmt.push ('|', cds.context && cds.context.tenant) + if ($.reqid) fmt.push ('|', cds.context && cds.context.id) + if ($.id) fmt.push ('|', id) + fmt[0] = '[', fmt.push ('] -', ...args) + return fmt + } + // Apply this format to all existing loggers + Object.values(cds.log.loggers).forEach (l => l.setFormat (cds.log.format)) + }) + } + +} + +const _logger = ({id,level}) => ({id, level:_levels[level] }) +const _levels = [ 'SILENT', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE' ] diff --git a/loggers/test/requests.http b/loggers/test/requests.http new file mode 100644 index 00000000..904d44bf --- /dev/null +++ b/loggers/test/requests.http @@ -0,0 +1,18 @@ +http://localhost:4004/loggers.html +@body: = Content-Type: application/json\n\n + +### +GET http://localhost:4004/log/Loggers + +### +PUT http://localhost:4004/log/Logger/sqlite +{{body:}} { "level": "debug" } + +### +POST http://localhost:4004/log/debug(logger='sqlite') + +### +POST http://localhost:4004/log/reset(logger='sqlite') + +### Dummy request to see sqlite debug output +GET http://localhost:4004/sue/Dummy diff --git a/media/srv/media-service.js b/media/srv/media-service.js index f70d3b90..b656f03c 100644 --- a/media/srv/media-service.js +++ b/media/srv/media-service.js @@ -40,7 +40,7 @@ module.exports = srv => { req.reject(404, 'Media not found for the ID') return } - const decodedMedia = new Buffer( + const decodedMedia = Buffer.from( mediaObj.media.split(';base64,').pop(), 'base64' ) diff --git a/orders/app/fiori.cds b/orders/app/fiori.cds index 8e2a3ab6..017b806b 100644 --- a/orders/app/fiori.cds +++ b/orders/app/fiori.cds @@ -18,22 +18,22 @@ annotate OrdersService.Orders with @( UI: { SelectionFields: [ createdBy ], LineItem: [ - {Value: OrderNo, Label:'OrderNo'}, - {Value: buyer, Label:'Customer'}, - {Value: currency.symbol, Label:'Currency'}, - {Value: createdAt, Label:'Date'}, + {Value: OrderNo, Label:'{i18n>OrderNo}'}, + {Value: buyer, Label:'{i18n>Customer}'}, + {Value: currency.symbol, Label:'{i18n>Currency}'}, + {Value: createdAt, Label:'{i18n>Date}'}, ], HeaderInfo: { - TypeName: 'Order', TypeNamePlural: 'Orders', + TypeName: '{i18n>Order}', TypeNamePlural: '{i18n>Orders}', Title: { - Label: 'Order number ', //A label is possible but it is not considered on the ObjectPage yet + Label: '{i18n>OrderNo}', //A label is possible but it is not considered on the ObjectPage yet Value: OrderNo }, Description: {Value: createdBy} }, Identification: [ //Is the main field group - {Value: createdBy, Label:'Customer'}, - {Value: createdAt, Label:'Date'}, + {Value: createdBy, Label:'{i18n>Customer}'}, + {Value: createdAt, Label:'{i18n>Date}'}, {Value: OrderNo }, ], HeaderFacets: [ @@ -46,7 +46,7 @@ annotate OrdersService.Orders with @( ], FieldGroup#Details: { Data: [ - {Value: currency.code, Label:'Currency'} + {Value: currency.code, Label:'{i18n>Currency}'} ] }, FieldGroup#Created: { @@ -65,6 +65,7 @@ annotate OrdersService.Orders with @( ) { createdAt @UI.HiddenFilter:false; createdBy @UI.HiddenFilter:false; + ID @UI.Hidden; }; @@ -72,15 +73,15 @@ annotate OrdersService.Orders with @( annotate OrdersService.Orders.Items with @( UI: { LineItem: [ - {Value: product_ID, Label:'Product ID'}, - {Value: title, Label:'Product Title'}, - {Value: price, Label:'Unit Price'}, - {Value: quantity, Label:'Quantity'}, + {Value: product_ID, Label:'{i18n>ProductID}'}, + {Value: title, Label:'{i18n>ProductTitle}'}, + {Value: price, Label:'{i18n>UnitPrice}'}, + {Value: quantity, Label:'{i18n>Quantity}'}, ], Identification: [ //Is the main field group - {Value: quantity, Label:'Quantity'}, - {Value: title, Label:'Product'}, - {Value: price, Label:'Unit Price'}, + {Value: quantity, Label:'{i18n>Quantity}'}, + {Value: title, Label:'{i18n>Product}'}, + {Value: price, Label:'{i18n>UnitPrice}'}, ], Facets: [ {$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'}, @@ -90,4 +91,7 @@ annotate OrdersService.Orders.Items with @( quantity @( Common.FieldControl: #Mandatory ); + ID @UI.Hidden; + up_ @UI.Hidden; + }; diff --git a/package-lock.json b/package-lock.json index a8bbb01a..c1f96d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,9 @@ } }, "node_modules/@sap/cds": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-6.2.2.tgz", - "integrity": "sha512-l1ps/Ofp+0k/ngSrCHBFNUXQew6n9A4OJhrm3CroxIEq8wQeXB5LBq53YQQYoCj807eQlXbVbzg6hRCm+BgDaQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-6.3.1.tgz", + "integrity": "sha512-EywUoV16yfYMMEgpY5M4NdNrdjw7dPcIK5c+pAVjio+16PDa7l2x81AhO/JNWD7g7j/POsNUc2ry+LtRxUuceQ==", "dependencies": { "@sap/cds-compiler": "^3.2.0", "@sap/cds-foss": "^4" @@ -442,7 +442,7 @@ "cds": "bin/cds.js" }, "engines": { - "node": ">=14.15.0" + "node": ">=14.18.0" } }, "node_modules/@sap/cds-compiler": { @@ -962,14 +962,14 @@ } }, "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", - "deep-eql": "^3.0.1", + "deep-eql": "^4.1.2", "get-func-name": "^2.0.0", "loupe": "^2.3.1", "pathval": "^1.1.1", @@ -1165,15 +1165,15 @@ } }, "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", + "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", "dev": true, "dependencies": { "type-detect": "^4.0.0" }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, "node_modules/deep-is": { @@ -4191,9 +4191,9 @@ } }, "@sap/cds": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-6.2.2.tgz", - "integrity": "sha512-l1ps/Ofp+0k/ngSrCHBFNUXQew6n9A4OJhrm3CroxIEq8wQeXB5LBq53YQQYoCj807eQlXbVbzg6hRCm+BgDaQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-6.3.1.tgz", + "integrity": "sha512-EywUoV16yfYMMEgpY5M4NdNrdjw7dPcIK5c+pAVjio+16PDa7l2x81AhO/JNWD7g7j/POsNUc2ry+LtRxUuceQ==", "requires": { "@sap/cds-compiler": "^3.2.0", "@sap/cds-foss": "^4" @@ -4612,14 +4612,14 @@ "dev": true }, "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", - "deep-eql": "^3.0.1", + "deep-eql": "^4.1.2", "get-func-name": "^2.0.0", "loupe": "^2.3.1", "pathval": "^1.1.1", @@ -4762,9 +4762,9 @@ } }, "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", + "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", "dev": true, "requires": { "type-detect": "^4.0.0" diff --git a/package.json b/package.json index fcb58d3f..3780ed0a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "./hello", "./media", "./orders", + "./loggers", "./reviews" ], "devDependencies": { diff --git a/test/odata.test.js b/test/odata.test.js index 71d6f0a8..c0987828 100644 --- a/test/odata.test.js +++ b/test/odata.test.js @@ -128,11 +128,11 @@ describe('cap/samples - Bookshop APIs', () => { it('serves user info', async () => { { const { data } = await GET (`/user/me`) - expect(data).to.containSubset({ id: 'alice', locale:'en', tenant: null }) + expect(data).to.containSubset({ id: 'alice', locale:'en' }) } { const { data } = await GET (`/user/me`, {auth: { username: 'joe' }}) - expect(data).to.containSubset({ id: 'joe', locale:'en', tenant: null }) + expect(data).to.containSubset({ id: 'joe', locale:'en' }) } })