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' })
}
})