ISO content for common CAP types
There's an NPM package that provides default content based on the ISO specifications for CAP common types for countries, languages, currencies and timezones. In this post I explore what that package is and how it works. The post is quite long, mostly because I fell down a rabbit hole and was stuck there for quite a while. Hopefully though it's something you might enjoy. Happy exploring!
Update 15 Mar 2024: In this morning's Hands-on SAP Dev live stream we added this feature to our test project, so you can see this whole thing in action.
Background
Earlier this month in part 7 of our back to basics Hands-on SAP Dev live stream series on CAP Node.js, we added a new element countryOfBirth
to the Authors
entity definition, so that our simple services.cds
looked like this:
using { cuid, Country } from '@sap/cds/common';
service bookshop {
entity Books : cuid {
title: String;
}
entity Authors : cuid {
name: String;
countryOfBirth: Country;
}
}
This resulted in the generation of lots of DDL for a persistence layer, based on the definition of that Country
type, which, in @sap/cds/common
, looks like this (and I've also included here the definitions that are used to describe that type):
type Country : Association to sap.common.Countries;
context sap.common {
entity Countries : CodeList {
key code : String(3) @(title : '{i18n>CountryCode}');
}
aspect CodeList @(
cds.autoexpose,
cds.persistence.skip : 'if-unused'
) {
name : localized String(255) @title : '{i18n>Name}';
descr : localized String(1000) @title : '{i18n>Description}';
}
}
As a result of referring to the Country
type in @sap/cds/common
, we saw this in the output of cds watch
:
[cds] - loaded model from 2 file(s):
services.cds
[...]/node_modules/@sap/cds/common.cds
See the Appendix - loading @sap/cds/common section for an explanation of why
[...]
has been used as a path prefix indicator here.
New entity sets
Additionally, we saw that as well as entity sets for the Books
and Authors
entities, the OData service also contained two more entity sets, as we can see from the service document (which can be obtained with curl localhost:4004/odata/v4/bookshop | jq .
):
{
"@odata.context": "$metadata",
"@odata.metadataEtag": "W/\"l+enQJd57takPctEB4NIbv/1U6KLaLMKeKijx7AfnOo=\"",
"value": [
{
"name": "Books",
"url": "Books"
},
{
"name": "Authors",
"url": "Authors"
},
{
"name": "Countries",
"url": "Countries"
},
{
"name": "Countries_texts",
"url": "Countries_texts"
}
]
}
as well as in the CAP server landing page:
For those of you wondering why
Countries_texts
is not listed in the CAP server landing page, there's an interesting reason, but that's a story for another time.
What about the data?
The response to an OData query operation on the Countries
entity set looked, however, like this:
{
"@odata.context": "$metadata#Countries",
"value": []
}
No data.
On the one hand, that's understandable, we haven't supplied any. But on the other hand (and like the discussion which took place at the time mentioned) it would be great to have that data. Not only for the Country
type, but also for the other CAP common reuse types Currency, Language and Timezone.
After all, that data is standard, predictable and pretty much static. It's also something that we all take for granted in R/3 systems, for example, in these tables (and their related -T
suffixed language-dependent siblings):
T005
(countries)TCUR
(currencies)T002
(languages)TTZZ
(timezones)
In the out-of-the-box provisions from CAP, we don't have this data. But we do have the data in the form of a standard installable NPM package!
I'd totally forgotten about this, which is why I failed to mention it while we were discussing the question. So as a penance (not sure whether to me as the writer of this post, or to you as the reader, sorry) I'm writing up the details here now.
NPM package @sap/cds-common-content
The NPM package @sap/cds-common-content "holds default content based on the ISO specification" for these exact types. Bingo!
The simplest way to make use of this package is to add it to your project:
npm add @sap/cds-common-content
and then add a using
directive in your CDS, such as:
using from '@sap/cds-common-content';
I prefer the semantics of invoking
npm add
overnpm install
, but asadd
is just an alias forinstall
, it's all the same under thenpm
hood anyway.
This all seems quite straightforward. So let's now move away from the customised bookshop project from the live stream series and start with a super simple example project, where we'll see how easy it is to get the data to appear. And it will seem like magic! Then we'll dig in to how it actually works, which will help us understand that bit more about how CAP works. And that's always a bonus, right?
Simple example
So, moving away from the authors and books in the previous services.cds
file, we'll start with a brand new CAP project for this simple example, so you can follow along too if you want.
Initialising a new CAP project with tiny-sample
While initialising the new project, we'll use the --add
option to request the addition of the "tiny-sample" facet which gives us a super simple service exposing a single Books
entity, complete with a couple of data records supplied in a CSV file.
Here's an example of doing that, with the use of the tree
command at the end to show the contents of the new project directory (excluding the hidden files):
# /home/user/work/scratch
; cds init --add tiny-sample iso-data-test
Creating new CAP project in ./iso-data-test
Adding feature 'nodejs'...
Adding feature 'tiny-sample'...
Successfully created project. Continue with 'cd iso-data-test'.
Find samples on https://github.com/SAP-samples/cloud-cap-samples
Learn about next steps at https://cap.cloud.sap
# /home/user/work/scratch
; cd iso-data-test/
# /home/user/work/scratch/iso-data-test
; tree -F
.
|-- README.md
|-- app/
|-- db/
| |-- data/
| | `-- my.bookshop-Books.csv
| `-- data-model.cds
|-- package.json
`-- srv/
`-- cat-service.cds
5 directories, 5 files
# /home/user/work/scratch/iso-data-test
;
The
-F
option tellstree
to use standard symbols to signify special files, as I want to highlight directories with a trailing/
. This-F
works in a similar way to the same-named option with thels
command.
The persistence layer definitions in db/data-model.cds
look like this:
namespace my.bookshop;
entity Books {
key ID : Integer;
title : String;
stock : Integer;
}
and the service layer definitions in srv/cat-service.cds
look like this:
using my.bookshop as my from '../db/data-model';
service CatalogService {
@readonly entity Books as projection on my.Books;
}
Nothing unexpected there, all nice and straightforward.
Basic output from cds watch
Starting the CAP server up at this point, we see (amongst other log lines):
[cds] - loaded model from 2 file(s):
srv/cat-service.cds
db/data-model.cds
[cds] - connect to db > sqlite { database: ':memory:' }
> init from db/data/my.bookshop-Books.csv
/> successfully deployed to in-memory database.
And the service document at http://localhost:4004/odata/v4/catalog
looks like this:
{
"@odata.context": "$metadata",
"@odata.metadataEtag": "W/\"8PKoOs3VhYwQoFzBoQObhMsFJJa5jpD1GLFcWZG9r60=\"",
"value": [
{
"name": "Books",
"url": "Books"
}
]
}
So far so good.
We've covered this in the back to basics series but it's worth re-iterating here too ... the reason why we see these two files specifically:
srv/cat-service.cds
db/data-model.cds
listed in the sources for the CDS model being served (see the "loaded model from 2 file(s)" message in the log output above), is that they're in some specially named directories (db/
and srv/
) that form part of CAP's convention-over-configuration approach to doing the right thing by developers. On startup, the server will automatically look in certain "well-known" locations for CDS definitions. What are these "well-known" locations?
You can ask to see them like this:
cds env folders
which emits:
{ db: 'db/', srv: 'srv/', app: 'app/' }
In fact, there's another environment value which it's possible to query, and that is roots
:
cds env roots
and the value returned:
[ 'db/', 'srv/', 'app/', 'schema', 'services' ]
contains these three directory names, plus two special filenames services
and schema
, which we can interpret as services.cds
and schema.cds
respectively.
💡 This is incidentally why, in the simple capb2b project we're using for the early episodes in the back to basics series, simply putting all our content into a file called services.cds works!
Adding an element with the Country type
Now to start moving towards the use of the Country
type.
First, let's add an element to the Books
entity definition to show where a book was published. We'll do this (and other enhancements in this experiment) in a separate CDS file, to remind ourselves of how well thought out and capable the CDS language and compilation process is.
In a new file, let's call it db/publicationinfo.cds
, let's add this:
using from './data-model';
using { Country } from '@sap/cds/common';
extend my.bookshop.Books with {
publishedIn: Country;
}
- The first
using
directive just imports the definitions from the existingdb/data-model.cds
file, i.e. theBooks
entity in themy.bookshop
namespace. With this firstusing
directive we can then refer to themy.bookshop.Books
entity, as we do with theextend
directive shortly. - The second
using
directive is to bring in the definition of the Country reuse type from@sap/cds/common
. This is so we can use thisCountry
type to describe the new element we're adding to themy.bookshop.Books
entity. - Then in the
extend
directive we can simply add the newpublishedIn
element and define it as having theCountry
type. We already know about how this type is defined from the background section earlier.
As the CAP server is still running in "watch" mode, things restart and now we see something like this:
[cds] - loaded model from 4 file(s):
srv/cat-service.cds
db/publicationinfo.cds
[...]/node_modules/@sap/cds/common.cds
db/data-model.cds
[cds] - connect using bindings from: { registry: '~/.cds-services.json' }
[cds] - connect to db > sqlite { database: ':memory:' }
> init from db/data/my.bookshop-Books.csv
/> successfully deployed to in-memory database.
The log output here shows us that there are two new files in the list from which the model has been loaded:
db/publicationinfo.cds
[...]/node_modules/@sap/cds/common.cds
What's happened of course is that a new file, publicationinfo.cds
, is discovered in the well-known db/
directory. So that is loaded and added to the overall model compilation. And within that file, the using { Country } from '@sap/cds/common';
directive causes the corresponding file from that (built-in) NPM package @sap/cds/common
to be loaded too. Nice!
And now the CAP server has restarted, serving the new enhanced overall model, we see with a request like this:
curl -s localhost:4004/odata/v4/catalog | jq .
that the OData service document now sports the Countries
and Countries_texts
entity sets too (because, briefly, sap.common.Countries
is defined as an entity in [...]/node_modules/@sap/cds/common.cds
and therefore is exposed as an entity set in the OData service):
{
"@odata.context": "$metadata",
"@odata.metadataEtag": "W/\"2n4HnZUJly4q6xyptJ6+4ZptvIICSUNdpk6NYX73bGY=\"",
"value": [
{
"name": "Books",
"url": "Books"
},
{
"name": "Countries",
"url": "Countries"
},
{
"name": "Countries_texts",
"url": "Countries_texts"
}
]
}
Again. So far, so good.
Starting to look at the data
We have some books data, courtesy of the two CSV records that came as part of the "tiny-sample" facet, which we can see with:
curl -s localhost:4004/odata/v4/catalog/Books | jq .
which returns this entity set response:
{
"@odata.context": "$metadata#Books",
"value": [
{
"ID": 1,
"title": "Wuthering Heights",
"stock": 100,
"publishedIn_code": null
},
{
"ID": 2,
"title": "Jane Eyre",
"stock": 500,
"publishedIn_code": null
}
]
}
Those eagle-eyed amongst you might be wondering about the
publishedIn_code
property. That's one that's been auto-generated as a result of CAP's excellent managed associations constructs.Here, specifically, it comes from the combination of the
Country
type used to describe thepublishedIn
element:extend my.bookshop.Books with {
publishedIn: Country;
}and the very definition of
Country
which is a managed "to-one" association as we have already seen:type Country : Association to sap.common.Countries;
This results in, amongst other things, a foreign-key relationship being needed, and being realised via the construction of a property for this, made up of the names of the two elements in the relationship, i.e.
publishedIn
(the new element inmy.bookshop.Books
)code
(the key element insap.common.Countries
)joined with an
_
underscore character to becomepublishedIn_code
.
However, we don't yet have any country data. Requesting the equivalent entity set like this:
curl -s localhost:4004/odata/v4/catalog/Countries | jq .
returns a rather sad and empty looking entity set:
{
"@odata.context": "$metadata#Countries",
"value": []
}
So ... @sap/cds-common-content
to the rescue!
Using @sap/cds-common-content
Like we saw earlier, this can be brought into the project simply by adding it as a package. So let's do that now:
npm add @sap/cds-common-content
After seeing output like this:
added 115 packages, and audited 116 packages in 17s
21 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
we see that package.json
now has the package listed in the dependencies
section:
{
"name": "iso-data-test",
"version": "1.0.0",
"description": "A simple CAP project.",
"repository": "<Add your repository here>",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@sap/cds": "^7",
"@sap/cds-common-content": "^1.4.0",
"express": "^4"
},
"devDependencies": {
"@cap-js/sqlite": "^1"
},
"scripts": {
"start": "cds-serve"
}
}
and the rest of the dependencies have been installed too (through a more general NPM install side-effect) - we can see this with npm list
; here's an example invocation:
; npm list
iso-data-test@1.0.0 /home/user/work/scratch/iso-data-test
+-- @cap-js/sqlite@1.5.1
+-- @sap/cds-common-content@1.4.0
+-- @sap/cds@7.7.2
`-- express@4.18.3
So now, to actually make use of this package and what it brings, we have to add a using
directive, as we saw earlier.
Let's add that to the db/publicationinfo.cds
file, like this:
using from './data-model';
using { Country } from '@sap/cds/common';
using from '@sap/cds-common-content';
extend my.bookshop.Books with {
publishedIn: Country;
}
As the CAP server is still running in "watch" mode, it restarts, and 💥 what an explosion of log output!
[cds] - loaded model from 6 file(s):
srv/cat-service.cds
db/publicationinfo.cds
node_modules/@sap/cds-common-content/index.cds
db/data-model.cds
node_modules/@sap/cds-common-content/db/index.cds
node_modules/@sap/cds/common.cds
[cds] - connect using bindings from: { registry: '~/.cds-services.json' }
[cds] - connect to db > sqlite { url: ':memory:' }
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_zh_TW.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_zh_CN.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_tr.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_th.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_sv.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ru.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ro.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_pt.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_pl.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_no.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_nl.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ms.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ko.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ja.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_it.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_hu.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_fr.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_fi.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_es_MX.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_es.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_en.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_de.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_da.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_cs.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries_texts_ar.csv
> init from node_modules/@sap/cds-common-content/db/data/sap-common-Countries.csv
> init from db/data/my.bookshop-Books.csv
/> successfully deployed to in-memory database.
Wow! What's more, we now have country data in the Countries
entity set:
curl -s 'localhost:4004/odata/v4/catalog/Countries?$top=5' \
| jq .
{
"@odata.context": "$metadata#Countries",
"value": [
{
"name": "Andorra",
"descr": "Andorra",
"code": "AD"
},
{
"name": "Utd Arab Emir.",
"descr": "United Arab Emirates",
"code": "AE"
},
{
"name": "Afghanistan",
"descr": "Afghanistan",
"code": "AF"
},
{
"name": "Antigua/Barbuda",
"descr": "Antigua and Barbuda",
"code": "AG"
},
{
"name": "Anguilla",
"descr": "Anguilla",
"code": "AI"
}
]
}
That's fab. But. What's going on? Where is this coming from? How does this work?
Digging in to what's happening
Let's take a bit of time to figure out how this is all working, and why we now have country data.
We added a single line to the CDS model:
using from '@sap/cds-common-content';
What did the addition of this line actually do to cause that explosion of change and the appearance of ISO country data?
Well, our first clue is the extra entries that now are appearing in the list of files from which the CDS model is constructed:
[cds] - loaded model from 6 file(s):
srv/cat-service.cds
db/publicationinfo.cds
node_modules/@sap/cds-common-content/index.cds
db/data-model.cds
node_modules/@sap/cds-common-content/db/index.cds
node_modules/@sap/cds/common.cds
Working through them, we first see our base files:
srv/cat-service.cds
db/data-model.cds
We also see the two extra files that were picked up once we added the db/publicationinfo.cds
file which itself summoned the @sap/cds/common
content:
db/publicationinfo.cds
node_modules/@sap/cds/common.cds
At this point I've stopped being deliberately vague (with
[...]
) about the specific location of the files innode_modules/
, because it's worth highlighting here something that has changed. Thenpm add
action just before caused the rest of the packages (defined inpackage.json
) to be installed in the project. This means that there's now a project-localnode_modules/
directory containing everything that this project needs, including all the@sap
prefixed NPM packages.A quick
tree -F -L 3
shows the directory structure containing those resources (I've removed some of the output for brevity):./
|-- README.md
|-- app/
|-- db/
| |-- data/
| | `-- my.bookshop-Books.csv
| |-- data-model.cds
| `-- publicationinfo.cds
|-- node_modules/
| |-- @cap-js/
| | |-- cds-types/
| | |-- db-service/
| | `-- sqlite/
| |-- @sap/
| | |-- cds/
| | |-- cds-common-content/
| | |-- cds-compiler/
| | |-- cds-fiori/
| | `-- cds-foss/
| |-- ...
| `-- yaml/
| |-- LICENSE
| |-- README.md
| |-- bin.mjs*
| |-- browser/
| |-- dist/
| |-- package.json
| `-- util.js
|-- package-lock.json
|-- package.json
`-- srv/
`-- cat-service.cdsSo this means that the
@sap/cds/common
package is being loaded now from the project-local set of packages, i.e. innode_modules/
relative to the project directory (i.e../node_modules/
) and not the global NPM package area any more.This in turn means that the full (relative) path to this file now in the list is clean and short(er):
node_modules/@sap/cds/common.cds
OK, so we know why these four of the six files are being loaded, and where from:
srv/cat-service.cds
db/data-model.cds
db/publicationinfo.cds
node_modules/@sap/cds/common.cds
So what about the other two in the list:
node_modules/@sap/cds-common-content/index.cds
node_modules/@sap/cds-common-content/db/index.cds
which are now also being brought in to construct the model?
Well, given the "cds-common-content" that appears in the path of these files, we can be pretty certain that they're related to this single line we just added:
using from '@sap/cds-common-content';
so what is actually happening here?
Well, if we look a bit deeper inside the @sap/cds-common-content
package, like this:
tree -F -L 2 node_modules/@sap/cds-common-content
there's a bit of a clue in the output, especially if we're familiar with CAP's Reuse and Compose concepts:
node_modules/@sap/cds-common-content/
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- db/
| |-- index.cds
| `-- data/
|-- index.cds
`-- package.json
3 directories, 6 files
Look at those index.cds
files. The Using index.cds Entry Points section of the Reuse and Compose section of the Capire documentation says this, in the context of a using
directive like we have here (i.e. using from '@sap/cds-common-content';
):
"The using from
statements assume that the imported packages provide index.cds
in their roots as public entry points, which they do."
So, that means that the using
directive will cause this file:
node_modules/@sap/cds-common-content/index.cds
to also be loaded and its CDS contents added into the overall model construction. So what's in this file? This:
using from './db';
So let's now follow this using
directive, the resource reference within which should be interpreted as local to the containing index.cds
file, i.e. we're now going to follow this path to ./db/
, which also contains an index.cds
:
node_modules/@sap/cds-common-content/
|-- CHANGELOG.md
|-- LICENSE
|-- README.md
|-- db/
| |-- index.cds <--------------------+
| `-- data/ |
|-- index.cds -- using from './db'; ---+
`-- package.json
So what's in ./db/index.cds
? This:
using sap.common.Languages from '@sap/cds/common';
using sap.common.Countries from '@sap/cds/common';
using sap.common.Currencies from '@sap/cds/common';
using sap.common.Timezones from '@sap/cds/common';
Ooh!
What is the effect of doing this? Well it has a direct effect and a sort of side-effect too.
The direct effect is that all four entity definitions referenced in this ./db/index.cds
file, that is to say these four:
sap.common.Languages
sap.common.Countries
sap.common.Currencies
sap.common.Timezones
are added to the overall model.
But the side-effect is that the ./db/data/
directory here also becomes a candidate location for the automatic provision of initial data!
And what's in that ./db/data/
directory? Let's have a look:
ls node_modules/@sap/cds-common-content/db/data/
Lots and lots of CSV files (I've cut the list down to just a few here):
./ sap-common-Languages_texts_cs.csv
../ sap-common-Languages_texts_da.csv
sap-common-Countries.csv sap-common-Languages_texts_de.csv
sap-common-Countries_texts.csv sap-common-Languages_texts_en.csv
sap-common-Countries_texts_ar.csv sap-common-Languages_texts_es.csv
sap-common-Countries_texts_zh_CN.csv sap-common-Timezones_texts_cs.csv
sap-common-Countries_texts_zh_TW.csv sap-common-Timezones_texts_da.csv
sap-common-Currencies.csv sap-common-Timezones_texts_de.csv
sap-common-Currencies_texts.csv sap-common-Timezones_texts_el.csv
sap-common-Currencies_texts_ar.csv sap-common-Timezones_texts_en.csv
...
And we know what happens with initial data, for those entities whose namespaced-names match up with CSV filenames in directories such as these - the data is automatically imported to become data for those entities!
Notice too that for each of the four entities, there is a single CSV file containing the core data, and multiple CSV files for the corresponding localized elements, in accompanying _texts*
suffixed files.
Here's one example, for the sap.common.Countries
entity. A neat list of the intial CSV data files for this entity can be retrieved with:
cd node_modules/@sap/cds-common-content/db/data/ \
&& ls -1 sap-common-Countries*.csv
which produces:
sap-common-Countries.csv
sap-common-Countries_texts.csv
sap-common-Countries_texts_ar.csv
sap-common-Countries_texts_cs.csv
sap-common-Countries_texts_da.csv
sap-common-Countries_texts_de.csv
sap-common-Countries_texts_en.csv
sap-common-Countries_texts_es.csv
sap-common-Countries_texts_es_MX.csv
sap-common-Countries_texts_fi.csv
sap-common-Countries_texts_fr.csv
sap-common-Countries_texts_hu.csv
sap-common-Countries_texts_it.csv
sap-common-Countries_texts_ja.csv
sap-common-Countries_texts_ko.csv
sap-common-Countries_texts_ms.csv
sap-common-Countries_texts_nl.csv
sap-common-Countries_texts_no.csv
sap-common-Countries_texts_pl.csv
sap-common-Countries_texts_pt.csv
sap-common-Countries_texts_ro.csv
sap-common-Countries_texts_ru.csv
sap-common-Countries_texts_sv.csv
sap-common-Countries_texts_th.csv
sap-common-Countries_texts_tr.csv
sap-common-Countries_texts_zh_CN.csv
sap-common-Countries_texts_zh_TW.csv
We can see the three groups of files for sap.common.Countries
:
- the single core data file
sap-common-Countries.csv
containing values for thecode
,name
anddescr
fields (in English as default). - the single core localized data file
sap-common-Countries_texts.csv
- where the filename is not specific to an explicit locale - containing values for thelocale
,code
,name
anddescr
field. The language specific content is English by default. - the multiple language-specific localized data files
sap-common-Countries_texts_<locale-identifier>.csv
containing the same data as the core localized file but with the texts translated into the language indicated by the locale-identifier in the file name.
You'll likely remember seeing a list of all these CSV files in the explosion of output from the running CAP server earlier.
And what's the outcome of this?
To find out, let's expand the core books data now to include values for the new publishedIn_code
field at the persistence layer, so that the content of db/data/my.bookshop-Books.csv
now looks like this:
ID;title;stock;publishedIn_code
1;Wuthering Heights;100;DE
2;Jane Eyre;500;HU
Yes I know Wuthering Heights wasn't published in Germany, nor was Jane Eyre published in Hungary, but thank you for wondering about that.
Now, NOT ONLY (μέν) can we follow navigation properties to see the publication countries for our books, like this (note the country names "Germany" and "Hungary"):
curl \
--silent \
--url 'localhost:4004/odata/v4/catalog/Books?$expand=publishedIn' \
| jq .
to get this:
{
"@odata.context": "$metadata#Books(publishedIn())",
"value": [
{
"ID": 1,
"title": "Wuthering Heights",
"stock": 100,
"publishedIn_code": "DE",
"publishedIn": {
"name": "Germany",
"descr": "Germany",
"code": "DE"
}
},
{
"ID": 2,
"title": "Jane Eyre",
"stock": 500,
"publishedIn_code": "HU",
"publishedIn": {
"name": "Hungary",
"descr": "Hungary",
"code": "HU"
}
}
]
}
BUT ALSO (δέ) we can ask for this information in our own language (locale)! Here's an example, requesting the same resource but with a different locale (French), via standard HTTP headers:
I'm fond of the strong particle pairing of μέν ... δέ which I first learned about as an important construct in Ancient Greek, and I find myself often using the (English) equivalent ("not only ... but also").
curl \
--silent \
--header 'Accept-Language: fr' \
--url 'localhost:4004/odata/v4/catalog/Books?$expand=publishedIn' \
| jq .
The representation of the resource requested is now different, in that the names of the countries are now in French ("Allemagne" and "Hungarie"):
{
"@odata.context": "$metadata#Books(publishedIn())",
"value": [
{
"ID": 1,
"title": "Wuthering Heights",
"stock": 100,
"publishedIn_code": "DE",
"publishedIn": {
"name": "Allemagne",
"descr": "Allemagne",
"code": "DE"
}
},
{
"ID": 2,
"title": "Jane Eyre",
"stock": 500,
"publishedIn_code": "HU",
"publishedIn": {
"name": "Hongrie",
"descr": "Hongrie",
"code": "HU"
}
}
]
}
C'est vraiment magnifique!
Of course, one wouldn't often use an explicit Accept-Language
header in an HTTP request, but think of what headers your browser sends by default when making requests, and think of what your French colleague's browser might send. Exactly!
Wrapping up
OK, I think I've reached a point where I can now safely escape this rabbit hole of discovery. The bottom line is that there is a standard NPM package available that provides actual ISO data for the four CAP reuse types. You've learned how to use it, and what it provides. Moreover, you've learned how it provides what it does, and what goes on behind the scenes. That, hopefully, has given you a tiny bit more insight into the wonders of CAP.
And there's still so much more to discover. Until next time!
Appendix - loading @sap/cds/common
In the Background section earlier, the output of cds watch
showed this line:
[...]/node_modules/@sap/cds/common.cds
The reason I included a [...]
"prefix" was to signify that there will be a different path shown depending on whether an npm install
of the project dependencies (which include @sap/cds
) has been executed, or not.
If an npm install
has been executed, then there will be a project-local node_modules/
directory, and the @sap/cds/common
resource will have been loaded from within there, i.e.
./node_modules/@sap/cds/common.cds
If an npm install
hasn't been executed, and we're running the CAP server from the NPM globally installed @sap/cds-dk
package, then it will have been loaded from that globally installed package location, and look something like this (depending on what your NPM global package management directory setup looks like):
../../usr/local/share/npm-global/lib/
node_modules/@sap/cds-dk/
node_modules/@sap/cds/common.cds
This location value would typically be shown in a single line; I've split the value up over a few lines purely for readability.
You can find out where your (normally globally installed) @sap/cds-dk
package is, using the cds env
command:
cds env _home_cds-dk
In the context of this particular dev container in which I'm working, this is the value that is emitted:
'/usr/lib/node_modules/@sap/cds-dk/'