@sap/cds-dk 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -9
- package/bin/bind.js +124 -8
- package/bin/cds-ts.js +1 -1
- package/bin/cds.js +1 -0
- package/bin/deploy/index.js +2 -1
- package/bin/env.js +50 -9
- package/bin/help.js +2 -1
- package/bin/import.js +126 -24
- package/bin/watch.js +45 -9
- package/lib/bind/bindingResolver.js +138 -0
- package/lib/bind/cfKeyProvider.js +353 -0
- package/lib/bind/userCdsrcJsonStorageProvider.js +144 -0
- package/lib/client/auth_manager.js +4 -4
- package/lib/client/settings_manager.js +1 -1
- package/lib/import/common.js +33 -1
- package/lib/import/importapi.js +4 -3
- package/lib/import/index.js +49 -23
- package/lib/import/message.js +11 -6
- package/lib/import/utils/MetadataConverter.js +25 -4
- package/lib/import/utils/MetadataConverterFactory.js +1 -2
- package/lib/import/utils/MultiMap.js +26 -0
- package/lib/import/v2parser.js +142 -24
- package/lib/import/v4parser.js +222 -149
- package/lib/init/constants.js +1 -1
- package/lib/init/index.js +2 -0
- package/lib/init/template/lint/files/.eslint/rules/no-entity-moo.js +36 -52
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/rule.test.js +15 -0
- package/lib/init/template/lint/index.js +14 -6
- package/lib/init/template/mta/files/mta.yaml.hbs +0 -7
- package/lib/init/template/mtx/files/xs-security.json.hbs +3 -1
- package/lib/init/template/mtx/package/dependencies.package.json +1 -1
- package/lib/init/template/nodejs/files/.gitignore.hbs +3 -0
- package/lib/init/util/commandUtil.js +5 -0
- package/lib/lint/Linter.js +31 -20
- package/lib/lint/checks.js +5 -1
- package/lib/lint/generators.js +1 -1
- package/lib/lint/io.js +59 -35
- package/lib/util/term.js +2 -0
- package/lib/watch/node-watch.js +38 -19
- package/npm-shrinkwrap.json +1276 -1102
- package/package.json +4 -3
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/test.js +0 -10
package/bin/watch.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
const watchOnlyOptions = ['--ext', '--livereload', '--open']
|
|
1
2
|
module.exports = Object.assign ( watch, {
|
|
2
3
|
flags: [ '--debug', '--in-memory', '--in-memory?' ],
|
|
3
|
-
options: [ '--port',
|
|
4
|
+
options: [ '--port', ...watchOnlyOptions ],
|
|
4
5
|
help: `
|
|
5
6
|
# SYNOPSIS
|
|
6
7
|
|
|
@@ -61,15 +62,16 @@ async function watch ([cwd], {
|
|
|
61
62
|
ext = extDefaults,
|
|
62
63
|
...options
|
|
63
64
|
}={}) {
|
|
64
|
-
|
|
65
65
|
args.push (`--${Object.keys(options).find(a => /^in-memory\??$/.test(a)) || 'in-memory?'}`)
|
|
66
|
-
args.push (...process.argv.slice(3)
|
|
67
|
-
|
|
66
|
+
args.push (...process.argv.slice(3)
|
|
67
|
+
.filter(removeArgsIn(watchOnlyOptions)) // remove all watch-only options, as `cds serve` would not accept them
|
|
68
|
+
.filter(a => a !== cwd))
|
|
69
|
+
|
|
70
|
+
let env = Object.assign (process.env, {
|
|
68
71
|
_args: JSON.stringify(args),
|
|
69
72
|
_options: JSON.stringify(options)
|
|
70
73
|
})
|
|
71
74
|
if (typeof ext !== 'string') ext = extDefaults
|
|
72
|
-
if ('port' in options) env.PORT = options.port
|
|
73
75
|
env.NODE_PATH = path.resolve (__dirname,'../node_modules') // allow global sqlite to be resolved
|
|
74
76
|
|
|
75
77
|
let liveReload //= ...
|
|
@@ -98,8 +100,12 @@ async function watch ([cwd], {
|
|
|
98
100
|
log (`watching: ${ext}...`)
|
|
99
101
|
if (liveReload) log (`live reload enabled for browsers`)
|
|
100
102
|
|
|
103
|
+
const bindingManager = new require('../lib/bind/bindingResolver').bindingManager({cwd})
|
|
104
|
+
const bindings = await bindingManager.resolve()
|
|
105
|
+
if (bindings) env = {...env, CDS_CONFIG: JSON.stringify(bindings)}
|
|
106
|
+
|
|
101
107
|
const nodemon = require('../lib/watch/node-watch')
|
|
102
|
-
|
|
108
|
+
const watcher = nodemon ({
|
|
103
109
|
watch: ['*','.env','.cdsrc.json'],
|
|
104
110
|
script:__filename, cwd, env, ext, ignore,
|
|
105
111
|
delay: parseInt(process.env.CDS_WATCH_DELAY) || 200,
|
|
@@ -109,12 +115,18 @@ async function watch ([cwd], {
|
|
|
109
115
|
if (liveReload) liveReload.markPending(files)
|
|
110
116
|
if (this.delayed) clearTimeout (this.delayed)
|
|
111
117
|
this.delayed = setTimeout(()=>{
|
|
118
|
+
bindingManager.update({
|
|
119
|
+
onBeforeUpdate: () => watcher.stop(),
|
|
120
|
+
onAfterUpdate: bindings => {
|
|
121
|
+
watcher.setEnv('CDS_CONFIG', bindings && JSON.stringify(bindings))
|
|
122
|
+
watcher.restart()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
112
125
|
log (`${t.bold} _______________________\n`)
|
|
113
126
|
FileHandlers.changed (files,cwd)
|
|
114
127
|
}, 111)
|
|
115
128
|
}).on('quit', ()=>{
|
|
116
129
|
log (`${t.bold+t.green}\n[cds] - my watch has ended.\n`)
|
|
117
|
-
process.exit()
|
|
118
130
|
}).on('message', async (msg) => { switch (msg.code) { // messages from child process
|
|
119
131
|
case 'listening':
|
|
120
132
|
env.PORT = (msg.address && msg.address.port) || msg.port
|
|
@@ -125,6 +137,8 @@ async function watch ([cwd], {
|
|
|
125
137
|
case 'EADDRINUSE': env.PORT = 0; _addr_in_use(msg); break
|
|
126
138
|
default: console.error (msg)
|
|
127
139
|
}})
|
|
140
|
+
|
|
141
|
+
return watcher
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
|
|
@@ -143,7 +157,20 @@ const FileHandlers = {
|
|
|
143
157
|
},
|
|
144
158
|
}
|
|
145
159
|
|
|
146
|
-
|
|
160
|
+
function removeArgsIn (toRemove) {
|
|
161
|
+
let argRemoved = false
|
|
162
|
+
return arg => { // filter function
|
|
163
|
+
if (toRemove.includes(arg)) {
|
|
164
|
+
argRemoved = true
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
else if (argRemoved) { // if next arg is a value, skip it as well
|
|
168
|
+
argRemoved = false
|
|
169
|
+
if (!arg.startsWith('-')) return false
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
147
174
|
|
|
148
175
|
//
|
|
149
176
|
// Child process impl launched by watch
|
|
@@ -153,8 +180,17 @@ if (!module.parent) (async()=>{ // launched by nodemon
|
|
|
153
180
|
// run through cli in order to use its error handling
|
|
154
181
|
const bootstrap = require('./cds')(...JSON.parse(process.env._args))
|
|
155
182
|
const {cds} = global
|
|
183
|
+
|
|
184
|
+
cds.once('listening', ({server, url}) => {
|
|
185
|
+
process.send ({ code:'listening', port: server.address().port })
|
|
186
|
+
// Need to give bindings an explicit call to purge,
|
|
187
|
+
// as its .exit handler has no chance to execute on Windows.
|
|
188
|
+
process.on ('message', (msg)=> { if (msg && msg.exit) { // see node-watch
|
|
189
|
+
(new cds.service.bindings(url)).purge()
|
|
190
|
+
}})
|
|
191
|
+
})
|
|
192
|
+
|
|
156
193
|
try {
|
|
157
|
-
cds.once('listening', ({server}) => process.send ({ code:'listening', port: server.address().port }))
|
|
158
194
|
await bootstrap
|
|
159
195
|
if (!cds.service.providers.length) return _no_services_found()
|
|
160
196
|
} catch (e) { switch (e.code) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const Config = require('@sap/cds/lib/env')
|
|
2
|
+
const cds = require('@sap/cds/lib');
|
|
3
|
+
const LOG = cds.log('bind');
|
|
4
|
+
|
|
5
|
+
function getUnresolvedBindings({cwd = process.cwd(), env}) {
|
|
6
|
+
if (!env) env = Config.for('cds', cwd)
|
|
7
|
+
if (feature(env) === false || feature(env) === "false") return {}
|
|
8
|
+
|
|
9
|
+
const requires = env.requires || {}
|
|
10
|
+
let bindings = {}
|
|
11
|
+
for (const name in requires) {
|
|
12
|
+
const service = requires[name]
|
|
13
|
+
if (service && service.binding && !service.binding.resolved) {
|
|
14
|
+
bindings[name] = {...service.binding, kind: service.kind }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return bindings
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function resolveBindings({bindings, cwd = process.cwd(), env, output = 'info', cache = {} }) {
|
|
22
|
+
const config = { requires: { } }
|
|
23
|
+
bindings = bindings || getUnresolvedBindings({cwd, env})
|
|
24
|
+
if (Object.keys(bindings).length===0) return config;
|
|
25
|
+
LOG[output](`resolving cloud service bindings...`)
|
|
26
|
+
await Promise.all( Object.keys(bindings).map( async name => {
|
|
27
|
+
const binding = bindings[name]
|
|
28
|
+
const cacheKey = JSON.stringify(binding)
|
|
29
|
+
if (!cache[cacheKey]) {
|
|
30
|
+
cache[cacheKey] = config.requires[name] = await resolveBinding(name, binding)
|
|
31
|
+
LOG[output](`bound ${name} to Cloud Foundry service instance ${binding.instance}`)
|
|
32
|
+
} else {
|
|
33
|
+
config.requires[name] = cache[cacheKey]
|
|
34
|
+
}
|
|
35
|
+
} ));
|
|
36
|
+
|
|
37
|
+
// Add kind to result, so that it can be applied to old @sap/cds versions that do not read `.cdsrc-private.json`
|
|
38
|
+
for (const name in config.requires) {
|
|
39
|
+
if (bindings[name].kind) config.requires[name].kind = bindings[name].kind
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cfKeyProvider
|
|
46
|
+
|
|
47
|
+
async function resolveBinding(name, binding) {
|
|
48
|
+
switch (binding.type) {
|
|
49
|
+
case 'cf':
|
|
50
|
+
if (!cfKeyProvider) {
|
|
51
|
+
cfKeyProvider = require('./cfKeyProvider');
|
|
52
|
+
cfKeyProvider.setLogger(LOG);
|
|
53
|
+
}
|
|
54
|
+
return cfKeyProvider.resolve(name, binding);
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported binding type "${binding.type}" for service "${name}".`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function applyTo(options) {
|
|
61
|
+
const config = await resolveBindings(options)
|
|
62
|
+
if (!config) return
|
|
63
|
+
const {env} = options
|
|
64
|
+
for (const name in config.requires) {
|
|
65
|
+
const service = config.requires[name]
|
|
66
|
+
const { binding, credentials } = service
|
|
67
|
+
Object.assign(env.requires[name], { binding, credentials })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Config._emulate_vcap_services) {
|
|
71
|
+
Config._emulate_vcap_services()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class BindingManager {
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.cache = {}
|
|
78
|
+
this.bindingsStr = ''
|
|
79
|
+
this.options = options
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getBindings() {
|
|
83
|
+
const bindings = getUnresolvedBindings(this.options)
|
|
84
|
+
return bindings
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async resolve(bindings) {
|
|
88
|
+
if (!bindings) {
|
|
89
|
+
bindings = this.getBindings(this.options)
|
|
90
|
+
this.bindingsStr = JSON.stringify(bindings)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const config = await resolveBindings({...this.options, bindings, cache: this.cache})
|
|
94
|
+
if (config && Object.keys(config.requires || {}).length > 0) return config;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async update({ onBeforeUpdate, onAfterUpdate } = {}) {
|
|
98
|
+
const oldBindingsStr = this.bindingsStr
|
|
99
|
+
const bindings = this.getBindings()
|
|
100
|
+
const bindingsStr = JSON.stringify(bindings)
|
|
101
|
+
this.bindingsStr = bindingsStr
|
|
102
|
+
|
|
103
|
+
if (this.bindingsStr === oldBindingsStr) return // No change
|
|
104
|
+
if (onBeforeUpdate) await onBeforeUpdate()
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (this.pendingUpdate) await this.pendingUpdate
|
|
108
|
+
} catch(error) {
|
|
109
|
+
// ignore
|
|
110
|
+
}
|
|
111
|
+
this.pendingUpdate = undefined
|
|
112
|
+
|
|
113
|
+
if (this.bindingsStr !== bindingsStr) return // Next update in progress
|
|
114
|
+
|
|
115
|
+
let resolvedBindings
|
|
116
|
+
try {
|
|
117
|
+
this.pendingUpdate = this.resolve(bindings)
|
|
118
|
+
resolvedBindings = await this.pendingUpdate
|
|
119
|
+
} catch(error) {
|
|
120
|
+
LOG.error(error.message)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.bindingsStr !== bindingsStr) return // Next update in progress
|
|
125
|
+
if (onAfterUpdate) onAfterUpdate(resolvedBindings)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function feature(env) {
|
|
130
|
+
return env.features && env.features.dk && env.features.dk.resolveBindingsForTesting
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
applyTo,
|
|
135
|
+
bindingManager: function(options) {
|
|
136
|
+
return new BindingManager(options)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const cp = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const util = require('util');
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
|
|
8
|
+
const execAsync = util.promisify(cp.exec);
|
|
9
|
+
const IS_WIN = os.platform() === 'win32';
|
|
10
|
+
|
|
11
|
+
const KindToRequiresNameMap = {
|
|
12
|
+
xsuaa: 'uaa',
|
|
13
|
+
hana: 'db',
|
|
14
|
+
destinations: 'destinations',
|
|
15
|
+
connectivity: 'connectivity',
|
|
16
|
+
'audit-log': 'audit-log',
|
|
17
|
+
'enterprise-messaging': 'messaging',
|
|
18
|
+
'enterprise-messaging-amqp': 'messaging',
|
|
19
|
+
'enterprise-messaging-http': 'messaging',
|
|
20
|
+
'enterprise-messaging-shared': 'messaging'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CFKeyProvider {
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.logger = console;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async _cfRead(...args) {
|
|
30
|
+
this.logger.debug(`_cfRead: cf ${args}`);
|
|
31
|
+
|
|
32
|
+
const result = await execAsync(`cf ${args}`, {
|
|
33
|
+
shell: IS_WIN,
|
|
34
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return result.stdout.toString().trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async _req(target, urlPath) {
|
|
41
|
+
const url = new URL(urlPath, target.apiEndpoint);
|
|
42
|
+
|
|
43
|
+
this.logger.debug(`_req: ${url.toString()}`);
|
|
44
|
+
|
|
45
|
+
const result = await axios.get(url.toString(), {
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: target.authorization
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
extract(string, pattern, defaultValue) {
|
|
55
|
+
const array = string.split(/[\n\r]+/);
|
|
56
|
+
for (let line of array) {
|
|
57
|
+
const match = line.match(pattern);
|
|
58
|
+
if (match) {
|
|
59
|
+
return match[1];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof defaultValue === 'undefined') {
|
|
64
|
+
throw Error(`Pattern not found: ${pattern}`);
|
|
65
|
+
} else {
|
|
66
|
+
return defaultValue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getCfTargetFromCli() {
|
|
71
|
+
const result = await this._cfRead('target');
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
apiEndpoint: this.extract(result, /^api endpoint:\s+(.*)$/i),
|
|
75
|
+
user: this.extract(result, /^user:\s+(.*)$/i),
|
|
76
|
+
org: this.extract(result, /^org:\s+(.*)$/i),
|
|
77
|
+
space: this.extract(result, /^space:\s+(.*)$/i),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getCfSpaceInfo() {
|
|
82
|
+
this.logger.debug('getting space info');
|
|
83
|
+
|
|
84
|
+
const target = await this.getCfTargetFromCli();
|
|
85
|
+
const authorization = await this.getCfAuthorization();
|
|
86
|
+
const targetWithAuth = Object.assign({}, target, { authorization });
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
const { org, space } = target;
|
|
90
|
+
const orgs = await this._req(targetWithAuth, `/v3/organizations?names=${org}`);
|
|
91
|
+
if (!(orgs && orgs.resources.length === 1)) {
|
|
92
|
+
throw new Error(`CF org ${org} not found!`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const orgGuid = orgs.resources[0].guid;
|
|
96
|
+
const spaces = await this._req(targetWithAuth, `/v3/spaces?names=${space}&organization_guids=${orgGuid}`)
|
|
97
|
+
if (!(spaces && spaces.resources.length === 1)) {
|
|
98
|
+
throw new Error(`CF space ${space} not found in org ${org}!`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const spaceGuid = spaces.resources[0].guid;
|
|
102
|
+
|
|
103
|
+
return Object.assign({}, target, { orgGuid, spaceGuid, authorization });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getCfAuthorization() {
|
|
107
|
+
this.logger.debug('getting authorization');
|
|
108
|
+
return this._cfRead('oauth-token');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setLogger(logger) {
|
|
112
|
+
this.logger = logger;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async init() {
|
|
116
|
+
const target = this.target = await this.getCfSpaceInfo();
|
|
117
|
+
const logger = this.logger;
|
|
118
|
+
this.getOrg = BatchRequest.create({logger, target, url: '/v3/organizations'});
|
|
119
|
+
this.getSpace = BatchRequest.create({logger, target, url: '/v3/spaces', extractors: {
|
|
120
|
+
organization_guids: resource => resource.relationships.organization.data.guid
|
|
121
|
+
}});
|
|
122
|
+
this.getInstance = BatchRequest.create({logger, target, url: '/v3/service_instances', extractors: {
|
|
123
|
+
space_guids: resource => resource.relationships.space.data.guid
|
|
124
|
+
}});
|
|
125
|
+
this.getKey = BatchRequest.create({logger, target, url: '/v3/service_credential_bindings', extractors: {
|
|
126
|
+
service_instance_guids: resource => resource.relationships.service_instance.data.guid
|
|
127
|
+
}});
|
|
128
|
+
this.getPlans = BatchRequest.create({logger, target, url: '/v3/service_plans' });
|
|
129
|
+
this.getOfferings = BatchRequest.create({logger, target, url: '/v3/service_offerings' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async resolve(name, binding) {
|
|
133
|
+
if (!this.initPromise) {
|
|
134
|
+
this.initPromise = this.init();
|
|
135
|
+
}
|
|
136
|
+
await this.initPromise;
|
|
137
|
+
|
|
138
|
+
const target = this.target;
|
|
139
|
+
|
|
140
|
+
let {instance, key, apiEndpoint, org, space} = binding;
|
|
141
|
+
key = key || `${instance}-key`;
|
|
142
|
+
name = name || `${instance}:${key}`;
|
|
143
|
+
|
|
144
|
+
if (apiEndpoint && apiEndpoint !== target.apiEndpoint) {
|
|
145
|
+
throw new Error(`Current Cloud Foundry api endpoint "${target.apiEndpoint}" doesn't match with requested api endpoint "${apiEndpoint}" for service binding "${name}".`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let spaceGuid;
|
|
149
|
+
if (org && space) {
|
|
150
|
+
const orgObj = await this.getOrg({ names: org });
|
|
151
|
+
const spaceObj = await this.getSpace({ names: space, organization_guids: orgObj.guid });
|
|
152
|
+
spaceGuid = spaceObj.guid;
|
|
153
|
+
} else {
|
|
154
|
+
org = target.org;
|
|
155
|
+
space = target.space;
|
|
156
|
+
spaceGuid = target.spaceGuid;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let instanceObj;
|
|
160
|
+
try {
|
|
161
|
+
instanceObj = await this.getInstance({ names: instance, space_guids: spaceGuid });
|
|
162
|
+
} catch(error) {
|
|
163
|
+
throw new Error(`Service instance "${instance}" not found.`);
|
|
164
|
+
}
|
|
165
|
+
let keyObj;
|
|
166
|
+
try {
|
|
167
|
+
keyObj = await this.getKey({names: key, service_instance_guids: instanceObj.guid });
|
|
168
|
+
} catch(error) {
|
|
169
|
+
throw new Error(`No service key "${key}" found for service instance "${instance}".`);
|
|
170
|
+
}
|
|
171
|
+
const planObj = await this.getPlans({guids: instanceObj.relationships.service_plan.data.guid});
|
|
172
|
+
const offeringObj = await this.getOfferings({guids: planObj.relationships.service_offering.data.guid });
|
|
173
|
+
const keyCredentialsObj = await req(this.logger, target, `/v3/service_credential_bindings/${encodeURIComponent(keyObj.guid)}/details`);
|
|
174
|
+
const resolvedBinding = {
|
|
175
|
+
binding: {
|
|
176
|
+
type: "cf",
|
|
177
|
+
apiEndpoint: target.apiEndpoint,
|
|
178
|
+
org: org,
|
|
179
|
+
space: space,
|
|
180
|
+
instance: instance,
|
|
181
|
+
key: key,
|
|
182
|
+
vcap: {
|
|
183
|
+
label: offeringObj.name,
|
|
184
|
+
plan: planObj.name,
|
|
185
|
+
},
|
|
186
|
+
resolved: true,
|
|
187
|
+
},
|
|
188
|
+
credentials: keyCredentialsObj.credentials
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return resolvedBinding;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getRequiresNameFromKind(service, kind) {
|
|
195
|
+
const requiresName = KindToRequiresNameMap[kind];
|
|
196
|
+
if (!requiresName) throw new Error(`Unknown CDS service name for service "${service}". Please specify as argument for "cds bind".`)
|
|
197
|
+
return requiresName;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Determine kind value for services based on VCAP binding
|
|
202
|
+
* information in the configuration.
|
|
203
|
+
*/
|
|
204
|
+
determineKinds(services) {
|
|
205
|
+
const vcapServices = {}
|
|
206
|
+
|
|
207
|
+
const getIndex = service => {
|
|
208
|
+
const { credentials } = service;
|
|
209
|
+
return credentials && credentials.binding && credentials.binding.index;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Build VCAP_SERVICES
|
|
213
|
+
for (let index = 0; index<services.length; index++) {
|
|
214
|
+
const service = services[index];
|
|
215
|
+
const { label, name, tags } = service.binding.vcap;
|
|
216
|
+
if (!vcapServices[label]) vcapServices[label] = [];
|
|
217
|
+
vcapServices[label].push({ credentials: { binding: { index } }, label, name, tags });
|
|
218
|
+
|
|
219
|
+
service.kindCandidates = [ ]
|
|
220
|
+
service.nameCandidates = [ ]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Auto configure services based on VCAP_SERVICES
|
|
224
|
+
const cds = require('@sap/cds/lib');
|
|
225
|
+
const VCAP_SERVICES = process.env.VCAP_SERVICES;
|
|
226
|
+
try {
|
|
227
|
+
process.env.VCAP_SERVICES = JSON.stringify(vcapServices);
|
|
228
|
+
const env = cds.env.for('cds', '.');
|
|
229
|
+
|
|
230
|
+
// Extract "kind" from auto configured services
|
|
231
|
+
for (const kind of Object.keys(env.requires)) {
|
|
232
|
+
const requireService = env.requires[kind];
|
|
233
|
+
const superKind = requireService.kind;
|
|
234
|
+
const index = getIndex(requireService);
|
|
235
|
+
if (!Number.isInteger(index)) continue;
|
|
236
|
+
|
|
237
|
+
const superRequireService = env.requires[superKind];
|
|
238
|
+
if (superRequireService) {
|
|
239
|
+
const superIndex = getIndex(superRequireService);
|
|
240
|
+
if (index === superIndex) continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
services[index].kindCandidates.push(kind);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if one "kind" per service was found
|
|
247
|
+
for (const service of services) {
|
|
248
|
+
if (service.kindCandidates.length !== 1) {
|
|
249
|
+
throw new Error(`Unknown kind for service "${service.binding.instance}". Please specify with "--kind" option.`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
service.kind = service.kindCandidates[0];
|
|
253
|
+
delete service.kindCandidates;
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
process.env.VCAP_SERVICES = VCAP_SERVICES;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
class BatchRequest {
|
|
262
|
+
constructor({logger, target, url, extractors = {}}) {
|
|
263
|
+
this.logger = logger;
|
|
264
|
+
this.target = target;
|
|
265
|
+
this.url = url;
|
|
266
|
+
this.filters = [];
|
|
267
|
+
this.timeout = undefined;
|
|
268
|
+
this.extractors = Object.assign({ names: resource => resource.name, guids: resource => resource.guid }, extractors);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static create(options) {
|
|
272
|
+
const obj = new BatchRequest(options);
|
|
273
|
+
return (...args) => obj.request(...args);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async request(filter) {
|
|
277
|
+
let resolve;
|
|
278
|
+
let reject;
|
|
279
|
+
const promise = new Promise( (_resolve, _reject) => {
|
|
280
|
+
resolve = _resolve;
|
|
281
|
+
reject = _reject;
|
|
282
|
+
});
|
|
283
|
+
this.filters.push({ filter, resolve, reject });
|
|
284
|
+
|
|
285
|
+
if (this.timeout) clearTimeout(this.timeout);
|
|
286
|
+
this.timeout = setTimeout(() => this.executeRequest(), 0);
|
|
287
|
+
|
|
288
|
+
return promise;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async executeRequest() {
|
|
292
|
+
const filters = this.filters;
|
|
293
|
+
this.filters = [];
|
|
294
|
+
const filterParams = {};
|
|
295
|
+
for (const filter of filters) {
|
|
296
|
+
for (const name of Object.keys(filter.filter)) {
|
|
297
|
+
if (!filterParams[name]) filterParams[name] = [];
|
|
298
|
+
filterParams[name].push(filter.filter[name]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const params = Object.keys(filterParams).map(
|
|
303
|
+
name => `${name}=${filterParams[name].map( value => encodeURIComponent(value) ).join(',')}`
|
|
304
|
+
).join("&");
|
|
305
|
+
|
|
306
|
+
const url = this.url + (this.url.match(/\?/) ? '&' : '?') + params;
|
|
307
|
+
|
|
308
|
+
let data;
|
|
309
|
+
try {
|
|
310
|
+
data = await this.httpRequest(url);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
for (const filter of filters) {
|
|
313
|
+
filter.reject(error);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const fmt = filter => Object.keys(filter).map( key => `${key}: ${filter[key]}` ).join(', ');
|
|
318
|
+
const resources = data.resources;
|
|
319
|
+
for (const filter of filters) {
|
|
320
|
+
let filteredResources = resources;
|
|
321
|
+
for (const name of Object.keys(filter.filter)) {
|
|
322
|
+
filteredResources = filteredResources.filter( resource => this.extractors[name](resource) === filter.filter[name] );
|
|
323
|
+
}
|
|
324
|
+
if (filteredResources.length === 1) {
|
|
325
|
+
filter.resolve(filteredResources[0]);
|
|
326
|
+
} else if (filteredResources.length === 0) {
|
|
327
|
+
filter.reject(new Error(`Couldn't find resource for url "${this.url}" with filter "${fmt(filter.filter)}"`));
|
|
328
|
+
} else {
|
|
329
|
+
filter.reject(new Error(`Found more than one resources for url "${this.url}" with filter "${fmt(filter.filter)}"`));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async httpRequest(urlPath) {
|
|
335
|
+
return req(this.logger, this.target, urlPath);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function req(logger, target, urlPath) {
|
|
340
|
+
const url = new URL(urlPath, target.apiEndpoint);
|
|
341
|
+
|
|
342
|
+
logger.debug(`_req: ${url.toString()}`);
|
|
343
|
+
|
|
344
|
+
const result = await axios.get(url.toString(), {
|
|
345
|
+
headers: {
|
|
346
|
+
authorization: target.authorization
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return result.data;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = new CFKeyProvider();
|