@@ -14,6 +14,7 @@
@@ -77,6 +78,7 @@ import play from '/icons/play.svg?url&raw'
import { runners, runWithModel } from './runners'
import highlighter from './highlighter'
import templates from 'virtual:templates'
+import { transformerMetaHighlight } from '@shikijs/transformers'
const uid = useId()
@@ -91,6 +93,10 @@ const props = defineProps({
type: Boolean,
default: false
},
+ async: {
+ type: Boolean,
+ default: false
+ },
language: {
type: String,
default: 'js'
@@ -103,6 +109,10 @@ const props = defineProps({
type: String,
default: ''
},
+ highlightLines: {
+ type: String,
+ default: ''
+ },
onEvaluate: {
type: Function
}
@@ -156,14 +166,18 @@ function toggleModel() {
}
}
-function format({ value, kind }, dark) {
+function format({ value, kind }, dark, highlightSpec = '') {
if (!highlighter.getLoadedLanguages().includes(kind)) {
kind = 'plaintext'
}
- const html = highlighter.codeToHtml(
+ const opts = { lang: kind, theme: dark ? 'github-dark' : 'github-light', transformers: [], meta: undefined }
+ if (highlightSpec) {
+ opts.meta = { __raw: highlightSpec }
+ opts.transformers = [transformerMetaHighlight()]
+ }
+ return highlighter.codeToHtml(
typeof value === 'string' ? value : JSON.stringify(value, null, 2),
- { lang: kind, theme: dark ? 'github-dark' : 'github-light' })
- return html
+ opts)
}
function formatTabs(result) {
@@ -220,7 +234,7 @@ async function evaluate() {
const exec = props.onEvaluate
?? (props.modelSource ? (q) => runWithModel(q, props.modelSource, props.modelData ? JSON.parse(props.modelData) : undefined) : runners[props.language])
if (!exec) throw new Error(`No runner found for language: ${props.language}. Available runners: ${Object.keys(runners).join(', ')}`)
- const result = await exec(queryText.value)
+ const result = await exec(queryText.value, props.async)
tabs.value = formatTabs(result).filter(({ value }) => value)
if (!tabs.value.map(tab => tab.key).includes(selectedTab.value)) selectedTab.value = tabs.value[0].key
diff --git a/.vitepress/theme/components/cds-playground/MonacoEditor.vue b/.vitepress/theme/components/cds-playground/MonacoEditor.vue
index 98d5a6577..6d16244bd 100644
--- a/.vitepress/theme/components/cds-playground/MonacoEditor.vue
+++ b/.vitepress/theme/components/cds-playground/MonacoEditor.vue
@@ -26,6 +26,10 @@ const props = defineProps({
rows: {
type: Number,
default: 3
+ },
+ highlightLines: {
+ type: String,
+ default: ''
}
})
@@ -86,6 +90,17 @@ async function createEditor() { try {
const contentSizeDispose = editor.onDidContentSizeChange(() => updateHeight())
updateHeight()
+ if (props.highlightLines) {
+ const lines = props.highlightLines.replace(/^\{|\}$/g, '').split(',').flatMap(part => {
+ const [a, b] = part.trim().split('-').map(Number)
+ return b ? Array.from({ length: b - a + 1 }, (_, i) => a + i) : [a]
+ })
+ editor.createDecorationsCollection(lines.map(line => ({
+ range: new monaco.Range(line, 1, line, 1),
+ options: { isWholeLine: true, className: 'live-code-highlighted-line' }
+ })))
+ }
+
// Emit evaluate on Cmd/Ctrl+Enter
editor.addAction({
id: 'eval',
@@ -155,4 +170,8 @@ watch(() => isDark.value, (dark) => {
background-color: var(--vp-code-block-bg) !important;
font-family: var(--vp-font-family-mono) !important;
}
+
+.live-code-highlighted-line {
+ background-color: var(--vp-code-line-highlight-color) !important;
+}
diff --git a/.vitepress/theme/components/cds-playground/highlighter.js b/.vitepress/theme/components/cds-playground/highlighter.js
index 5dce6261c..381d52e43 100644
--- a/.vitepress/theme/components/cds-playground/highlighter.js
+++ b/.vitepress/theme/components/cds-playground/highlighter.js
@@ -3,7 +3,7 @@ import languages from '../../../languages'
const highlighter = await createHighlighter({
themes: ['github-dark', 'github-light'],
- langs: ['javascript', 'js', 'sql', 'typescript', 'vue', ...languages],
+ langs: ['javascript', 'js', 'sql', 'typescript', 'vue', 'yaml', ...languages],
langAlias: Object.fromEntries(languages.flatMap(l => {
if (!l || typeof l !== 'object' || !Array.isArray(l.aliases) || !l.name) return []
return l.aliases.map(alias => [alias, l.name])
diff --git a/.vitepress/theme/components/cds-playground/runners.js b/.vitepress/theme/components/cds-playground/runners.js
index c383391cc..93d241368 100644
--- a/.vitepress/theme/components/cds-playground/runners.js
+++ b/.vitepress/theme/components/cds-playground/runners.js
@@ -27,7 +27,7 @@ function injectLogger(sqlite) {
return sqlLog;
}
-
+/** @returns {Promise} */
async function initialize() {
const cds = (await import('@sap/cds')).default;
const express = (await import('express')).default;
@@ -61,6 +61,7 @@ async function initialize() {
return cds;
}
+/** @type {ReturnType} */
let initialized;
if (!import.meta.env.SSR) {
// runs only in the browser
@@ -68,15 +69,40 @@ if (!import.meta.env.SSR) {
}
const AsyncFunction = async function () {}.constructor;
-async function evalJS(code) {
- await initialized;
- const fn = new AsyncFunction(code);
- const { result, formatted } = await sql.trace(fn);
+async function evalJS(code, isAsync) {
+ const cds = await initialized;
+ const source = compile(code);
+
+ function resultTabs(result, kind) {
+ if (kind === 'json') {
+ let yaml
+ try { yaml = cds.compile.to.yaml(result) } catch {/* ignore */}
+ if (yaml) return [
+ { value: yaml, kind: 'yaml', name: 'Result (as yaml)' },
+ { value: JSON.stringify(result, null, 2), kind: 'json', name: 'Result (raw)' },
+ ]
+ }
+ return [{ value: result ? typeof result !== 'string' ? JSON.stringify(result, null, 2) : result : "success", kind, name: 'Result' }]
+ }
+
+ if (isAsync) {
+ let fn;
+ try { fn = new AsyncFunction(source) }
+ catch { fn = new AsyncFunction(code) } // rewrite had a syntax error -> run the code unmodified
+ const { result, formatted } = await sql.trace(fn);
+ const kind = result? 'json' : 'plaintext'
+ return [
+ ...resultTabs(result, kind),
+ { value: formatted, kind: 'sql', name: 'SQL'}
+ ];
+ }
+
+ let fn;
+ try { fn = new Function(source) }
+ catch { fn = new Function(code) } // rewrite had a syntax error -> run the code unmodified
+ const result = fn();
const kind = result? 'json' : 'plaintext'
- return [
- { value: result ? typeof result !== 'string' ? JSON.stringify(result, null, 2) : result : "success", kind, name: 'Result' },
- { value: formatted, kind: 'sql', name: 'SQL'}
- ];
+ return resultTabs(result, kind);
}
async function cdsQL(query) {
@@ -138,3 +164,103 @@ export const runners = {
cql: cdsQL,
cds: cdsQL,
}
+
+function compile(code) {
+ const stmts = splitTopLevelStatements(code)
+ if (!stmts.length) return code
+ const last = stmts[stmts.length - 1]
+
+ // last statement already returns, or is a control-flow/declaration keyword -> leave the code as is
+ if (/^(return|throw|if|for|while|function|class|import|export)\b/.test(last.text)) return code
+
+ // anchored right after the keyword so we don't match "=" occurring inside the initializer, e.g. in a template literal
+ const declRe = /^(?:let|const|var)\s+([A-Za-z_$][\w$]*)\s*=/
+ if (declRe.test(last.text)) {
+ // last statement declares a variable, e.g. "let result = 1+1" -> collect all top-level declarations in the
+ // snippet so earlier ones aren't silently dropped, e.g. comparing "let q = ...; let p = ..." side by side
+ const names = stmts.map(s => s.text.match(declRe)?.[1]).filter(Boolean)
+ return names.length > 1
+ ? `${code}\nreturn { ${names.join(', ')} };`
+ : `${code}\nreturn ${names[0]};`
+ }
+
+ // last statement isn't a declaration -> treat it (possibly spanning multiple lines) as the expression to return
+ return `${code.slice(0, last.start)}\nreturn (\n${last.text.replace(/;\s*$/, '')}\n);`
+}
+
+
+// splits code into its top-level statements (ignoring newlines/semicolons nested inside brackets, strings,
+// template literals or comments), so multi-line statements like object literals are kept intact as one unit
+function splitTopLevelStatements(code) {
+ const scrubbed = blankComments(code) // same length as code, but with comments replaced by spaces
+ const stmts = []
+ let start = 0, depth = 0, i = 0
+ while (i < scrubbed.length) {
+ const c = scrubbed[i]
+ if (c === '"' || c === "'") { i = skipString(scrubbed, i, c); continue }
+ if (c === '`') { i = skipTemplate(scrubbed, i); continue }
+ if (c === '(' || c === '{' || c === '[') { depth++; i++; continue }
+ if (c === ')' || c === '}' || c === ']') { depth--; i++; continue }
+ if (depth <= 0 && (c === ';' || c === '\n')) {
+ const text = scrubbed.slice(start, i).trim()
+ if (text) stmts.push({ text, start })
+ i++; start = i; continue
+ }
+ i++
+ }
+ const text = scrubbed.slice(start).trim()
+ if (text) stmts.push({ text, start })
+ return stmts
+}
+
+// replaces line and block comments with spaces of the same length, so a trailing comment (e.g. after the last
+// statement, or commented-out code on its own line) is never mistaken for code, while offsets stay unchanged
+function blankComments(code) {
+ let out = ''
+ let i = 0
+ while (i < code.length) {
+ const c = code[i]
+ if (c === '/' && code[i + 1] === '/') { while (i < code.length && code[i] !== '\n') { out += ' '; i++ }; continue }
+ if (c === '/' && code[i + 1] === '*') {
+ while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) { out += code[i] === '\n' ? '\n' : ' '; i++ }
+ out += ' '; i += 2; continue
+ }
+ if (c === '"' || c === "'") { const j = skipString(code, i, c); out += code.slice(i, j); i = j; continue }
+ if (c === '`') { const j = skipTemplate(code, i); out += code.slice(i, j); i = j; continue }
+ out += c; i++
+ }
+ return out
+}
+
+// skips a single- or double-quoted string starting at code[i], returning the index right after the closing quote
+function skipString(code, i, quote) {
+ i++
+ while (i < code.length && code[i] !== quote) { if (code[i] === '\\') i++; i++ }
+ return i + 1
+}
+
+// skips a template literal starting at code[i] (the opening backtick), diving into ${...} interpolations
+function skipTemplate(code, i) {
+ i++
+ while (i < code.length) {
+ if (code[i] === '\\') { i += 2; continue }
+ if (code[i] === '`') return i + 1
+ if (code[i] === '$' && code[i + 1] === '{') { i = skipBraces(code, i + 2); continue }
+ i++
+ }
+ return i
+}
+
+// skips forward to the '}' balancing the '${' whose contents start at code[i]
+function skipBraces(code, i) {
+ let depth = 1
+ while (i < code.length && depth > 0) {
+ const c = code[i]
+ if (c === '"' || c === "'") { i = skipString(code, i, c); continue }
+ else if (c === '`') { i = skipTemplate(code, i); continue }
+ else if (c === '{') depth++
+ else if (c === '}') depth--
+ i++
+ }
+ return i
+}
diff --git a/cds/cdl.md b/cds/cdl.md
index 9487f7826..e4ffc07e4 100644
--- a/cds/cdl.md
+++ b/cds/cdl.md
@@ -147,8 +147,7 @@ Within those strings, escape sequences from JavaScript, such as `\t` or `\u0020`
Using directives allow to import definitions from other CDS models. As shown in line 3 below, you optionally can specify local aliases to be used subsequently. You can import single definitions as well as several ones with a common namespace prefix.
-::: code-group
-
+```cds
using foo.bar.scoped.Bar from './contexts';
using foo.bar.scoped.nested from './contexts';
using foo.bar.scoped.nested as animal from './contexts';
@@ -158,8 +157,6 @@ entity Moo : nested.Zoo {} //> : foo.bar.scoped.nested.Zoo
entity Zoo : animal.Zoo {} //> : foo.bar.scoped.nested.Zoo
```
-:::
-
Multiple named imports through ES6-like deconstructors:
```cds
diff --git a/cds/cxl.md b/cds/cxl.md
index 41936fe71..9c7208104 100644
--- a/cds/cxl.md
+++ b/cds/cxl.md
@@ -44,7 +44,7 @@ The cds model initialized on this page is a slightly modified version of the [ca
All samples run on a single browser-local `cds` instance, you can access it via the dev tools
or run statements in the following code block:
-```js live
+```js live [async]
await INSERT.into('Books').entries(
{ ID: 2, author_ID: 150, title: 'Eldorado' }
)
diff --git a/node.js/cds-compile.md b/node.js/cds-compile.md
index 7c2ae8f40..e493580e3 100644
--- a/node.js/cds-compile.md
+++ b/node.js/cds-compile.md
@@ -54,7 +54,7 @@ let csn = await cds.compile ('file:db')
> The given filenames are resolved to effective absolute filenames using [`cds.resolve`](#cds-resolve).
> [!TIP] Use cds compile as CLI equivalent
-> The [`cds compile` CLI](../tools/cds-cli#cds-compile) is available as entry point to the functions described here. For example, `cds compile --to hana` maps to `cds.compile.to.hana` etc.
+> The [`cds compile` CLI](../tools/cds-cli#cds-compile) is available as entry point to the functions described here. For example, `cds compile --to hana` maps to [`cds.compile.to.hana`](#hana) etc.
@@ -62,34 +62,35 @@ let csn = await cds.compile ('file:db')
If a single string, not starting with `file:` is passed as first argument, it is interpreted as a CDL source string and compiled to CSN synchronously:
-```js
+```js live
let csn = cds.compile (`
- using {cuid} from '@sap/cds/common';
- entity Foo : cuid { foo:String }
+ entity Foo { foo:String }
entity Bar as projection on Foo;
extend Foo with { bar:String }
`)
```
-> Note: `using from` clauses are not resolved in this usage.
-
+> [!note] `using from` clauses are not resolved in this usage.
+> In this example, there is an error at the line where [`cuid`](../cds/common#aspect-cuid) gets used:
+>
+> ```js live
+> let csn = cds.compile (`
+> using { cuid } from '@sap/cds/common';
+> entity Foo : cuid { foo:String }
+> `)
+> ```
### Multiple in-memory sources
-Finally, you can pass an object with multiple named CDL or CSN sources, which allows to also resolve `using from` clauses:
+Finally, you can pass an object with multiple named CDL or CSN sources, which allows to also resolve [`using from` clauses](../cds/cdl#model-imports):
-```js
+```js live {3,6}
let csn = cds.compile ({
'db/schema.cds': `
- using {cuid} from '@sap/cds/common';
+ using { cuid } from '@sap/cds/common';
entity Foo : cuid { foo:String }
`,
- 'srv/services.cds': `
- using {Foo} from '../db/schema';
- entity Bar as projection on Foo;
- extend Foo with { bar:String }
- `,
'@sap/cds/common.csn': `
{"definitions":{
"cuid": { "kind": "aspect", "elements": {
@@ -100,20 +101,30 @@ let csn = cds.compile ({
})
```
-
-
+> [!tip] Reference imported models with canonic names
+> In the example, note that the `@sap/cds/common.csn` source is referenced through the canonic `@sap/cds/common` name.
+> From the usage perspective, it should not matter if imported definitions are defined as [CDL](../cds/cdl) or [CSN](../cds/csn) and what their technical (file) name is.
+>
+> [Learn more on CDS model resolution.](../cds/cdl#model-resolution){.learn-more}
### Additional Options
You can pass additional options like so:
-```js
-let csn = await cds.compile('*',{ min:true, docs:true })
+```js live
+let messages = []
+let csn = cds.compile(`
+ /** A comment about */
+ /** entity Foo */
+ entity Foo { foo:String }
+ entity Bar as projection on Foo;
+ type T { e:String }
+ `,
+ { min:true, flavor:'parsed', docs:true, locations:true, messages }
+)
```
-
-
| Option | Description |
| ----------- | ------------------------------------------------------------ |
| `flavor` | By default the returned CSN is in `'inferred'` flavor, which is an effective model, with all aspects, includes, extensions and redirects applied and all views and projections inferred. Specify `'parsed'` to only have single models parsed. |
@@ -122,15 +133,22 @@ let csn = await cds.compile('*',{ min:true, docs:true })
| `locations` | Specify `true` to have the all `$location` properties preserved in serialized CSN. |
| `messages` | Pass an empty array to get all compiler messages collected in there. |
-
+Run the example. See that:
+- `messages` is filled with a compiler warning about the double comment,
+- `docs:true` leads to a `doc` field in the CSN for entity `Foo`,
+- `min:true` discards the unused type `T`,
+- `flavor:'parsed'` does not resolve `Bar`'s elements from `Foo`.
## cds. compile .to ... {.property}
Following are a collection of model processors which take a CSN as input and compile it to a target output. They can be used in two API flavors:
-```js
+```js live
+let csn = {definitions:{}}
let sql = cds.compile(csn).to.sql ({dialect:'sqlite'}) //> fluent
+```
+```js live
let sql = cds.compile.to.sql (csn,{dialect:'sqlite'}) //> direct
```
@@ -189,6 +207,7 @@ for (let [edm,{file,suffix}] of all)
Use [`cds.compile.to.hana`](#hana) instead.
### .hana() {.method}
+###### hana
Generates `hdbtable/hdbview` output.
diff --git a/node.js/cds-ql.md b/node.js/cds-ql.md
index 67d92763e..05896fce9 100644
--- a/node.js/cds-ql.md
+++ b/node.js/cds-ql.md
@@ -18,29 +18,28 @@ Module `cds.ql` provides facilities to construct queries in [*Core Query Notatio
1. Fluent API style, with query-by-example objects for where clauses and order by clauses:
-```js
+```js live
let q = SELECT.from('Books').where({ID:201}).orderBy({title:1})
```
2. Using with [tagged template literals (TTL)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates):
-```js
+```js live
let q = cds.ql `SELECT from Books where ID=${201} order by title`
```
3. Fluent API with interspersed [tagged template literals (TTL)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates):
-```js
+```js live
let q = SELECT.from `Books where ID=${201} order by title`
let p = SELECT.from `Books`.where`ID=${201}`.orderBy`title`
```
4. Manually constructing CQN objects:
-```js
+```js live
const { expr, ref, val, columns, expand, where, orderBy } = cds.ql
-```
-```js
+
let q = {
SELECT: {
from: ref`Books`,
@@ -49,7 +48,9 @@ let q = {
}
}
```
-```js
+```js live
+const { expr, ref, val, columns, expand, where, orderBy } = cds.ql
+
let q = {
SELECT: {
from: ref`Authors`,
@@ -78,7 +79,7 @@ const { SELECT, INSERT, UPDATE, DELETE } = cds.ql
It is recommended best practice to use entity definitions reflected from a service's model to construct queries. Doing so simplifies code as it avoids repeating namespaces all over the place.
-```js
+```js live
const { Books } = cds.entities
let q1 = SELECT.from (Books) .where `ID=${201}`
```
@@ -96,30 +97,32 @@ While both [CQL](../cds/cql) / [CQN](../cds/cqn) as well as the fluent API of `c
Queries are executed by passing them to a service's [`srv.run()`](core-services#srv-run-query) method, for example, to the primary database:
-```js
+```js live [async]
let query = SELECT `ID,title` .from `Books`
let books = await cds.db.run (query)
```
Alternatively, you can just `await` a constructed query, which by default passes the query to `cds.db.run()`. So, the following is equivalent to the above:
-```js
+```js live [async]
let books = await SELECT `ID,title` .from `Books`
```
Instead of a database service, you can also send queries to other services, local or remote ones. For example:
-```js
+```js live [async]
const cats = await cds.connect.to ('CatalogService')
+let query = SELECT `ID,title` .from `Books`
let books = await cats.run (query)
+return {query, books}
```
> `CatalogService` might be a remote service connected via OData. In this case, the query would be translated to an OData request sent via HTTP.
The APIs are also available through [`cds.Service`'s CRUD-style Convenience API](core-services#crud-style-api), for example:
-```js
+```js live [async]
const db = cds.db
-let books = await db.read`Books`.where`ID=${201}`.orderBy`title`
+await db.read`Books`.where`ID=${201}`.orderBy`title`
```
@@ -128,10 +131,10 @@ let books = await db.read`Books`.where`ID=${201}`.orderBy`title`
Constructing queries doesn't execute them immediately, but just captures the given query information. Very much like functions in JavaScript, queries are first-class objects, which can be assigned to variables, modified, passed as arguments, or returned from functions. Let's investigate this somewhat more, given this example:
-```js
-let cats = await cds.connect.to('CatalogService') //> connected via OData
-let PoesBooks = SELECT.from (Books) .where `name like '%Poe%'`
-let books = await cats.get (PoesBooks)
+```js live [async]
+cats = await cds.connect.to('CatalogService')//> connected via OData
+PoesBooks = SELECT.from ('Books') .where `author like '%Poe%'`
+books = await cats.get (PoesBooks)
```
This is what happens behind the scenes:
@@ -150,24 +153,26 @@ This is what happens behind the scenes:
You can also combine queries much like sub selects in SQL to form more complex queries as shown in this example:
-```sql
+```js live [async]
let input = '%Brontë%'
let Authors = SELECT `ID` .from `Authors` .where `name like ${ input }`
let Books = SELECT.from `Books` .where `author_ID in ${ Authors }`
-```
-```js
await cds.run (Books) //> late/no materialization of Authors
+// TODO fails with 'Authors not found'
```
With that we leverage late materialization, offered by SQL databases.
Compare that to inferior imperative programming:
-```js
+```js live [async]
let input = '%Brontë%'
let Authors = await SELECT `ID` .from `Authors` .where `name like ${ input }`
-for (let a of Authors) { //> looping over eagerly materialized Authors
- let Books = await SELECT.from `Books` .where `author_ID = ${ a.ID }`
-}
+// looping over eagerly materialized Auxthors
+let books = []
+for (let a of Authors) { books.push (
+ ...await SELECT.from `Books` .where `author_ID = ${ a.ID }`
+)}
+return books
```