@sap/cds 5.6.4 → 5.7.1
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 +102 -0
- package/_i18n/i18n_fr.properties +4 -4
- package/apis/cds.d.ts +7 -10
- package/apis/connect.d.ts +3 -3
- package/apis/core.d.ts +2 -4
- package/apis/models.d.ts +2 -3
- package/apis/ql.d.ts +0 -1
- package/apis/services.d.ts +3 -3
- package/bin/build/buildTaskFactory.js +16 -10
- package/bin/build/buildTaskProviderFactory.js +3 -3
- package/bin/build/constants.js +2 -1
- package/bin/build/provider/buildTaskProviderInternal.js +14 -14
- package/bin/build/provider/hana/2migration.js +2 -3
- package/bin/build/provider/hana/index.js +34 -0
- package/bin/build/provider/hana/migrationtable.js +90 -22
- package/bin/build/provider/hana/template/undeploy.json +5 -0
- package/bin/build/provider/node-cf/index.js +9 -2
- package/bin/serve.js +16 -18
- package/lib/compile/cdsc.js +15 -5
- package/lib/compile/etc/_localized.js +4 -4
- package/lib/compile/extend.js +8 -0
- package/lib/compile/index.js +3 -1
- package/lib/compile/minify.js +61 -0
- package/lib/compile/resolve.js +4 -1
- package/lib/compile/to/gql.js +9 -0
- package/lib/compile/to/sql.js +26 -30
- package/lib/connect/index.js +1 -1
- package/lib/core/entities.js +0 -3
- package/lib/core/infer.js +1 -0
- package/lib/core/reflect.js +0 -34
- package/lib/deploy.js +25 -17
- package/lib/env/defaults.js +3 -1
- package/lib/env/index.js +8 -3
- package/lib/env/presets.js +38 -0
- package/lib/env/requires.js +16 -11
- package/lib/index.js +13 -11
- package/lib/log/format/kibana.js +3 -1
- package/lib/log/index.js +2 -2
- package/lib/req/cds-context.js +79 -0
- package/lib/req/context.js +5 -77
- package/lib/req/request.js +1 -1
- package/lib/serve/Service-api.js +8 -4
- package/lib/serve/Service-dispatch.js +0 -7
- package/lib/serve/Service-methods.js +6 -8
- package/lib/serve/Transaction.js +35 -30
- package/lib/serve/adapters.js +1 -4
- package/lib/utils/axios.js +1 -1
- package/libx/_runtime/audit/Service.js +44 -20
- package/libx/_runtime/audit/generic/personal/access.js +16 -11
- package/libx/_runtime/audit/generic/personal/modification.js +5 -5
- package/libx/_runtime/audit/generic/personal/utils.js +46 -37
- package/libx/_runtime/{common/auth → auth}/index.js +21 -7
- package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
- package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
- package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
- package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +24 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
- package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
- package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
- package/libx/_runtime/cds-services/services/Service.js +0 -6
- package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
- package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -6
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
- package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
- package/libx/_runtime/cds-services/util/assert.js +1 -262
- package/libx/_runtime/cds.js +6 -9
- package/libx/_runtime/common/aspects/entity.js +1 -1
- package/libx/_runtime/common/composition/delete.js +4 -2
- package/libx/_runtime/common/composition/update.js +22 -38
- package/libx/_runtime/common/composition/utils.js +3 -7
- package/libx/_runtime/common/error/standardError.js +11 -0
- package/libx/_runtime/common/generic/auth.js +61 -30
- package/libx/_runtime/common/generic/crud.js +11 -23
- package/libx/_runtime/common/generic/input.js +20 -0
- package/libx/_runtime/common/generic/put.js +4 -10
- package/libx/_runtime/common/generic/sorting.js +12 -30
- package/libx/_runtime/common/perf/index.js +24 -0
- package/libx/_runtime/common/utils/cqn.js +58 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
- package/libx/_runtime/common/utils/csn.js +38 -56
- package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
- package/libx/_runtime/common/utils/resolveView.js +4 -5
- package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
- package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
- package/libx/_runtime/common/utils/structured.js +35 -25
- package/libx/_runtime/db/Service.js +0 -6
- package/libx/_runtime/db/expand/expand-v2.js +130 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
- package/libx/_runtime/db/expand/index.js +3 -1
- package/libx/_runtime/db/generic/input.js +52 -10
- package/libx/_runtime/db/generic/integrity.js +367 -26
- package/libx/_runtime/db/generic/virtual.js +51 -13
- package/libx/_runtime/db/query/update.js +9 -3
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
- package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
- package/libx/_runtime/fiori/generic/activate.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +2 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -0
- package/libx/_runtime/fiori/generic/patch.js +1 -1
- package/libx/_runtime/fiori/generic/read.js +123 -57
- package/libx/_runtime/fiori/uiflex/index.js +1 -1
- package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +3 -3
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/hana/Service.js +1 -8
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
- package/libx/_runtime/hana/execute.js +10 -4
- package/libx/_runtime/hana/pool.js +55 -45
- package/libx/_runtime/hana/search.js +7 -6
- package/libx/_runtime/hana/search2cqn4sql.js +8 -5
- package/libx/_runtime/hana/searchToContains.js +3 -1
- package/libx/_runtime/index.js +5 -5
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
- package/libx/_runtime/messaging/Outbox.js +53 -0
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
- package/libx/_runtime/messaging/common-utils/connections.js +14 -9
- package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
- package/libx/_runtime/messaging/file-based.js +5 -5
- package/libx/_runtime/messaging/message-queuing.js +2 -3
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
- package/libx/_runtime/messaging/outbox/utils.js +192 -0
- package/libx/_runtime/messaging/service.js +16 -30
- package/libx/_runtime/remote/Service.js +15 -0
- package/libx/_runtime/remote/utils/client.js +15 -3
- package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
- package/libx/_runtime/sqlite/Service.js +7 -10
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
- package/libx/_runtime/sqlite/execute.js +18 -12
- package/libx/_runtime/types/api.js +2 -1
- package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +19 -15
- package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
- package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +171 -130
- package/libx/odata/index.js +16 -14
- package/libx/odata/parser.js +1 -0
- package/libx/odata/utils.js +57 -0
- package/libx/rest/RestAdapter.js +2 -6
- package/libx/rest/utils/data.js +1 -6
- package/package.json +4 -3
- package/server.js +4 -5
- package/srv/audit-log.cds +87 -0
- package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
- package/srv/flex.js +1 -0
- package/srv/outbox.cds +11 -0
- package/srv/outbox.js +0 -0
- package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
- package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
- package/libx/odata/odata2cqn/index.js +0 -3
- package/libx/odata/odata2cqn/parser.js +0 -1
- package/libx/odata/readme.md +0 -1
- package/libx/odata/utils/index.js +0 -64
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const waitingTime = require('../common-utils/waitingTime')
|
|
3
|
+
const OutboxRunner = require('./OutboxRunner')
|
|
4
|
+
const { isStandardError } = require('../../common/error/standardError')
|
|
5
|
+
const LOG = cds.log('persistent-outbox')
|
|
6
|
+
const _safeJSONParse = string => {
|
|
7
|
+
try {
|
|
8
|
+
return string && JSON.parse(string)
|
|
9
|
+
} catch (e) {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const outboxRunner = new OutboxRunner()
|
|
13
|
+
const cdsUser = 'cds.internal.user'
|
|
14
|
+
const messageProcessorRegistered = Symbol('message processor registered')
|
|
15
|
+
|
|
16
|
+
const _getMessagesEntity = () => {
|
|
17
|
+
const messagesDbName = 'cds.outbox.Messages'
|
|
18
|
+
const messagesEntity = cds.model.definitions[messagesDbName]
|
|
19
|
+
if (!messagesEntity)
|
|
20
|
+
throw new Error(`The entity '${messagesDbName}' is missing but needed for the persistent outbox.`)
|
|
21
|
+
return messagesEntity
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// REVISIT: Is there a better way?
|
|
25
|
+
const _isProviderTenant = tenant =>
|
|
26
|
+
cds.requires.uaa && cds.requires.uaa.credentials && cds.requires.uaa.credentials.identityzoneid === tenant
|
|
27
|
+
|
|
28
|
+
const hasPersistentOutbox = (srv, tenant) => {
|
|
29
|
+
if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
|
|
30
|
+
if (srv.options && srv.options.outbox && srv.options.outbox.kind && srv.options.outbox.kind !== 'persistent-outbox')
|
|
31
|
+
return false
|
|
32
|
+
if (cds._mtxEnabled && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isUnrecoverable = (service, error) => {
|
|
37
|
+
let unrecoverable = service.isUnrecoverableError && service.isUnrecoverableError(error)
|
|
38
|
+
if (unrecoverable === undefined) unrecoverable = error.unrecoverable
|
|
39
|
+
return unrecoverable || isStandardError(error)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const _processSingleMessage = async (service, message, succeededMessages) => {
|
|
43
|
+
const msg = _safeJSONParse(message.msg)
|
|
44
|
+
const userId = msg[cdsUser]
|
|
45
|
+
delete msg[cdsUser]
|
|
46
|
+
if (!msg) return message.ID
|
|
47
|
+
// Promise resolve is necessary because we want to set `cds.context` only
|
|
48
|
+
// inside this call
|
|
49
|
+
return Promise.resolve().then(async () => {
|
|
50
|
+
if (userId) cds.context.user = new cds.User.Privileged(userId)
|
|
51
|
+
try {
|
|
52
|
+
service._emitImmediate && (await service._emitImmediate(msg))
|
|
53
|
+
succeededMessages.push(message.ID)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
const failedMessage = {
|
|
56
|
+
event: msg.event,
|
|
57
|
+
ID: message.ID,
|
|
58
|
+
attempts: message.attempts,
|
|
59
|
+
error: e,
|
|
60
|
+
unrecoverable: isUnrecoverable(service, e)
|
|
61
|
+
}
|
|
62
|
+
throw Object.assign(new Error('processing failed'), { failedMessage })
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Note: This function can also run for each tenant on startup
|
|
68
|
+
const processMessages = async (service, tenant, _opts = {}) => {
|
|
69
|
+
const opts = Object.assign({ attempt: 0 }, _opts)
|
|
70
|
+
const name = service.name
|
|
71
|
+
const messagesEntity = _getMessagesEntity()
|
|
72
|
+
|
|
73
|
+
outboxRunner.run({ name, tenant }, () => {
|
|
74
|
+
let letAppCrash = false
|
|
75
|
+
const config = tenant ? { tenant, user: new cds.User.Privileged() } : { user: new cds.User.Privileged() }
|
|
76
|
+
const spawn = cds.spawn(async () => {
|
|
77
|
+
let messages
|
|
78
|
+
try {
|
|
79
|
+
const messagesQuery = SELECT.from(messagesEntity)
|
|
80
|
+
.where({ target: name })
|
|
81
|
+
.orderBy('timestamp')
|
|
82
|
+
.limit(opts.chunkSize)
|
|
83
|
+
.forUpdate()
|
|
84
|
+
if (opts.maxAttempts) messagesQuery.where({ attempts: { '<': opts.maxAttempts } })
|
|
85
|
+
messages = await messagesQuery
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// could potentially be a timeout
|
|
88
|
+
const _waitingTime = waitingTime(opts.attempt)
|
|
89
|
+
LOG._error &&
|
|
90
|
+
LOG.error(
|
|
91
|
+
'Outbox SELECT FOR UPDATE failed',
|
|
92
|
+
opts.attempt > 0
|
|
93
|
+
? ''
|
|
94
|
+
: {
|
|
95
|
+
cause: e
|
|
96
|
+
},
|
|
97
|
+
`Retrying in ${Math.round(_waitingTime / 1000)} s`
|
|
98
|
+
)
|
|
99
|
+
outboxRunner.schedule(
|
|
100
|
+
{
|
|
101
|
+
name,
|
|
102
|
+
tenant,
|
|
103
|
+
waitingTime: _waitingTime
|
|
104
|
+
},
|
|
105
|
+
() => processMessages(service, tenant, { ...opts, attempt: opts.attempt + 1 })
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
const messagesToBeDeleted = []
|
|
110
|
+
let error
|
|
111
|
+
for (const m of messages) {
|
|
112
|
+
try {
|
|
113
|
+
await _processSingleMessage(service, m, messagesToBeDeleted)
|
|
114
|
+
} catch (e) {
|
|
115
|
+
error = e
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (error && error.failedMessage.unrecoverable) {
|
|
120
|
+
// opts.crashOnError is not official!!!
|
|
121
|
+
if (opts.crashOnError !== false) letAppCrash = true
|
|
122
|
+
messagesToBeDeleted.push(error.failedMessage.ID)
|
|
123
|
+
}
|
|
124
|
+
if (messagesToBeDeleted.length) await DELETE.from(messagesEntity).where('ID in', messagesToBeDeleted)
|
|
125
|
+
if (error) {
|
|
126
|
+
if (!error.failedMessage.unrecoverable) {
|
|
127
|
+
const _waitingTime = waitingTime(error.failedMessage.attempts)
|
|
128
|
+
const info = { service: name, event: error.failedMessage.event }
|
|
129
|
+
if (error.failedMessage.attempts > 0) info.cause = error.failedMessage.error
|
|
130
|
+
LOG._error && LOG.error('Emit failed', info, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
|
|
131
|
+
await UPDATE(messagesEntity)
|
|
132
|
+
.where({ ID: error.failedMessage.ID })
|
|
133
|
+
.set({ attempts: { '+=': 1 } })
|
|
134
|
+
outboxRunner.schedule(
|
|
135
|
+
{
|
|
136
|
+
name,
|
|
137
|
+
tenant,
|
|
138
|
+
waitingTime: _waitingTime
|
|
139
|
+
},
|
|
140
|
+
() => processMessages(service, tenant, opts)
|
|
141
|
+
)
|
|
142
|
+
} else {
|
|
143
|
+
LOG._error &&
|
|
144
|
+
LOG.error(
|
|
145
|
+
'Emit failed',
|
|
146
|
+
{ service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
|
|
147
|
+
'Unrecoverable, outbox entry deleted'
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
outboxRunner.success({ name, tenant })
|
|
152
|
+
if (messages.length === opts.chunkSize) {
|
|
153
|
+
processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
|
|
154
|
+
} else {
|
|
155
|
+
LOG._trace && LOG.trace(`All persistent-outbox messages processed for service '${service.name}'`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, config)
|
|
159
|
+
spawn.on('done', () => {
|
|
160
|
+
if (letAppCrash) process.exit(1)
|
|
161
|
+
outboxRunner.end({ name, tenant }, () => processMessages(service, tenant, opts))
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const registerMessageProcessor = (name, context) => {
|
|
167
|
+
const registry = context[messageProcessorRegistered] || (context[messageProcessorRegistered] = new Set())
|
|
168
|
+
if (!registry.has(name)) {
|
|
169
|
+
registry.add(name)
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const _createMessage = (name, msg, context) => {
|
|
176
|
+
const _msg = { ...msg, [cdsUser]: context.user.id }
|
|
177
|
+
const outboxMsg = {
|
|
178
|
+
ID: cds.utils.uuid(),
|
|
179
|
+
target: name,
|
|
180
|
+
timestamp: new Date().toISOString(), // needs to be different for each emit
|
|
181
|
+
msg: JSON.stringify(_msg)
|
|
182
|
+
}
|
|
183
|
+
return outboxMsg
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const writeInOutbox = async (name, msg, context) => {
|
|
187
|
+
const outboxMsg = _createMessage(name, msg, context)
|
|
188
|
+
const messagesEntity = _getMessagesEntity()
|
|
189
|
+
return INSERT.into(messagesEntity).entries(outboxMsg)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { processMessages, registerMessageProcessor, writeInOutbox, hasPersistentOutbox, isUnrecoverable }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
const LOG = cds.log('messaging')
|
|
3
3
|
const queued = require('./common-utils/queued')
|
|
4
|
+
const OutboxService = require('./Outbox')
|
|
4
5
|
|
|
5
6
|
const _topic = declared => declared['@topic'] || declared.name
|
|
6
7
|
|
|
@@ -18,7 +19,7 @@ const _warnAndStripTopicPrefix = event => {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// There's currently no mechanism to detect mocked services, this is the best we can do.
|
|
21
|
-
class MessagingService extends
|
|
22
|
+
class MessagingService extends OutboxService {
|
|
22
23
|
init() {
|
|
23
24
|
// enables queued async operations (without awaiting)
|
|
24
25
|
this.queued = queued()
|
|
@@ -56,22 +57,6 @@ class MessagingService extends cds.Service {
|
|
|
56
57
|
})
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
// if outbox is switched on, decorate the emit method to actually do
|
|
60
|
-
// the emit only when the request succeeded
|
|
61
|
-
if (this.options.outbox) {
|
|
62
|
-
const { emit } = this
|
|
63
|
-
this.emit = function (...args) {
|
|
64
|
-
const context = this.context || cds.context
|
|
65
|
-
if (context && typeof context.on === 'function')
|
|
66
|
-
return context.on('succeeded', () =>
|
|
67
|
-
emit.call(this, ...args).catch(e => {
|
|
68
|
-
LOG._error && LOG.error(e)
|
|
69
|
-
})
|
|
70
|
-
)
|
|
71
|
-
return emit.call(this, ...args)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
60
|
const { on } = this
|
|
76
61
|
this.on = function (...args) {
|
|
77
62
|
if (Array.isArray(args[0])) {
|
|
@@ -80,6 +65,7 @@ class MessagingService extends cds.Service {
|
|
|
80
65
|
}
|
|
81
66
|
return on.call(this, ...args)
|
|
82
67
|
}
|
|
68
|
+
return super.init()
|
|
83
69
|
}
|
|
84
70
|
|
|
85
71
|
emit(event, data, headers) {
|
|
@@ -114,22 +100,22 @@ class MessagingService extends cds.Service {
|
|
|
114
100
|
}
|
|
115
101
|
}
|
|
116
102
|
|
|
117
|
-
message4(
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (!
|
|
121
|
-
if (!
|
|
122
|
-
|
|
123
|
-
this.prepareHeaders(
|
|
124
|
-
|
|
103
|
+
message4(msg) {
|
|
104
|
+
const _msg = { ...msg }
|
|
105
|
+
_msg.event = _warnAndStripTopicPrefix(_msg.event)
|
|
106
|
+
if (!_msg.headers) _msg.headers = {}
|
|
107
|
+
if (!_msg.inbound) {
|
|
108
|
+
_msg.headers = { ..._msg.headers } // don't change the original object
|
|
109
|
+
this.prepareHeaders(_msg.headers, _msg.event)
|
|
110
|
+
_msg.event = this.prepareTopic(_msg.event, false)
|
|
125
111
|
} else if (this.subscribedTopics) {
|
|
126
112
|
const subscribedEvent =
|
|
127
|
-
this.subscribedTopics.get(
|
|
128
|
-
(this.wildcarded && this.subscribedTopics.get(this.wildcarded(
|
|
129
|
-
if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${
|
|
130
|
-
|
|
113
|
+
this.subscribedTopics.get(_msg.event) ||
|
|
114
|
+
(this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
|
|
115
|
+
if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
|
|
116
|
+
_msg.event = subscribedEvent
|
|
131
117
|
}
|
|
132
|
-
return
|
|
118
|
+
return _msg
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
|
|
@@ -185,6 +185,21 @@ class RemoteService extends cds.Service {
|
|
|
185
185
|
})
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// FIXME: This is a dirty hack for this situation:
|
|
189
|
+
// - This PR has cds.Service.model setter to always consistently apply cds.compile.for.odata, also for RemoteServices, which wasn't the case before
|
|
190
|
+
// - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavoir of RemoteServices
|
|
191
|
+
// NOTE: that test would never have worked for RemoteServices bootstrapped from single cds.model, which is always cds.compiled.for.odata
|
|
192
|
+
// REVISIT: should become obsolete with Universal CSN
|
|
193
|
+
set model(m) {
|
|
194
|
+
const fn = cds.compile.for.odata
|
|
195
|
+
try {
|
|
196
|
+
cds.compile.for.odata = m => m
|
|
197
|
+
super.model = m
|
|
198
|
+
} finally {
|
|
199
|
+
cds.compile.for.odata = fn
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
188
203
|
// Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
|
|
189
204
|
// Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
|
|
190
205
|
async handle(req) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const LOG = cds.log('remote')
|
|
3
3
|
|
|
4
|
+
const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
5
|
+
|
|
4
6
|
const cdsLocale = require('../../../../lib/req/locale')
|
|
5
7
|
|
|
6
|
-
const { convertV2ResponseData } = require('./
|
|
8
|
+
const { convertV2ResponseData, deepSanitize } = require('./data')
|
|
7
9
|
|
|
8
10
|
let _cloudSdkCore
|
|
9
11
|
|
|
@@ -16,6 +18,12 @@ const PPPD = {
|
|
|
16
18
|
|
|
17
19
|
const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
|
|
18
20
|
|
|
21
|
+
const _sanitizeHeaders = headers => {
|
|
22
|
+
if (headers && headers.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ...'
|
|
23
|
+
|
|
24
|
+
return headers
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
|
|
20
28
|
const { getDestination, executeHttpRequest } = cloudSdkCore()
|
|
21
29
|
|
|
@@ -41,6 +49,11 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
41
49
|
requestOptions = { fetchCsrfToken: true }
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
LOG._debug &&
|
|
53
|
+
LOG.debug(`${requestConfig.method} ${destination.url}${requestConfig.url}`, {
|
|
54
|
+
headers: _sanitizeHeaders({ ...requestConfig.headers }),
|
|
55
|
+
data: requestConfig.data && SANITIZE_VALUES ? deepSanitize(requestConfig.data) : requestConfig.data
|
|
56
|
+
})
|
|
44
57
|
return executeHttpRequest(destination, requestConfig, requestOptions)
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -194,8 +207,7 @@ const _getSanitizedError = (e, reqOptions, suppressRemoteResponseBody) => {
|
|
|
194
207
|
if (correlationId) e.correlationId = correlationId
|
|
195
208
|
|
|
196
209
|
// sanitize authorization
|
|
197
|
-
|
|
198
|
-
e.request.headers.authorization = e.request.headers.authorization.split(' ')[0] + ' ...'
|
|
210
|
+
_sanitizeHeaders(e.request.headers)
|
|
199
211
|
|
|
200
212
|
// delete functions and complex objects in config
|
|
201
213
|
for (const k in e) if (typeof e[k] === 'function') delete e[k]
|
|
@@ -49,7 +49,6 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
|
|
|
49
49
|
return record
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// eslint-disable-next-line complexity
|
|
53
52
|
const _convertValue = (value, type, ieee754Compatible) => {
|
|
54
53
|
if (value == null) {
|
|
55
54
|
return value
|
|
@@ -121,6 +120,17 @@ const convertV2ResponseData = (data, target, ieee754Compatible) => {
|
|
|
121
120
|
return _convertData(data, target, ieee754Compatible)
|
|
122
121
|
}
|
|
123
122
|
|
|
123
|
+
const deepSanitize = arg => {
|
|
124
|
+
if (Array.isArray(arg)) return arg.map(deepSanitize)
|
|
125
|
+
if (typeof arg === 'object')
|
|
126
|
+
return Object.keys(arg).reduce((acc, cur) => {
|
|
127
|
+
acc[cur] = deepSanitize(arg[cur])
|
|
128
|
+
return acc
|
|
129
|
+
}, {})
|
|
130
|
+
return '***'
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
module.exports = {
|
|
125
|
-
convertV2ResponseData
|
|
134
|
+
convertV2ResponseData,
|
|
135
|
+
deepSanitize
|
|
126
136
|
}
|
|
@@ -45,12 +45,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
45
45
|
this.dbcs = new Map()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
set model(csn) {
|
|
49
|
-
const m = csn && 'definitions' in csn ? cds.linked(cds.compile.for.odata(csn)) : csn
|
|
50
|
-
cds.alpha_localized(m)
|
|
51
|
-
super.model = m
|
|
52
|
-
}
|
|
53
|
-
|
|
54
48
|
init() {
|
|
55
49
|
this._registerBeforeHandlers()
|
|
56
50
|
this._registerOnHandlers()
|
|
@@ -114,8 +108,12 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
114
108
|
* connection
|
|
115
109
|
*/
|
|
116
110
|
async acquire(arg) {
|
|
117
|
-
//
|
|
118
|
-
|
|
111
|
+
// in non-multi-tenant scenarios, the default db should be returned regardless of arg
|
|
112
|
+
let tenant = 'anonymous'
|
|
113
|
+
if (this.options.multiTenant && arg) {
|
|
114
|
+
if (typeof arg === 'string') tenant = arg
|
|
115
|
+
else tenant = arg.tenant || (arg.user && arg.user.tenant) || tenant // > REVISIT: remove fallback arg.user.tenant with cds^6
|
|
116
|
+
}
|
|
119
117
|
|
|
120
118
|
let dbc = this.dbcs.get(tenant)
|
|
121
119
|
if (!dbc) {
|
|
@@ -129,9 +127,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
129
127
|
dbc = await _new(dbUrl)
|
|
130
128
|
|
|
131
129
|
dbc._queued = []
|
|
132
|
-
dbc._tenant = tenant
|
|
133
130
|
|
|
134
|
-
if (cds.env.features.
|
|
131
|
+
if (cds.env.features._db_foreign_key_constraints) {
|
|
135
132
|
await new Promise((resolve, reject) => {
|
|
136
133
|
dbc.exec('PRAGMA foreign_keys = ON', err => {
|
|
137
134
|
if (err) reject(err)
|
|
@@ -18,6 +18,25 @@ class CustomExpressionBuilder extends ExpressionBuilder {
|
|
|
18
18
|
Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })
|
|
19
19
|
return FunctionBuilder
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
_addListToOutputObj(list) {
|
|
23
|
+
this._outputObj.sql.push('(')
|
|
24
|
+
|
|
25
|
+
// sqlite requires "VALUES" keyword for list of list of values
|
|
26
|
+
if (list[0] && list[0].list && list[0].list[0] && 'val' in list[0].list[0]) {
|
|
27
|
+
this._outputObj.sql.push('VALUES')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
31
|
+
this._expressionElementToSQL(list[i])
|
|
32
|
+
|
|
33
|
+
if (len > 1 && i + 1 < len) {
|
|
34
|
+
this._outputObj.sql.push(',')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._outputObj.sql.push(')')
|
|
39
|
+
}
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
module.exports = CustomExpressionBuilder
|
|
@@ -2,7 +2,7 @@ const { SQLITE_TYPE_CONVERSION_MAP } = require('./conversion')
|
|
|
2
2
|
const CustomBuilder = require('./customBuilder')
|
|
3
3
|
const { sqlFactory } = require('../db/sql-builder/')
|
|
4
4
|
const { getPostProcessMapper, postProcess } = require('../db/data-conversion/post-processing')
|
|
5
|
-
const { createJoinCQNFromExpanded, hasExpand, rawToExpanded } = require('../db/expand')
|
|
5
|
+
const { createJoinCQNFromExpanded, hasExpand, rawToExpanded, expandV2 } = require('../db/expand')
|
|
6
6
|
const { Readable } = require('stream')
|
|
7
7
|
|
|
8
8
|
const cds = require('../cds')
|
|
@@ -10,6 +10,8 @@ const LOG = cds.log('sqlite|db|sql')
|
|
|
10
10
|
// && {_debug:true, debug(sql){ cds._debug && console.log(sql+';\n') }} //> please keep that for debugging stakeholder tests
|
|
11
11
|
const { inspect } = require('util')
|
|
12
12
|
|
|
13
|
+
const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
|
|
14
|
+
|
|
13
15
|
/*
|
|
14
16
|
* capture stack trace on the way to exec to know origin
|
|
15
17
|
* -> very expensive
|
|
@@ -32,20 +34,21 @@ const _colored = {
|
|
|
32
34
|
COMMIT: '\x1b[1m\x1b[32mCOMMIT\x1b[0m',
|
|
33
35
|
ROLLBACK: '\x1b[1m\x1b[91mROLLBACK\x1b[0m'
|
|
34
36
|
}
|
|
35
|
-
const _augmented = (err, sql, o) => {
|
|
37
|
+
const _augmented = (err, sql, values, o) => {
|
|
36
38
|
err.query = sql
|
|
39
|
+
if (values) err.values = SANITIZE_VALUES ? ['***'] : values
|
|
37
40
|
err.message += ' in: \n' + sql
|
|
38
41
|
if (o) err.stack = err.message + o.stack.slice(5)
|
|
39
42
|
return err
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
function _executeSimpleSQL(dbc, sql, values) {
|
|
43
|
-
LOG._debug && LOG.debug(_colored[sql] || sql, values
|
|
46
|
+
LOG._debug && LOG.debug(_colored[sql] || sql, Array.isArray(values) ? (SANITIZE_VALUES ? ['***'] : values) : '')
|
|
44
47
|
|
|
45
48
|
return new Promise((resolve, reject) => {
|
|
46
49
|
const o = _captureStack()
|
|
47
50
|
dbc.run(sql, values, function (err) {
|
|
48
|
-
if (err) return reject(_augmented(err, sql, o))
|
|
51
|
+
if (err) return reject(_augmented(err, sql, values, o))
|
|
49
52
|
|
|
50
53
|
resolve(this.changes)
|
|
51
54
|
})
|
|
@@ -53,12 +56,12 @@ function _executeSimpleSQL(dbc, sql, values) {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function executeSelectSQL(dbc, sql, values, isOne, postMapper) {
|
|
56
|
-
LOG._debug && LOG.debug(sql, values)
|
|
59
|
+
LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
|
|
57
60
|
|
|
58
61
|
return new Promise((resolve, reject) => {
|
|
59
62
|
const o = _captureStack()
|
|
60
63
|
dbc[isOne ? 'get' : 'all'](sql, values, (err, result) => {
|
|
61
|
-
if (err) return reject(_augmented(err, sql, o))
|
|
64
|
+
if (err) return reject(_augmented(err, sql, values, o))
|
|
62
65
|
|
|
63
66
|
// REVISIT
|
|
64
67
|
// .get returns undefined if nothing in db
|
|
@@ -96,6 +99,10 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
|
|
|
96
99
|
|
|
97
100
|
function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
98
101
|
if (hasExpand(query)) {
|
|
102
|
+
// expand: '**' or '*3' is handled by new impl
|
|
103
|
+
if (query.SELECT.columns.some(c => c.expand && typeof c.expand === 'string' && /^\*{1}[\d|*]+/.test(c.expand))) {
|
|
104
|
+
return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
|
|
105
|
+
}
|
|
99
106
|
return _processExpand(model, dbc, query, user, locale, txTimestamp)
|
|
100
107
|
}
|
|
101
108
|
const { sql, values = [] } = sqlFactory(
|
|
@@ -138,10 +145,10 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
|
|
|
138
145
|
return reject(new Error(`Cannot execute SQL statement. Invalid values provided: ${inspect(values)}`))
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
LOG._debug && LOG.debug(sql, values)
|
|
148
|
+
LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
|
|
142
149
|
const o = _captureStack()
|
|
143
150
|
const stmt = dbc.prepare(sql, err => {
|
|
144
|
-
if (err) return reject(_augmented(err, sql, o))
|
|
151
|
+
if (err) return reject(_augmented(err, sql, values, o))
|
|
145
152
|
|
|
146
153
|
if (!Array.isArray(values[0])) values = [values]
|
|
147
154
|
|
|
@@ -155,11 +162,10 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
|
|
|
155
162
|
i++
|
|
156
163
|
stmt.run(each, function (err) {
|
|
157
164
|
if (err) {
|
|
158
|
-
err.values = each
|
|
159
165
|
if (!isFinalized) {
|
|
160
166
|
isFinalized = true
|
|
161
167
|
stmt.finalize()
|
|
162
|
-
return reject(_augmented(err, sql, o))
|
|
168
|
+
return reject(_augmented(err, sql, each, o))
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -209,12 +215,12 @@ function executeInsertSQL(dbc, sql, values, query) {
|
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
|
|
212
|
-
LOG._debug && LOG.debug(sql, values)
|
|
218
|
+
LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
|
|
213
219
|
|
|
214
220
|
return new Promise((resolve, reject) => {
|
|
215
221
|
const o = _captureStack()
|
|
216
222
|
dbc.run(sql, values, function (err) {
|
|
217
|
-
if (err) return reject(_augmented(err, sql, o))
|
|
223
|
+
if (err) return reject(_augmented(err, sql, values, o))
|
|
218
224
|
|
|
219
225
|
// InsertResult needs an object per row with its values
|
|
220
226
|
if (query && values.length > 0) {
|
|
@@ -101,7 +101,8 @@
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* @typedef {object} cqn2cqn4sqlOptions
|
|
104
|
-
* @property {boolean} suppressSearch=false Indicates whether the search handler is called.
|
|
104
|
+
* @property {boolean} [suppressSearch=false] Indicates whether the search handler is called.
|
|
105
|
+
* @property {import('../db/Service')} [service]
|
|
105
106
|
*/
|
|
106
107
|
|
|
107
108
|
module.exports = {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../_runtime/cds')
|
|
2
2
|
|
|
3
|
-
const { where2obj } = require('
|
|
4
|
-
const { findCsnTargetFor } = require('
|
|
3
|
+
const { where2obj } = require('../_runtime/common/utils/cqn')
|
|
4
|
+
const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
|
|
5
5
|
|
|
6
6
|
function _keysOf(entity) {
|
|
7
7
|
return entity && entity.keys
|
|
@@ -11,11 +11,11 @@ function _keysOf(entity) {
|
|
|
11
11
|
: []
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function _getDefinition(definition, name) {
|
|
14
|
+
function _getDefinition(definition, name, namespace) {
|
|
15
15
|
return (
|
|
16
16
|
(definition.definitions && definition.definitions[name]) ||
|
|
17
17
|
(definition.elements && definition.elements[name]) ||
|
|
18
|
-
(definition.actions && definition.actions[name]) ||
|
|
18
|
+
(definition.actions && (definition.actions[name] || definition.actions[name.replace(namespace + '.', '')])) ||
|
|
19
19
|
definition[name]
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -29,7 +29,7 @@ function _resolveAliasInWhere(where, entity) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// case: single key without name, e.g., Foo(1)
|
|
32
|
-
function
|
|
32
|
+
function addRefToWhereIfNecessary(where, entity) {
|
|
33
33
|
if (!where || where.length !== 1) return 0
|
|
34
34
|
const keys = _keysOf(entity)
|
|
35
35
|
if (keys.length !== 1) return 0
|
|
@@ -37,8 +37,9 @@ function _addRefToWhereIfNecessary(where, entity) {
|
|
|
37
37
|
return 1
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function _processSegments(cqn, model) {
|
|
41
|
-
const
|
|
40
|
+
function _processSegments(cqn, model, namespace) {
|
|
41
|
+
const from = _resolveFrom(cqn.SELECT.from)
|
|
42
|
+
const ref = from.ref
|
|
42
43
|
|
|
43
44
|
let current = model
|
|
44
45
|
let path
|
|
@@ -79,7 +80,7 @@ function _processSegments(cqn, model) {
|
|
|
79
80
|
|
|
80
81
|
path = path ? path + `${path.match(/:/) ? '.' : ':'}${seg}` : seg
|
|
81
82
|
// REVISIT: replace use case: <namespace>.<entity>_history is at <namespace>.<entity>.history
|
|
82
|
-
current = _getDefinition(current, seg) || _getDefinition(current, seg.replace(/_/g, '.'))
|
|
83
|
+
current = _getDefinition(current, seg, namespace) || _getDefinition(current, seg.replace(/_/g, '.'), namespace)
|
|
83
84
|
// REVISIT: 404 or 400?
|
|
84
85
|
if (!current) cds.error(`Invalid resource path "${path}"`, { code: 404 })
|
|
85
86
|
|
|
@@ -88,7 +89,7 @@ function _processSegments(cqn, model) {
|
|
|
88
89
|
one = !!(ref[i].where || current._isSingleton)
|
|
89
90
|
incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
|
|
90
91
|
if (ref[i].where) {
|
|
91
|
-
keyCount +=
|
|
92
|
+
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
92
93
|
_resolveAliasInWhere(ref[i].where, current)
|
|
93
94
|
}
|
|
94
95
|
} else if ({ action: 1, function: 1 }[current.kind]) {
|
|
@@ -106,7 +107,7 @@ function _processSegments(cqn, model) {
|
|
|
106
107
|
incompleteKeys = one || i === ref.length - 1 ? false : true
|
|
107
108
|
current = model.definitions[current.target]
|
|
108
109
|
if (ref[i].where) {
|
|
109
|
-
keyCount +=
|
|
110
|
+
keyCount += addRefToWhereIfNecessary(ref[i].where, current)
|
|
110
111
|
_resolveAliasInWhere(ref[i].where, current)
|
|
111
112
|
}
|
|
112
113
|
} else if (current._isStructured) {
|
|
@@ -133,7 +134,7 @@ function _processSegments(cqn, model) {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
// remove all nulled refs
|
|
136
|
-
|
|
137
|
+
from.ref = ref.filter(r => r)
|
|
137
138
|
|
|
138
139
|
// one?
|
|
139
140
|
if (one) cqn.SELECT.one = true
|
|
@@ -143,11 +144,13 @@ function _processSegments(cqn, model) {
|
|
|
143
144
|
cqn.__target = current
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
const _resolveFrom = from => (from.SELECT ? _resolveFrom(from.SELECT.from) : from)
|
|
148
|
+
|
|
146
149
|
function _4service(service) {
|
|
147
150
|
const { namespace, model } = service
|
|
148
151
|
|
|
149
152
|
return cqn => {
|
|
150
|
-
const { ref } = cqn.SELECT.from
|
|
153
|
+
const { ref } = _resolveFrom(cqn.SELECT.from)
|
|
151
154
|
|
|
152
155
|
// REVISIT: shouldn't be necessary
|
|
153
156
|
/*
|
|
@@ -162,7 +165,7 @@ function _4service(service) {
|
|
|
162
165
|
/*
|
|
163
166
|
* key vs. path segments (/Books/1/author/books/2/...) and more
|
|
164
167
|
*/
|
|
165
|
-
_processSegments(cqn, model)
|
|
168
|
+
_processSegments(cqn, model, namespace)
|
|
166
169
|
|
|
167
170
|
return cqn
|
|
168
171
|
}
|
|
@@ -174,5 +177,6 @@ module.exports = {
|
|
|
174
177
|
for: service => {
|
|
175
178
|
if (!cache.has(service)) cache.set(service, _4service(service))
|
|
176
179
|
return cache.get(service)
|
|
177
|
-
}
|
|
180
|
+
},
|
|
181
|
+
addRefToWhereIfNecessary
|
|
178
182
|
}
|