diff --git a/.gitignore b/.gitignore
index 2359260..6af6f9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,10 @@ gen/
*.sqlite-shm
*.sqlite-wal
.vscode/
+
+**/gen/
+**/edmx/
+**/target/
+.flattened-pom.xml
+schema*.sql
+*.log*
diff --git a/lib/cds-test.js b/lib/cds-test.js
index a9f8584..b8fc093 100644
--- a/lib/cds-test.js
+++ b/lib/cds-test.js
@@ -24,15 +24,21 @@ class Test extends require('./axios') {
default: this.in(folder_or_cmd); args.push('--in-memory?')
}
const {cds} = this
+ const self = this
// launch cds server...
- before (async ()=>{
+ before (async function () {
process.env.cds_test_temp = cds.utils.path.resolve(cds.root,'_out',''+process.pid)
if (!args.includes('--port')) args.push('--port', '0')
+ if (cds.env.profiles.indexOf('java') > -1 && cds.env.profiles.indexOf('node') < 0) {
+ this.timeout?.(30000)
+ cds.exec
+ cds.exec = require('./java').bind(self)
+ }
let { server, url } = await cds.exec(...args)
- this.server = server
- this.url = url
- })
+ self.server = server
+ self.url = url
+ }, 30000)
// gracefully shutdown cds server...
after (async ()=>{
@@ -208,8 +214,8 @@ let _expect = undefined
} else if (is_jest) { // it's jest
- global.before = (msg,fn) => { return global.beforeAll(fn||msg) }
- global.after = (msg,fn) => global.afterAll(fn||msg)
+ global.before = (msg,fn,time) => global.beforeAll(typeof fn === 'function' ? fn : msg,time||fn)
+ global.after = (msg,fn,time) => global.afterAll(typeof fn === 'function' ? fn : msg,time||fn)
} else { // it's node --test
@@ -217,7 +223,8 @@ let _expect = undefined
describe.each = test.each = describe.skip.each = test.skip.each = each
global.describe = describe; global.xdescribe = describe.skip
global.test = global.it = test; global.xtest = test.skip
- global.beforeAll = global.before = (msg,fn=msg) => {
+ global.beforeAll = global.before = (msg,fn) => {
+ if (typeof msg === 'function') fn = msg
if (fn.length > 0) { const f = fn; fn = (_,done)=>f(done) }
return before(fn) // doesn't work for some reason
}
diff --git a/lib/java-hcql.js b/lib/java-hcql.js
new file mode 100644
index 0000000..9cf8251
--- /dev/null
+++ b/lib/java-hcql.js
@@ -0,0 +1,42 @@
+const cds = require('@sap/cds')
+
+const InsertResults = require('@cap-js/db-service/lib/InsertResults')
+
+module.exports = class extends cds.Service {
+ init() {
+ this.on('*', async req => {
+ const { axios } = this.options
+
+ // REVISIT: make draft and text direct access work
+ if (
+ req.target.isDraft ||
+ req.target.includes?.includes('sap.common.TextsAspect')
+ ) return []
+
+ if (req.query.INSERT?.rows) {
+ req.query.INSERT.entries = req.query.INSERT?.rows
+ .map(r => req.query.INSERT.columns.reduce((l, c, i) => {
+ l[c] = r[i]
+ return l
+ }, {}))
+ req.query.INSERT.rows = undefined
+ }
+
+ const service = cds.model.services[req.path.split('.')[0]]
+ if (!service) {
+ const sub = req.query[req.query.kind]
+ const ref = sub.from || sub.into || sub.entity
+ if (!ref) { debugger }
+ if (ref.ref[0].id) ref.ref[0].id = 'db.' + ref.ref[0].id
+ else ref.ref[0] = 'db.' + ref.ref[0]
+ }
+ const res = await axios.post('/hcql/' + (service?.['@path'] ?? 'db'), req.query, { headers: { 'content-type': 'application/json' } })
+
+ // Convert HCQL result format to @cap-js/db-service compliant results
+ if (req.query.SELECT) return req.query.SELECT?.one ? res.data.data[0] : res.data.data
+ if (req.query.INSERT) return new InsertResults(req.query, res.data.data)
+ return res.data.rowCounts.reduce((l, c) => l + c)
+ })
+ }
+ url4() { return 'Java Proxy' }
+}
\ No newline at end of file
diff --git a/lib/java.js b/lib/java.js
new file mode 100644
index 0000000..4c80ba1
--- /dev/null
+++ b/lib/java.js
@@ -0,0 +1,82 @@
+const childProcess = require('child_process')
+const { setTimeout } = require('node:timers/promises')
+
+module.exports = async function java(...args) {
+ const { cds } = this
+ const { fs: { promises: fs }, path } = cds.utils
+ const srv = path.resolve(cds.root, cds.env.folders.srv)
+
+ // forces java to respond @odata.context and @odata.count just like the node runtime
+ this.axios.defaults.headers.common['Odata-Version'] = '4.0'
+
+ cds.env.requires.db = { impl: require.resolve('./java-hcql.js'), axios: this.axios }
+
+ const [_, options] = require('@sap/cds/bin/args')(require('@sap/cds/bin/serve'), args)
+
+ // load application model
+ const from = [...(options.from?.split(',') ?? ['*'])]
+ const model = await cds.load(from)
+ if (model.definitions.db) {
+ // link test enviroment with application linked model
+ cds.model = cds.linked(model)
+ } else {
+ // enhance java model with database hcql service
+ const db = { ...model, definitions: { db: { kind: 'service', '@path': 'db', '@protocol': ['hcql'], '@requires': 'any' } } }
+ const services = []
+ for (const name in model.definitions) {
+ const def = model.definitions[name]
+ if (def.kind === 'service') services.push(name)
+ if (def.kind !== 'entity') continue
+ if (services.find(s => name.startsWith(s))) continue
+ db.definitions['db.' + name] = { "kind": "entity", "projection": { "from": { "ref": [name] } } }
+ }
+ await Promise.all([
+ fs.writeFile(path.resolve(srv, 'db.cds'), `using from './db.json';`),
+ fs.writeFile(path.resolve(srv, 'db.json'), JSON.stringify(db)),
+ ])
+
+ // link test enviroment with application linked model
+ cds.model = cds.linked(await cds.load([...from, path.resolve(srv, 'db.cds')]))
+ }
+
+ let res, rej
+ const ready = new Promise((resolve, reject) => {
+ res = resolve
+ rej = reject
+ })
+
+ const p = await port()
+ const url = `http://localhost:${p}`
+ const app = childProcess.spawn('mvn', ['spring-boot:run', `-Dspring-boot.run.arguments=--server.port=${p}`], { cwd: cds.root, stdio: 'inherit', env: process.env })
+ app.on('error', rej)
+ app.on('exit', () => rej(new Error('Application failed to start.')))
+ cds.shutdown = () => Promise.all([
+ app.kill(),
+ fs.rm(path.resolve(srv, 'db.cds')),
+ fs.rm(path.resolve(srv, 'db.json')),
+ ])
+
+ // REVISIT: make it call an actual /health check
+ // ping the server until it responds
+ const ping = () => cds.test.axios.get(url).catch(() => ping())
+ ping().then(res)
+ await ready
+
+ // connect to primary database hcql proxy service
+ await cds.connect()
+
+ return { server: { address: () => { return p } }, url }
+}
+
+function port() {
+ return new Promise((resolve, reject) => {
+ const net = require('net')
+ const server = net.createServer()
+ server.on('error', reject)
+
+ server.listen(() => {
+ const { port } = server.address()
+ server.close(() => resolve(port))
+ })
+ })
+}
\ No newline at end of file
diff --git a/test/app/pom.xml b/test/app/pom.xml
new file mode 100644
index 0000000..1b2c7e6
--- /dev/null
+++ b/test/app/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+ customer
+ app-parent
+ ${revision}
+ pom
+
+ app parent
+
+
+
+ 1.0.0-SNAPSHOT
+
+
+ 21
+ 3.2.0
+ 3.3.3
+ 8.1.2
+
+ https://nodejs.org/dist/
+ UTF-8
+
+
+
+ srv
+
+
+
+
+
+
+ com.sap.cds
+ cds-services-bom
+ ${cds.services.version}
+ pom
+ import
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring.boot.version}
+ pom
+ import
+
+
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+ ${jdk.version}
+ UTF-8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+ true
+
+
+
+
+
+ maven-surefire-plugin
+ 3.3.1
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.6.0
+
+ true
+ resolveCiFriendliesOnly
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+
+
+ maven-enforcer-plugin
+ 3.5.0
+
+
+ Project Structure Checks
+
+ enforce
+
+
+
+
+ 3.6.3
+
+
+ ${jdk.version}
+
+
+
+ true
+
+
+
+
+
+
+
diff --git a/test/app/srv/admin-service.cds b/test/app/srv/admin-service.cds
index 6a56af6..0adfe86 100644
--- a/test/app/srv/admin-service.cds
+++ b/test/app/srv/admin-service.cds
@@ -1,6 +1,6 @@
using { sap.capire.bookshop as my } from '../db/schema';
-@path: '/admin'
+@path: 'admin'
service AdminService {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
diff --git a/test/app/srv/cat-service.cds b/test/app/srv/cat-service.cds
index bd493e1..49d5647 100644
--- a/test/app/srv/cat-service.cds
+++ b/test/app/srv/cat-service.cds
@@ -1,4 +1,6 @@
using { sap.capire.bookshop as my } from '../db/schema';
+
+@path: 'catalog'
service CatalogService {
/** For displaying lists of Books */
diff --git a/test/app/srv/draft-service.cds b/test/app/srv/draft-service.cds
index 92b0fea..7a7f049 100644
--- a/test/app/srv/draft-service.cds
+++ b/test/app/srv/draft-service.cds
@@ -1,6 +1,6 @@
using { sap.capire.bookshop as my } from '../db/schema';
-@path: '/draft'
+@path: 'draft'
service DraftService {
@odata.draft.enabled
entity Books as projection on my.Books;
diff --git a/test/app/srv/pom.xml b/test/app/srv/pom.xml
new file mode 100644
index 0000000..3b01dd0
--- /dev/null
+++ b/test/app/srv/pom.xml
@@ -0,0 +1,125 @@
+
+ 4.0.0
+
+
+ app-parent
+ customer
+ ${revision}
+
+
+ app
+ jar
+
+ app
+
+
+
+
+
+ com.sap.cds
+ cds-starter-spring-boot
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.sap.cds
+ cds-adapter-odata-v4
+ runtime
+
+
+
+ com.sap.cds
+ cds-adapter-hcql
+ runtime
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+ ${project.artifactId}
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+ false
+
+
+
+ repackage
+
+ repackage
+
+
+ exec
+
+
+
+
+
+
+
+ com.sap.cds
+ cds-maven-plugin
+ ${cds.services.version}
+
+
+ cds.clean
+
+ clean
+
+
+
+
+ cds.resolve
+
+ resolve
+
+
+
+
+ cds.build
+
+ cds
+
+
+
+ build --for java
+ deploy --to h2 --dry > "${project.basedir}/src/main/resources/schema-h2.sql"
+
+
+
+
+
+ cds.generate
+
+ generate
+
+
+ cds.gen
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/app/srv/src/main/java/customer/app/Application.java b/test/app/srv/src/main/java/customer/app/Application.java
new file mode 100644
index 0000000..37cd623
--- /dev/null
+++ b/test/app/srv/src/main/java/customer/app/Application.java
@@ -0,0 +1,13 @@
+package customer.app;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/test/app/srv/src/main/resources/application.yaml b/test/app/srv/src/main/resources/application.yaml
new file mode 100644
index 0000000..91bf0f3
--- /dev/null
+++ b/test/app/srv/src/main/resources/application.yaml
@@ -0,0 +1,45 @@
+cds:
+ security:
+ # Match mock users with default node mock users
+ mock:
+ users:
+ - name: alice
+ tenant: t1
+ roles:
+ - admin
+ - name: bob
+ tenant: t1
+ roles:
+ - builder
+ - name: carol
+ tenant: t1
+ roles:
+ - admin
+ - builder
+ - name: dave
+ tenant: t1
+ features:
+ roles:
+ - admin
+ - name: erin
+ tenant: t2
+ roles:
+ - admin
+ - builder
+ - name: fred
+ tenant: t2
+ features:
+ - isbn
+ - name: me
+ tenant: t1
+ features:
+ - "*"
+ - name: yves
+ roles:
+ - internal-user
+---
+spring:
+ config.activate.on-profile: default
+ sql.init.schema-locations: classpath:schema-h2.sql
+cds:
+ data-source.auto-config.enabled: false
diff --git a/test/app/test/sample-bookshop.test.js b/test/app/test/sample-bookshop.test.js
index 68c629e..821ebde 100644
--- a/test/app/test/sample-bookshop.test.js
+++ b/test/app/test/sample-bookshop.test.js
@@ -2,11 +2,17 @@ const cds_test = require ('../../..')
const describe = global.describe ?? require('node:test').describe
describe('Sample tests', () => {
- const { GET, expect } = cds_test (__dirname+'/..')
+ const { GET, expect, cds } = cds_test (__dirname+'/..')
it('serves Books', async () => {
- const { data } = await GET `/odata/v4/catalog/Books`
+ const { data } = await GET`/odata/v4/catalog/Books`
expect(data.value.length).to.be.greaterThanOrEqual(5)
})
+ it('database Books', async () => {
+ const { Books } = cds.entities('sap.capire.bookshop')
+ const data = await cds.ql`SELECT ID FROM ${Books}`
+ expect(data.length).to.be.greaterThanOrEqual(5)
+ })
+
})
\ No newline at end of file