@sap/cds 9.2.1 → 9.3.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 +77 -1
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fr.properties +2 -2
- package/_i18n/messages.properties +6 -0
- package/app/index.js +0 -1
- package/bin/deploy.js +1 -1
- package/bin/serve.js +7 -20
- package/lib/compile/cdsc.js +3 -0
- package/lib/compile/for/flows.js +102 -0
- package/lib/compile/for/nodejs.js +28 -0
- package/lib/compile/to/edm.js +11 -4
- package/lib/core/classes.js +1 -1
- package/lib/core/linked-csn.js +8 -0
- package/lib/dbs/cds-deploy.js +12 -12
- package/lib/env/cds-env.js +1 -1
- package/lib/env/cds-requires.js +21 -20
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +5 -6
- package/lib/log/cds-log.js +6 -5
- package/lib/log/format/aspects/cf.js +2 -2
- package/lib/plugins.js +1 -1
- package/lib/ql/cds-ql.js +0 -3
- package/lib/req/request.js +3 -3
- package/lib/req/response.js +12 -7
- package/lib/srv/bindings.js +17 -17
- package/lib/srv/cds-connect.js +6 -9
- package/lib/srv/cds-serve.js +74 -137
- package/lib/srv/cds.Service.js +49 -0
- package/lib/srv/factory.js +4 -4
- package/lib/srv/middlewares/auth/ias-auth.js +29 -9
- package/lib/srv/middlewares/auth/index.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +19 -6
- package/lib/srv/protocols/hcql.js +16 -1
- package/lib/srv/srv-dispatch.js +1 -1
- package/lib/utils/cds-utils.js +4 -8
- package/lib/utils/csv-reader.js +27 -7
- package/libx/_runtime/cds.js +0 -6
- package/libx/_runtime/common/Service.js +5 -0
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/flows.js +106 -0
- package/libx/_runtime/common/generic/paging.js +3 -3
- package/libx/_runtime/common/utils/differ.js +5 -15
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/fiori/lean-draft.js +76 -40
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/remote/Service.js +68 -62
- package/libx/_runtime/remote/utils/client.js +29 -216
- package/libx/_runtime/remote/utils/query.js +197 -0
- package/libx/_runtime/ucl/Service.js +180 -112
- package/libx/_runtime/ucl/queries.js +61 -0
- package/libx/odata/ODataAdapter.js +1 -4
- package/libx/odata/index.js +2 -10
- package/libx/odata/middleware/error.js +8 -1
- package/libx/odata/middleware/stream.js +1 -1
- package/libx/odata/middleware/update.js +12 -2
- package/libx/odata/parse/afterburner.js +113 -20
- package/libx/odata/parse/cqn2odata.js +1 -3
- package/libx/rest/middleware/parse.js +9 -2
- package/package.json +2 -2
- package/server.js +2 -0
- package/srv/app-service.js +1 -0
- package/srv/db-service.js +1 -0
- package/srv/msg-service.js +1 -0
- package/srv/remote-service.js +1 -0
- package/srv/ucl-service.cds +32 -0
- package/srv/ucl-service.js +1 -0
- package/lib/ql/resolve.js +0 -45
- package/libx/common/assert/type-strict.js +0 -109
- package/libx/common/assert/utils.js +0 -60
package/lib/log/cds-log.js
CHANGED
|
@@ -162,9 +162,7 @@ const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
|
|
|
162
162
|
SILENT:0, ERROR:1, WARN:2, INFO:3, DEBUG:4, TRACE:5, SILLY:5, VERBOSE:5
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
// If cds.env is not yet loaded, update the loggers when it is
|
|
167
|
-
if (conf === defaults) cds.once ('env', ()=>{
|
|
165
|
+
function applyLogConfig() {
|
|
168
166
|
let { loggers, format } = exports, fmt = format
|
|
169
167
|
let { plain } = log.formatters
|
|
170
168
|
conf = cds.env.log
|
|
@@ -191,10 +189,13 @@ if (conf === defaults) cds.once ('env', ()=>{
|
|
|
191
189
|
if (fmt !== format) {
|
|
192
190
|
for (let each in loggers) loggers[each] .setFormat (fmt)
|
|
193
191
|
}
|
|
194
|
-
|
|
192
|
+
|
|
195
193
|
_init()
|
|
196
|
-
}
|
|
194
|
+
}
|
|
197
195
|
|
|
196
|
+
// If cds.env is not yet loaded, update the loggers when it is
|
|
197
|
+
if (conf === defaults) cds.once ('env', applyLogConfig);
|
|
198
|
+
else applyLogConfig() // otherwise apply the current config
|
|
198
199
|
|
|
199
200
|
function _init() {
|
|
200
201
|
if (conf.Logger) {
|
|
@@ -28,8 +28,8 @@ function cf_aspect(module, level, args, toLog) {
|
|
|
28
28
|
// add static fields from environment
|
|
29
29
|
Object.assign(toLog, this._CF_FIELDS)
|
|
30
30
|
|
|
31
|
-
// add subdomain, if available
|
|
32
|
-
const tenant_subdomain = cds.context?.
|
|
31
|
+
// add subdomain, if available
|
|
32
|
+
const tenant_subdomain = cds.context?.user?.authInfo?.getSubdomain?.()
|
|
33
33
|
if (tenant_subdomain) toLog.tenant_subdomain = tenant_subdomain
|
|
34
34
|
}
|
|
35
35
|
|
package/lib/plugins.js
CHANGED
|
@@ -11,7 +11,7 @@ const prio_plugins = {
|
|
|
11
11
|
*/
|
|
12
12
|
exports.fetch = function (DEV = process.env.NODE_ENV !== 'production') {
|
|
13
13
|
DEBUG?.time ('[cds.plugins] - fetched plugins in')
|
|
14
|
-
const plugins = {}
|
|
14
|
+
const plugins = JSON.parse(process.env.CDS_PLUGINS||'{}')
|
|
15
15
|
fetch_plugins_in (cds.home, false)
|
|
16
16
|
fetch_plugins_in (cds.root, DEV)
|
|
17
17
|
function fetch_plugins_in (root, dev) {
|
package/lib/ql/cds-ql.js
CHANGED
|
@@ -35,9 +35,6 @@ exports.DELETE = require('./DELETE')
|
|
|
35
35
|
exports.CREATE = require('./CREATE')
|
|
36
36
|
exports.DROP = require('./DROP')
|
|
37
37
|
|
|
38
|
-
exports.resolve = require('./resolve');
|
|
39
|
-
|
|
40
|
-
|
|
41
38
|
exports.predicate = require('./cds.ql-predicates')
|
|
42
39
|
exports.columns = require('./cds.ql-projections')
|
|
43
40
|
/** @import cqn from './cqn' */
|
package/lib/req/request.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../index')
|
|
2
|
-
const { Responses, Errors } = require('./response')
|
|
2
|
+
const { Responses, Errors, prepareError } = require('./response')
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Class Request represents requests received via synchronous protocols.
|
|
@@ -104,13 +104,13 @@ class Request extends require('./event') {
|
|
|
104
104
|
warn (...args) { return this._messages.add (3, ...args) }
|
|
105
105
|
error (...args) { return this._errors.add (null, ...args) }
|
|
106
106
|
reject (...args) {
|
|
107
|
-
if (args.length === 0 && this.errors) {
|
|
107
|
+
if (args.length === 0 && this.errors?.length) {
|
|
108
108
|
if (this.errors.length === 1) throw this.errors[0]
|
|
109
109
|
const err = new cds.error ('MULTIPLE_ERRORS', { details: this.errors })
|
|
110
110
|
delete err.stack
|
|
111
111
|
throw err
|
|
112
112
|
}
|
|
113
|
-
let e =
|
|
113
|
+
let e = prepareError(4, ...args)
|
|
114
114
|
if (!('stack' in e)) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
|
|
115
115
|
if (!('message' in e)) e.message = String (e.code || e.status)
|
|
116
116
|
throw e
|
package/lib/req/response.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
const cds = require ('../index')
|
|
2
2
|
const placeholders = [...'x'.repeat(9)].map((x,i) => `{${i+1}}`)
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
class Responses extends Array {
|
|
8
|
-
add (severity, code, message, target, args) {
|
|
9
|
-
let e // be filled in below...
|
|
4
|
+
function prepareError(severity, code, message, target, args) {
|
|
5
|
+
let e // be filled in below...
|
|
10
6
|
if (code?.raw) {
|
|
11
7
|
if (typeof message === 'object') {
|
|
12
8
|
target = Object.keys(message)[0]
|
|
@@ -31,6 +27,15 @@ class Responses extends Array {
|
|
|
31
27
|
}
|
|
32
28
|
}
|
|
33
29
|
if (severity) e.numericSeverity ??= severity
|
|
30
|
+
return e
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Messages Collector, used for `req.errors` and `req.messages`
|
|
35
|
+
*/
|
|
36
|
+
class Responses extends Array {
|
|
37
|
+
add (severity, code, message, target, args) {
|
|
38
|
+
const e = prepareError(severity, code, message, target, args)
|
|
34
39
|
this.push(e)
|
|
35
40
|
return e
|
|
36
41
|
}
|
|
@@ -44,4 +49,4 @@ class Errors extends Responses {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
module.exports = { Responses, Errors }
|
|
52
|
+
module.exports = { Responses, Errors, prepareError }
|
package/lib/srv/bindings.js
CHANGED
|
@@ -20,20 +20,28 @@ class Bindings {
|
|
|
20
20
|
bind (service) {
|
|
21
21
|
let required = cds.requires [service]
|
|
22
22
|
let binding = this.provides [required?.service || service]
|
|
23
|
-
if (binding) {
|
|
23
|
+
if (binding?.endpoints) {
|
|
24
|
+
const server = this.servers [binding.server]
|
|
25
|
+
const kind = [ required.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
|
|
26
|
+
const path = binding.endpoints [kind]
|
|
24
27
|
// in case of cds.requires.Foo = { ... }
|
|
25
28
|
if (typeof required === 'object') required.credentials = {
|
|
26
29
|
...required.credentials,
|
|
27
|
-
...binding.credentials
|
|
30
|
+
...binding.credentials,
|
|
31
|
+
url: server.url + path
|
|
28
32
|
}
|
|
29
33
|
// in case of cds.requires.Foo = true
|
|
30
|
-
else required = cds.requires[service] = {
|
|
34
|
+
else required = cds.requires[service] = cds.env.requires[service] = {
|
|
31
35
|
...cds.requires.kinds [binding.kind],
|
|
32
|
-
|
|
36
|
+
credentials: {
|
|
37
|
+
...binding.credentials,
|
|
38
|
+
url: server.url + path
|
|
39
|
+
}
|
|
33
40
|
}
|
|
41
|
+
required.kind = kind
|
|
34
42
|
// REVISIT: temporary fix to inherit kind as well for mocked odata services
|
|
35
43
|
// otherwise mocking with two services does not work for kind:odata-v2
|
|
36
|
-
if (
|
|
44
|
+
if (kind === 'odata-v2' || kind === 'odata-v4') required.kind = 'odata'
|
|
37
45
|
}
|
|
38
46
|
return required
|
|
39
47
|
}
|
|
@@ -79,20 +87,12 @@ class Bindings {
|
|
|
79
87
|
url
|
|
80
88
|
}
|
|
81
89
|
// register our services
|
|
82
|
-
for (let
|
|
90
|
+
for (let srv of services) {
|
|
83
91
|
// if (each.name in cds.env.requires) continue
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
credentials: {
|
|
88
|
-
...options.credentials,
|
|
89
|
-
url: url + each.path
|
|
90
|
-
},
|
|
91
|
-
server: pid
|
|
92
|
+
provides[srv.name] = {
|
|
93
|
+
endpoints: Object.fromEntries (srv.endpoints.map (ep => [ ep.kind, ep.path ])),
|
|
94
|
+
server: pid,
|
|
92
95
|
}
|
|
93
|
-
// if (each.endpoints.length > 1) provides[each.name].other = each.endpoints.slice(1).map(
|
|
94
|
-
// ep => ({ kind: ep.kind, url: url + ep.path })
|
|
95
|
-
// )
|
|
96
96
|
}
|
|
97
97
|
process.on ('exit', ()=> this.purge())
|
|
98
98
|
cds.on ('shutdown', ()=> this.purge())
|
package/lib/srv/cds-connect.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const cds = require('..'), LOG = cds.log('cds.connect')
|
|
2
|
-
const _pending = cds.services._pending ??= {} // used below to chain parallel connect.to(<same>)
|
|
3
2
|
const TRACE = cds.debug('trace')
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -27,10 +26,7 @@ connect.to = (datasource, options) => {
|
|
|
27
26
|
else if (datasource) {
|
|
28
27
|
if (datasource._is_service_class) [ Service, datasource ] = [ datasource, datasource.name ] // .to(ServiceClass)
|
|
29
28
|
else if (datasource.name) datasource = datasource.name // .to({ name: 'Service' }) from cds-typer
|
|
30
|
-
if (!options
|
|
31
|
-
if (datasource in cds.services) return Promise.resolve (cds.services[datasource])
|
|
32
|
-
if (datasource in _pending) return _pending[datasource]
|
|
33
|
-
}
|
|
29
|
+
if (!options && datasource in cds.services) return Promise.resolve (cds.services[datasource])
|
|
34
30
|
}
|
|
35
31
|
const promise = (async()=>{
|
|
36
32
|
TRACE?.time(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
|
|
@@ -45,24 +41,25 @@ connect.to = (datasource, options) => {
|
|
|
45
41
|
// construct new service instance
|
|
46
42
|
let srv = await new Service (datasource,m,o); await (Service._is_service_class ? srv.init?.() : Service.init?.(srv))
|
|
47
43
|
if (!srv.isDatabaseService && _is_queued(o)) srv = cds.queued(srv)
|
|
48
|
-
if (
|
|
44
|
+
if (!options && datasource) {
|
|
49
45
|
if (datasource === 'db') cds.db = srv
|
|
50
46
|
cds.services[datasource] = srv
|
|
51
|
-
delete _pending[datasource]
|
|
52
47
|
}
|
|
53
48
|
if (!o.silent) cds.emit ('connect',srv)
|
|
54
49
|
TRACE?.timeEnd(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
|
|
55
50
|
return srv
|
|
56
51
|
})()
|
|
57
52
|
// queue parallel requests to a single promise, to avoid creating multiple services
|
|
58
|
-
if (
|
|
53
|
+
if (!options && datasource) cds.services[datasource] = promise
|
|
59
54
|
return promise
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
function options4 (name, _o) {
|
|
63
|
-
|
|
58
|
+
if (name?.startsWith('http:')) [_o,name] = [{ url:name }]
|
|
59
|
+
const [, kind=_o?.kind, url=_o?.url ] = /^(\w+):(.*)/.exec(name) || []
|
|
64
60
|
const conf = cds.service.bindings.at(name) || cds.requires[name] || cds.requires[kind] || cds.requires.kinds[name] || cds.requires.kinds[kind]
|
|
65
61
|
const o = { kind, ...conf, ..._o }
|
|
62
|
+
if (!o.kind) o.kind = (url||conf?.credentials?.url)?.match (/\/(hcql|rest|odata)\//)?.[1]
|
|
66
63
|
if (!o.kind && !o.impl && !o.silent) throw cds.error(
|
|
67
64
|
conf ? `Configuration for 'cds.requires.${name}' lacks mandatory property 'kind' or 'impl'` :
|
|
68
65
|
name ? `Didn't find a configuration for 'cds.requires.${name}' in ${cds.root}` :
|
package/lib/srv/cds-serve.js
CHANGED
|
@@ -1,147 +1,84 @@
|
|
|
1
1
|
const cds = require ('..')
|
|
2
2
|
const Service = cds.service.factory
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!from || from === 'all' || from === '*') from = cds.model || '*'
|
|
36
|
-
if (from.definitions) return from
|
|
37
|
-
if (from === '?') try { return cds.model || await cds.load('*',o) } catch { return }
|
|
38
|
-
return cds.load(from, {...o, silent:true })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
// Pass 1: Construct service provider instances...
|
|
42
|
-
const all=[], provided = loaded.then (async csn => { // NOSONAR
|
|
43
|
-
|
|
44
|
-
// Shortcut for directly passed service classes
|
|
45
|
-
if (o.service?._is_service_class) {
|
|
46
|
-
const Service = o.service, d = { name: o.service.name }
|
|
47
|
-
const srv = await _new (Service, d,csn,o)
|
|
48
|
-
return all.push (srv)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Get relevant service definitions from model...
|
|
52
|
-
let {services} = csn = cds.compile.for.nodejs (csn)
|
|
53
|
-
const required = cds.requires
|
|
54
|
-
if (o.service && o.service !== 'all') {
|
|
55
|
-
// skip services not chosen by o.service, if specified
|
|
56
|
-
const specified = o.service.split(/\s*,\s*/).map (s => required[s] && required[s].service || s )
|
|
57
|
-
// matching exact or unqualified name
|
|
58
|
-
services = services.filter (s => specified.some (n => s.name === n || s.name.endsWith('.'+n)))
|
|
59
|
-
if (!services.length) throw cds.error (`No such service: '${o.service}'`)
|
|
60
|
-
}
|
|
61
|
-
services = services.filter (d => !(
|
|
62
|
-
// skip all services marked to be ignored
|
|
63
|
-
d['@cds.ignore'] || d['@cds.serve.ignore'] ||
|
|
64
|
-
// skip external services, unless asked to mock them and unbound
|
|
65
|
-
(d['@cds.external'] || required[d.name]?.external) && (!o.mocked || required[d.name]?.credentials)
|
|
66
|
-
))
|
|
67
|
-
if (services.length > 1 && o.at) {
|
|
68
|
-
throw cds.error `You cannot specify 'path' for multiple services`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Construct service instances and register them to cds.services
|
|
72
|
-
all.push (... await Promise.all (services.map (d => _new (Service,d,csn,o))))
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// Pass 2: Finalize service bootstrapping by calling their impl functions.
|
|
76
|
-
// Note: doing that in a second pass guarantees all own services are in
|
|
77
|
-
// cds.services, so they'll be found when they cds.connect to each others.
|
|
78
|
-
let ready = provided.then (()=> Promise.all (all.map (async srv => {
|
|
79
|
-
cds.services[srv.name] = await Service.init (srv)
|
|
3
|
+
const {serve} = cds.service.protocols
|
|
4
|
+
const {init} = Service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Fluent API */
|
|
8
|
+
module.exports = (services, options, o = {...options}) => ({
|
|
9
|
+
from (model) { o.from = model; return this },
|
|
10
|
+
with (impl) { o.with = impl; return this },
|
|
11
|
+
to (prot) { o.to = prot; return this },
|
|
12
|
+
at (path) { o.at = path; return this },
|
|
13
|
+
in (app) { o.app = app; return this },
|
|
14
|
+
then: (r,e) => _cds_serve (services,o) .then (r,e)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns { Promise<Record<string,Service>> } single or multiple services, like that:
|
|
20
|
+
* @example let { CatalogService } = await cds.serve(...) // single or many
|
|
21
|
+
* let CatalogService = await cds.serve(...) // single only
|
|
22
|
+
* @import Service from './cds.Service'
|
|
23
|
+
*/
|
|
24
|
+
async function _cds_serve (services='all', o) {
|
|
25
|
+
const TRACE = cds.log('trace'), t0 = TRACE._debug && performance.now()
|
|
26
|
+
|
|
27
|
+
if (services.service) { Object.assign(o,services); services = o.service }
|
|
28
|
+
else if (o.service) { o.from ??= services; services = o.service }
|
|
29
|
+
else if (is_files(services)) { o.from ??= services; services = 'all' }
|
|
30
|
+
else if (is_csn(services)) { o.from ??= services; services = 'all' }
|
|
31
|
+
|
|
32
|
+
const srvs = await _construct (services,o)
|
|
33
|
+
if (o.app) for (let srv of srvs) if (!_ignore(srv.definition)) {
|
|
34
|
+
serve (srv, o.app)
|
|
80
35
|
cds.service.providers.push (srv)
|
|
81
|
-
|
|
82
|
-
return srv
|
|
83
|
-
})))
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Return fluent API to fill in remaining options...
|
|
87
|
-
return {
|
|
88
|
-
|
|
89
|
-
from (model) { o.from = model; return this },
|
|
90
|
-
with (impl) { o.with = impl; return this },
|
|
91
|
-
at (path) { o.at = path; return this },
|
|
92
|
-
to (protocol) { o.to = protocol; return this },
|
|
93
|
-
|
|
94
|
-
/** Fluent method to serve constructed providers to express app */
|
|
95
|
-
in (app) {
|
|
96
|
-
const { serve } = cds.service.protocols
|
|
97
|
-
ready = ready.then (()=> all.forEach (each => {
|
|
98
|
-
const d = each.definition || {}
|
|
99
|
-
if (d['@protocol'] === 'none' || d['@cds.api.ignore']) return each._is_dark = true
|
|
100
|
-
else serve (each, /*to:*/ app)
|
|
101
|
-
if (!o.silent) cds.emit ('serving',each)
|
|
102
|
-
}))
|
|
103
|
-
return this
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Finally resolve to a single service or a map of many,
|
|
108
|
-
* which can be used like that: @example
|
|
109
|
-
* let { CatalogService } = await cds.serve(...) // single or many
|
|
110
|
-
* let CatalogService = await cds.serve(...) // single only
|
|
111
|
-
*/
|
|
112
|
-
then: (_resolve, _error) => ready.then ((s)=>{
|
|
113
|
-
TRACE?.timeEnd(`cds.serve ${som}`.padEnd(22).slice(0,22))
|
|
114
|
-
if (all.length === 0) return _resolve()
|
|
115
|
-
if (all.length === 1) return _resolve(Object.defineProperty(s=all[0],s.name,{value:s}))
|
|
116
|
-
else return _resolve (all.reduce ((r,s)=>{ r[s.name]=s; return r },{}))
|
|
117
|
-
}, _error),
|
|
118
|
-
|
|
119
|
-
catch: (e) => ready.catch(e)
|
|
36
|
+
o.silent || cds.emit ('serving', srv) // NOTE: only for protocol-served ones (compat behaviour)
|
|
120
37
|
}
|
|
38
|
+
|
|
39
|
+
if (t0) TRACE.debug ('cds.served in'.padEnd(21),':', (performance.now()-t0).toFixed(), 'ms')
|
|
40
|
+
if (srvs.length === 1) return Object.defineProperty (o=srvs[0], o.name, {value:o})
|
|
41
|
+
return srvs.reduce ((many,s)=>{ many[s.name] = s; return many },{})
|
|
121
42
|
}
|
|
122
43
|
|
|
123
44
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Constructs service instances from a model's service definitions.
|
|
47
|
+
* @param { string & typeof cds.Service } services
|
|
48
|
+
*/
|
|
49
|
+
async function _construct (services, o) {
|
|
50
|
+
if (services._is_service_class) // shortcut for directly passed service classes
|
|
51
|
+
return [ await init (new services (services.name, cds.model, o)) ]
|
|
52
|
+
|
|
53
|
+
// Resolve/load the model to use subsequently
|
|
54
|
+
const csn = !o.from || o.from === '*' ? cds.model || await cds.load('*')
|
|
55
|
+
: is_csn(o.from) ? o.from : await cds.load (o.from, { silent: true })
|
|
56
|
+
const m = cds.compile.for.nodejs (csn)
|
|
57
|
+
|
|
58
|
+
// Fetch service definitions from model
|
|
59
|
+
let defs = services == 'all' ? m.services : m.services.filter (_choose(_specified(services)))
|
|
60
|
+
defs = defs.filter (d => !d['@cds.ignore'] && !d['@cds.serve.ignore']) // skip ignored ones
|
|
61
|
+
defs = defs.filter (o.mocked ? _mock_external : _skip_external) // skip externals, or mock
|
|
62
|
+
|
|
63
|
+
// Construct services, adding upfront promises to cds.services
|
|
64
|
+
return Promise.all (defs.map (d => {
|
|
65
|
+
const n = cds.requires[d.name]?.name || d.name; o.service = d.name
|
|
66
|
+
return cds.services[n] = cds.services[d.name] = new Service(n,m,o) .then(init) .then (srv => {
|
|
67
|
+
if (d.is_external) srv.mocked = true
|
|
68
|
+
if (d.name !== n) Object.defineProperty (cds.services, d.name, {value:srv})
|
|
69
|
+
if (!o.silent) cds.emit (`serving:${n}`, srv)
|
|
70
|
+
return cds.services[n] = cds.services[d.name] = srv // replacing upfront promises with real services
|
|
71
|
+
})
|
|
72
|
+
}))
|
|
142
73
|
}
|
|
143
74
|
|
|
144
75
|
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
76
|
+
const _specified = services => services.split (/\s*,\s*/).map(s => cds.requires[s]?.service || s)
|
|
77
|
+
const _choose = specified => ({name:n}) => specified.some (s => s === n || n.endsWith('.'+s))
|
|
78
|
+
const _ignore = d => d?.['@protocol'] === 'none' || d?.['@cds.api.ignore']
|
|
79
|
+
const _mock_external = d => _skip_external(d) || !cds.requires[d.name]?.credentials
|
|
80
|
+
const _skip_external = d => !d['@external'] && !d['@cds.external'] && !cds.requires[d.name]?.external
|
|
81
|
+
|| (d.is_external = true, false) // tagging all external ones, and skipping them
|
|
82
|
+
|
|
83
|
+
const is_files = x => Array.isArray(x) || typeof x === 'string' && (/[^\w.$]/.test(x) || /\.(cds|csn|json)$/.test(x))
|
|
84
|
+
const is_csn = x => x?.definitions
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -154,6 +154,55 @@ class Service extends ReflectionAPI {
|
|
|
154
154
|
/** @deprecated */ get operations() { return this.actions }
|
|
155
155
|
/** @deprecated */ get transaction() { return this.tx }
|
|
156
156
|
/** @deprecated */ get isExtensible() { return this.model === cds.model && !this.name?.startsWith('cds.xt.') }
|
|
157
|
+
|
|
158
|
+
get resolve() {
|
|
159
|
+
if (this._resolve) return this._resolve
|
|
160
|
+
|
|
161
|
+
const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView')
|
|
162
|
+
const PERSISTENCE_TABLE = '@cds.persistence.table'
|
|
163
|
+
|
|
164
|
+
const _isPersistenceTable = target =>
|
|
165
|
+
Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE]
|
|
166
|
+
const _defaultAbort = tx => e => e._service?.name === tx.definition?.name
|
|
167
|
+
|
|
168
|
+
this._resolve = (query, abortCondition) => {
|
|
169
|
+
const ctx = cds.context
|
|
170
|
+
const model = ctx?.model || this.model
|
|
171
|
+
return resolveView(query, model, this, abortCondition || _defaultAbort(this))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// REVISIT: Remove argument `skipForbiddenViewCheck` once we get rid of composition tree
|
|
175
|
+
this._resolve.transitions = (query, abortCondition, skipForbiddenViewCheck) => {
|
|
176
|
+
const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined
|
|
177
|
+
const _tx = typeof tx === 'function' ? cds.context?.tx : this
|
|
178
|
+
return getTransition(target, _tx, skipForbiddenViewCheck, undefined, {
|
|
179
|
+
abort: abortCondition ?? (this.isDatabaseService ? this.resolve._abortDB : _defaultAbort(this))
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._resolve.resolve4db = query => {
|
|
184
|
+
return this.resolve(query, this, this.resolve.abortDB)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// REVISIT: Remove once we get rid of composition tree
|
|
188
|
+
this._resolve.table = target => {
|
|
189
|
+
if (target.query?._target && !_isPersistenceTable(target)) {
|
|
190
|
+
return this.resolve.table(target.query._target)
|
|
191
|
+
}
|
|
192
|
+
return target
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// REVISIT: Remove once we get rid of old db
|
|
196
|
+
this._resolve.abortDB = target => {
|
|
197
|
+
return !!(_isPersistenceTable(target) || !target.query?._target)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this._resolve.transitions4db = (query, skipForbiddenViewCheck) => {
|
|
201
|
+
return this.resolve.transitions(query, this.resolve.abortDB, skipForbiddenViewCheck)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this._resolve
|
|
205
|
+
}
|
|
157
206
|
}
|
|
158
207
|
|
|
159
208
|
const { dispatch, handle } = require('./srv-dispatch')
|
package/lib/srv/factory.js
CHANGED
|
@@ -2,7 +2,7 @@ const cds = require('..'), { path, isfile } = cds.utils
|
|
|
2
2
|
/**
|
|
3
3
|
* NOTE: Need this typed helper variable to be able to use IntelliSense for calls with new keyword.
|
|
4
4
|
* @import Service from './cds.Service'
|
|
5
|
-
* @type new() => Service
|
|
5
|
+
* @type new() => Service & Promise<Service>
|
|
6
6
|
*/
|
|
7
7
|
const factory = ServiceFactory
|
|
8
8
|
module.exports = exports = factory
|
|
@@ -37,7 +37,7 @@ function ServiceFactory (name, model, options) {
|
|
|
37
37
|
}}
|
|
38
38
|
|
|
39
39
|
function _kind (kind = o.kind ??= def['@kind'] || 'app-service') {
|
|
40
|
-
const {impl} = cds.
|
|
40
|
+
const {impl} = cds.requires[kind] || cds.requires.kinds[kind] || cds.error `No configuration found for 'cds.requires.${kind}'`
|
|
41
41
|
return impl || cds.error `No 'impl' configured for 'cds.requires.kinds.${kind}'`
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -67,8 +67,8 @@ const _legacy = impl => { // legacy hello-world style class
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Called by cds.connect() and cds.serve() for cds.service.impl-style implementations.
|
|
71
|
-
* @
|
|
70
|
+
* @protected Called by cds.connect() and cds.serve() for cds.service.impl-style implementations.
|
|
71
|
+
* @param {Service} srv
|
|
72
72
|
*/
|
|
73
73
|
exports.init = async function (srv) {
|
|
74
74
|
const {impl} = srv.options; if (typeof impl === 'function' && !impl._is_service_class)
|
|
@@ -43,7 +43,7 @@ module.exports = function ias_auth(config) {
|
|
|
43
43
|
|
|
44
44
|
const should_validate =
|
|
45
45
|
process.env.VCAP_APPLICATION &&
|
|
46
|
-
JSON.parse(process.env.VCAP_APPLICATION).application_uris?.some(uri => uri.match(/\.cert\./))
|
|
46
|
+
JSON.parse(process.env.VCAP_APPLICATION).application_uris?.some(uri => uri.match(/\.cert\.|\.mesh\.cf\./))
|
|
47
47
|
const validation_configured = serviceConfig.validation?.x5t?.enabled != null || serviceConfig.validation?.proofToken?.enabled != null
|
|
48
48
|
|
|
49
49
|
let validating_auth_service
|
|
@@ -68,13 +68,18 @@ module.exports = function ias_auth(config) {
|
|
|
68
68
|
|
|
69
69
|
try {
|
|
70
70
|
const _auth_service =
|
|
71
|
-
validating_auth_service && req.
|
|
71
|
+
validating_auth_service && req.hostname.match(/\.cert\.|\.mesh\.cf\./) ? validating_auth_service : auth_service
|
|
72
72
|
const securityContext = await createSecurityContext(xsuaa_service ? [_auth_service, xsuaa_service] : _auth_service, { req })
|
|
73
|
-
const tokenInfo = securityContext.token
|
|
74
73
|
const ctx = cds.context
|
|
75
|
-
ctx.user =
|
|
76
|
-
ctx.tenant =
|
|
77
|
-
|
|
74
|
+
ctx.user = securityContext.token instanceof XsuaaToken ? xsuaa_user_factory(securityContext) : user_factory(securityContext)
|
|
75
|
+
ctx.tenant = securityContext.token.getZoneId()
|
|
76
|
+
// REVISIT: remove compat in cds^10
|
|
77
|
+
Object.defineProperty(req, 'authInfo', {
|
|
78
|
+
get() {
|
|
79
|
+
cds.utils.deprecated({ kind: 'API', old: 'cds.context.http.req.authInfo', use: 'cds.context.user.authInfo' })
|
|
80
|
+
return securityContext
|
|
81
|
+
}
|
|
82
|
+
})
|
|
78
83
|
} catch (e) {
|
|
79
84
|
if (e instanceof ValidationError) {
|
|
80
85
|
LOG.warn('Unauthenticated request: ', e)
|
|
@@ -89,7 +94,8 @@ module.exports = function ias_auth(config) {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function get_user_factory(credentials, skipped_attrs) {
|
|
92
|
-
return function user_factory(
|
|
97
|
+
return function user_factory(securityContext) {
|
|
98
|
+
const tokenInfo = securityContext.token
|
|
93
99
|
const payload = tokenInfo.getPayload()
|
|
94
100
|
|
|
95
101
|
/*
|
|
@@ -107,7 +113,14 @@ function get_user_factory(credentials, skipped_attrs) {
|
|
|
107
113
|
if (Array.isArray(payload.ias_apis)) payload.ias_apis.forEach(r => (roles[r] = 1))
|
|
108
114
|
if (clientid === credentials.clientid) roles['internal-user'] = 1
|
|
109
115
|
else delete roles['internal-user']
|
|
110
|
-
return new cds.User({
|
|
116
|
+
return new cds.User({
|
|
117
|
+
id: 'system', roles, authInfo: securityContext,
|
|
118
|
+
// REVISIT: remove compat in cds^10
|
|
119
|
+
get tokenInfo() {
|
|
120
|
+
cds.utils.deprecated({ kind: 'API', old: 'cds.context.user.tokenInfo', use: 'cds.context.user.authInfo.token' })
|
|
121
|
+
return securityContext.token
|
|
122
|
+
}
|
|
123
|
+
})
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
// add all unknown attributes to req.user.attr in order to keep public API small
|
|
@@ -125,7 +138,14 @@ function get_user_factory(credentials, skipped_attrs) {
|
|
|
125
138
|
if (attr.given_name) attr.givenName = attr.given_name
|
|
126
139
|
if (attr.family_name) attr.familyName = attr.family_name
|
|
127
140
|
|
|
128
|
-
return new cds.User({
|
|
141
|
+
return new cds.User({
|
|
142
|
+
id: payload.sub, attr, authInfo: securityContext,
|
|
143
|
+
// REVISIT: remove compat in cds^10
|
|
144
|
+
get tokenInfo() {
|
|
145
|
+
cds.utils.deprecated({ kind: 'API', old: 'cds.context.user.tokenInfo', use: 'cds.context.user.authInfo.token' })
|
|
146
|
+
return securityContext.token
|
|
147
|
+
}
|
|
148
|
+
})
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
|
|
@@ -29,14 +29,15 @@ module.exports = function auth_factory (o) {
|
|
|
29
29
|
// from cds.requires.auth in the log or error output below.
|
|
30
30
|
|
|
31
31
|
// try resolving the impl, throw if not found
|
|
32
|
-
const config = { kind, impl
|
|
32
|
+
const config = { kind, impl }
|
|
33
33
|
// use cds.resolve() to allow './srv/auth.js' and 'srv/auth.js' -> REVISIT: cds.resolve() is not needed here, and not meant for that !
|
|
34
34
|
try { impl = require.resolve (cds.resolve (impl)?.[0], {paths:[cds.root]}) } catch {
|
|
35
35
|
throw cds.error `Didn't find auth implementation for ${config}`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// load the auth middleware from the resolved path
|
|
39
|
-
cds.
|
|
39
|
+
config.impl = cds.utils.local(impl)
|
|
40
|
+
cds.log().info ('using auth strategy', config)
|
|
40
41
|
let auth = require (impl)
|
|
41
42
|
|
|
42
43
|
// default export of ESM / .ts auth
|