CAP, CDS, CDL, CSN and jq - finding associations
In CAP, relationships between entities can be expressed with associations. In digging into how these are represented internally, I found jq yet again to be invaluable in parsing out info from the internal representation of the definitions. Here's what I did.
In CAP, SAP's Cloud Application Programming Model, models are defined declaratively in a human readable format known as CDL, which stands for Core Definition Language1.
CDL example
Here's a simple example, in a file called services.cds
taken from the current Hands-on SAP Dev live stream series on back to basics with CAP Node.js:
aspect cuid { key ID: UUID }
service bookshop {
entity Books : cuid {
title: String;
author: Association to Authors;
}
entity Authors : cuid {
name: String;
books: Association to many Books on books.author = $self;
}
}
Yes, there's a
cuid
aspect in@sap/cds/common
but I didn't want the entire contents of that package being expressed in the output that follows, as it would make it more difficult to see the essential parts in this short example.
There's a "to-one" relationship between Books
and Authors
, and a "to-many" relationship in the back link beween Authors
and Books
.
For those non-CAP folks wondering where the foreign key field is in the first relationship, it's constructed automatically as the
Books:author
association is a managed one.
CSN equivalent
There's an internal JSON-based representation of such definitions, which lends itself more readily to machine processing. This representation is called Core Schema Notation, or CSN. We can see the CSN for the CDL source above, like this:
cds compile --to csn services.cds
The default target format is in fact
csn
so--to csn
is unnecessary, but it's nice to express it explicitly here.
What we get is this:
{
"definitions": {
"cuid": {
"kind": "aspect",
"elements": {
"ID": {
"key": true,
"type": "cds.UUID"
}
}
},
"bookshop": {
"kind": "service"
},
"bookshop.Books": {
"kind": "entity",
"includes": [
"cuid"
],
"elements": {
"ID": {
"key": true,
"type": "cds.UUID"
},
"title": {
"type": "cds.String"
},
"author": {
"type": "cds.Association",
"target": "bookshop.Authors",
"keys": [
{
"ref": [
"ID"
]
}
]
}
}
},
"bookshop.Authors": {
"kind": "entity",
"includes": [
"cuid"
],
"elements": {
"ID": {
"key": true,
"type": "cds.UUID"
},
"name": {
"type": "cds.String"
},
"books": {
"type": "cds.Association",
"cardinality": {
"max": "*"
},
"target": "bookshop.Books",
"on": [
{
"ref": [
"books",
"author",
"ID"
]
},
"=",
{
"ref": [
"ID"
]
}
]
}
}
}
},
"meta": {
"creator": "CDS Compiler v4.6.2",
"flavor": "inferred"
},
"$version": "2.0"
}
Picking out detail with jq
I'm just interested to see how the associations are represented, so wanted to narrow this CSN down to just elements that are of type cds.Association
. Being a relatively involved JSON dataset, this was a job for one of my favourite languages, jq.
Here's what I came up with, in a file called associations
:
#!/usr/bin/env -S jq -f
# Lists entities and shows any elements that are associations
def is_entity: .value.kind == "entity";
def is_association: .value.type == "cds.Association";
.definitions
| to_entries
| map(
select(is_entity)
| {
(.key):
.value.elements
| with_entries(select(is_association))
}
)
And here's how I use it:
cds compile --to csn services.cds | ./associations
The output is:
[
{
"bookshop.Books": {
"author": {
"type": "cds.Association",
"target": "bookshop.Authors",
"keys": [
{
"ref": [
"ID"
]
}
]
}
}
},
{
"bookshop.Authors": {
"books": {
"type": "cds.Association",
"cardinality": {
"max": "*"
},
"target": "bookshop.Books",
"on": [
{
"ref": [
"books",
"author",
"ID"
]
},
"=",
{
"ref": [
"ID"
]
}
]
}
}
}
]
Here are some notes:
- I encapsulated the kind / type determination in helper predicate functions (
is_entity
andis_association
), mostly to keep the main code more succinct - I love how the to_entries,from_entries,with_entries(f) family of functions2 help out by normalising JSON objects that have dynamic values for property keys
- By using the
select(is_entity)
in the context of such normalisation, I can easily pick out the "enclosing" object that contains the condition I'm looking for - In contrast to the use of
to_entries
at the outer (entity) level, I usedwith_entries
to achieve theto_entries | map(...) | from_entries
pattern that is so useful
And that's about it! Score one more for the wonderful utility of jq, in today's world of JSON.
Alternative approach
While I like the is_entity
and is_association
definitions, one could make the main code even more succinct like this:
def entities: select(.value.kind == "entity");
def associations: select(.value.type == "cds.Association");
.definitions
| to_entries
| map(
entities
| {
(.key):
.value.elements | with_entries(associations)
}
)
Which do you prefer?
1: The human readable language we used to define models is commonly referred to as CDS. Capire, the CAP documentation resource, is more precise, calling it CDL, classing CDL, CSN and other CAP little languages such as CQL and CQN as all being under the general CDS umbrella term. As you can see from the bottom of the CSN output, even the compiler refers to it as CDS!
2: For more on this family of functions, see Reshaping data values using jq's with_entries and Quick conversion of multiple values using with_entries in jq.