@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
129 lines (114 loc) • 3.83 kB
JavaScript
const cds = require ('..')
const LOG = cds.log('serve|bindings',{label:'cds'})
const registry = '~/.cds-services.json'
const fs = require('fs')
class Bindings {
provides = {}
servers = {}
#bound = {}
then (r,e) {
delete Bindings.prototype.then // only once per process
cds.prependOnceListener ('connect', ()=> LOG.info ('connect using bindings from:', { registry }))
cds.once('listening', server => this.export (cds.service.providers, server.url))
return this.import() .then (r,e)
}
bind (service) {
let required = cds.requires [service]
let binding = this.provides [required?.service || service]
if (binding?.endpoints) {
const server = this.servers [binding.server]
const kind = [ required.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
const path = binding.endpoints [kind]
// in case of cds.requires.Foo = { ... }
if (typeof required === 'object') required.credentials = {
...required.credentials,
...binding.credentials,
url: server.url + path
}
// in case of cds.requires.Foo = true
else required = cds.requires[service] = cds.env.requires[service] = {
...cds.requires.kinds [binding.kind],
credentials: {
...binding.credentials,
url: server.url + path
}
}
required.kind = kind
// REVISIT: temporary fix to inherit kind as well for mocked odata services
// otherwise mocking with two services does not work for kind:odata-v2
if (kind === 'odata-v2' || kind === 'odata-v4') required.kind = 'odata'
}
return required
}
// used by cds.connect
at (service) {
return this.#bound [service] ??= this.bind (service)
}
get registry() {
return Bindings.registry ??= registry.replace(/^~/, require('os').homedir())
}
async load (read = fs.promises.readFile) {
LOG.debug ('reading bindings from:', registry)
try {
let src = read (this.registry)
let {cds} = JSON.parse (src.then ? await src : src)
Object.assign (this, cds)
}
catch { /* ignored */ }
return this
}
async store (write = fs.promises.writeFile) {
LOG.debug ('writing bindings to:', registry)
const json = JSON.stringify ({ cds: this },null,' ')
return write (this.registry, json)
}
async import() {
await this.load()
for (let each in cds.requires) this.bind (each)
return this
}
async export (services, url) {
this.cleanup (url)
const { servers, provides } = this, { pid } = process
// register our server
servers[pid] = {
root: 'file://' + cds.root,
url
}
// register our services
for (let srv of services) {
// if (each.name in cds.env.requires) continue
provides[srv.name] = {
endpoints: Object.fromEntries (srv.endpoints.map (ep => [ ep.kind, ep.path ])),
server: pid,
}
}
process.on ('exit', ()=> this.purge())
cds.on ('shutdown', ()=> this.purge())
return this.store()
}
purge() {
if (this.done) return;
this.load (fs.readFileSync)
LOG.debug ('purging bindings from:', registry)
this.cleanup()
this.store (fs.writeFileSync)
this.done = true
}
/**
* Remove all services served by this server or at the given url.
*/
cleanup (url) {
const { servers, provides } = this, { pid } = process
for (let [key,srv] of Object.entries (provides))
if (srv.server === pid || url && srv.credentials?.url?.startsWith(url)) delete provides [key]
delete servers [pid.toString()]
return this
}
}
const {NODE_ENV} = process.env
if (NODE_ENV === 'test' || global.it || cds.env.no_bindings) {
Object.defineProperty (module, 'exports', { value: { at: ()=> undefined }})
} else {
module.exports = new Bindings
}