@sap/cds 6.4.0 → 6.5.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +59 -3
  2. package/apis/cds.d.ts +2 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +12 -8
  5. package/apis/services.d.ts +39 -64
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -12
  8. package/bin/build/buildTaskHandler.js +3 -14
  9. package/bin/build/index.js +8 -2
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -7
  11. package/bin/build/provider/hana/template/package.json +3 -0
  12. package/bin/build/provider/mtx/resourcesTarBuilder.js +13 -4
  13. package/bin/build/provider/mtx-extension/index.js +41 -38
  14. package/bin/build/util.js +17 -0
  15. package/bin/deploy/to-hana/hdiDeployUtil.js +11 -5
  16. package/bin/serve.js +6 -2
  17. package/common.cds +7 -0
  18. package/lib/auth/index.js +17 -15
  19. package/lib/auth/jwt-auth.js +4 -3
  20. package/lib/compile/for/lean_drafts.js +1 -1
  21. package/lib/compile/minify.js +3 -3
  22. package/lib/core/index.js +1 -0
  23. package/lib/dbs/cds-deploy.js +13 -10
  24. package/lib/env/cds-requires.js +1 -1
  25. package/lib/env/defaults.js +5 -1
  26. package/lib/env/schemas/cds-rc.json +74 -3
  27. package/lib/lazy.js +6 -8
  28. package/lib/log/cds-error.js +2 -2
  29. package/lib/ql/Whereable.js +22 -11
  30. package/lib/ql/cds-ql.js +1 -1
  31. package/lib/req/response.js +8 -3
  32. package/lib/req/user.js +12 -2
  33. package/lib/srv/middlewares/cds-context.js +0 -2
  34. package/lib/srv/middlewares/ctx-auth.js +11 -0
  35. package/lib/srv/middlewares/ctx-model.js +22 -20
  36. package/lib/srv/middlewares/index.js +7 -9
  37. package/lib/srv/protocols/_legacy.js +4 -0
  38. package/lib/srv/protocols/graphql.js +2 -2
  39. package/lib/srv/protocols/index.js +7 -3
  40. package/lib/srv/srv-api.js +1 -0
  41. package/lib/srv/srv-models.js +6 -1
  42. package/lib/utils/cds-utils.js +3 -1
  43. package/lib/utils/data.js +2 -2
  44. package/lib/utils/tar.js +37 -12
  45. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  46. package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
  47. package/libx/_runtime/auth/strategies/mock.js +12 -1
  48. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  49. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  51. package/libx/_runtime/cds-services/services/Service.js +3 -0
  52. package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
  53. package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
  54. package/libx/_runtime/common/code-ext/config.js +13 -0
  55. package/libx/_runtime/common/code-ext/execute.js +106 -0
  56. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  57. package/libx/_runtime/common/code-ext/worker.js +36 -0
  58. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  59. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
  60. package/libx/_runtime/common/generic/crud.js +5 -1
  61. package/libx/_runtime/common/generic/paging.js +8 -7
  62. package/libx/_runtime/common/i18n/index.js +1 -1
  63. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -11
  64. package/libx/_runtime/common/utils/path.js +5 -25
  65. package/libx/_runtime/common/utils/resolveView.js +2 -0
  66. package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
  67. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
  68. package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
  69. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +9 -32
  70. package/libx/_runtime/db/sql-builder/annotations.js +6 -3
  71. package/libx/_runtime/db/utils/localized.js +1 -1
  72. package/libx/_runtime/fiori/generic/activate.js +4 -0
  73. package/libx/_runtime/fiori/generic/before.js +8 -1
  74. package/libx/_runtime/fiori/generic/edit.js +5 -0
  75. package/libx/_runtime/fiori/generic/read.js +8 -3
  76. package/libx/_runtime/fiori/lean-draft.js +12 -1
  77. package/libx/_runtime/hana/Service.js +1 -1
  78. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  79. package/libx/_runtime/hana/execute.js +5 -5
  80. package/libx/_runtime/hana/pool.js +1 -1
  81. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  82. package/libx/_runtime/sqlite/Service.js +1 -1
  83. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +20 -38
  84. package/libx/odata/afterburner.js +6 -3
  85. package/libx/odata/cqn2odata.js +1 -1
  86. package/libx/rest/middleware/parse.js +26 -4
  87. package/package.json +1 -1
  88. package/server.js +2 -20
@@ -0,0 +1,106 @@
1
+ const cds = require('../../cds')
2
+ const { Worker } = require('worker_threads')
3
+ const path = require('node:path')
4
+ const { timeout, resourceLimits } = require('./config')
5
+ const workerPath = path.resolve(__dirname, 'worker.js')
6
+ const { Errors } = require('../../../../lib/req/response')
7
+
8
+ const _getReqData = req => {
9
+ return {
10
+ data: req.data,
11
+ params: req.params,
12
+ results: req.results,
13
+ messages: req.messages,
14
+ errors: req.errors ?? new Errors()
15
+ }
16
+ }
17
+
18
+ module.exports = async function executeCode(code, req) {
19
+ const reqData = _getReqData(req)
20
+ const _getTarget = target => {
21
+ switch (target) {
22
+ case 'srv':
23
+ return this
24
+
25
+ case 'req':
26
+ return req
27
+
28
+ // no default
29
+ }
30
+ }
31
+ const workerId = cds.utils.uuid()
32
+ const worker = new Worker(workerPath, {
33
+ workerData: { id: workerId },
34
+ resourceLimits
35
+ })
36
+
37
+ const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
38
+ worker.on('online', onStarted)
39
+ worker.on('message', onMessageReceived)
40
+ worker.on('error', onError)
41
+ worker.on('exit', onExit)
42
+
43
+ let onStartTimeoutID
44
+
45
+ function onStarted() {
46
+ onStartTimeoutID = setTimeout(() => {
47
+ worker.terminate()
48
+ reject(new Error(`Script execution timed out after ${timeout}ms`))
49
+ }, timeout)
50
+ }
51
+
52
+ function onMessageReceived(message) {
53
+ switch (message.kind) {
54
+ case 'run':
55
+ run(message)
56
+ return
57
+
58
+ case 'success':
59
+ onSuccess(message)
60
+ cleanup()
61
+ return
62
+
63
+ case 'error':
64
+ onError(message.error)
65
+ return
66
+
67
+ // no default
68
+ }
69
+ }
70
+
71
+ async function onSuccess(message) {
72
+ for (const m of message.postMessages) await run(m)
73
+ req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
74
+ req.results = message.req.results
75
+ resolve(req.results ?? message.result)
76
+ }
77
+
78
+ function onError(error) {
79
+ reject(error)
80
+ }
81
+
82
+ function onExit(exitCode) {
83
+ if (exitCode !== 0) reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
84
+ }
85
+
86
+ async function run(message) {
87
+ try {
88
+ let result = _getTarget(message.target)[message.prop](...message.args)
89
+ if (typeof result?.then === 'function') result = await result
90
+ if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
91
+ } catch (error) {
92
+ worker.postMessage({ id: message.id, kind: 'cleanup' })
93
+ reject(error)
94
+ }
95
+ }
96
+
97
+ function cleanup() {
98
+ clearTimeout(onStartTimeoutID)
99
+ worker.terminate()
100
+ }
101
+ })
102
+
103
+ // triggers execution of the code in the worker thread
104
+ worker.postMessage({ id: workerId, code, reqData })
105
+ return executePromise
106
+ }
@@ -0,0 +1,49 @@
1
+ const cds = require('../../cds')
2
+ const executeCode = require('./execute')
3
+
4
+ const CODE_ANNOTATION = '@extension.code'
5
+
6
+ module.exports = cds.service.impl(function () {
7
+ const getCodeFromAnnotation = async (defName, operation, registration) => {
8
+ // REVISIT: tenant info in not in this.model and cds.context.model is undefined for single tenancy
9
+ const model = cds.context.model || this.model
10
+ const el = model.definitions[defName]
11
+ const boundEl = el.actions?.[operation]
12
+ const extensionCode = boundEl?.[CODE_ANNOTATION] ?? el[CODE_ANNOTATION]
13
+ if (extensionCode) {
14
+ const annotation = extensionCode.filter(element => element[registration] === operation)
15
+ return annotation.length && annotation[0].code
16
+ }
17
+ }
18
+
19
+ this.after('READ', async function (result, req) {
20
+ if (result == null) return // whether result is null or undefined
21
+ const code = await getCodeFromAnnotation(req.target.name, req.event, 'after')
22
+ if (!code) return
23
+ await executeCode.call(this, code, req)
24
+ })
25
+
26
+ this.before(['CREATE', 'UPDATE', 'DELETE'], async function (req) {
27
+ const code = await getCodeFromAnnotation(req.target.name, req.event, 'before')
28
+ if (!code) return
29
+ await executeCode.call(this, code, req)
30
+ })
31
+
32
+ this.on('*', async function (req, next) {
33
+ if (this.name.startsWith('cds.xt')) return next()
34
+ // REVISIT: req.target -> wait until implementation task finished
35
+ let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
36
+ if (!fqn) {
37
+ if (req.target) return next()
38
+ fqn = this.model.definitions[`${this.name}.${req.event}`] // check for unbound action/function or event
39
+ }
40
+
41
+ // REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
42
+ // REVISIT: check whether action/function or event is part of an extension
43
+ if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
44
+ const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
45
+ if (!code) return
46
+ return await executeCode.call(this, code, req)
47
+ }
48
+ })
49
+ })
@@ -0,0 +1,36 @@
1
+ const { parentPort, workerData } = require('worker_threads')
2
+ const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
3
+ const WorkerReq = require('./WorkerReq')
4
+ const { timeout } = require('./config')
5
+
6
+ parentPort.once('message', function onMessageReceived({ id, code, reqData }) {
7
+ if (id !== workerData.id) return
8
+
9
+ // eslint-disable-next-line cds/no-missing-dependencies
10
+ const { VM } = require('vm2')
11
+ const workerReq = new WorkerReq(reqData)
12
+ const vm = new VM({
13
+ console: 'inherit',
14
+ timeout, // specifies the number of milliseconds to execute code before terminating execution
15
+ allowAsync: true,
16
+
17
+ // the sandbox represents the global object inside the vm instance
18
+ sandbox: {
19
+ req: workerReq,
20
+ SELECT: WorkerSELECT._api(),
21
+ INSERT: WorkerINSERT._api(),
22
+ UPSERT: WorkerUPSERT._api(),
23
+ UPDATE: WorkerUPDATE._api(),
24
+ DELETE: WorkerDELETE._api()
25
+ }
26
+ })
27
+
28
+ try {
29
+ ;(async function () {
30
+ const result = await vm.run(code)
31
+ parentPort.postMessage({ kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
32
+ })()
33
+ } catch (error) {
34
+ parentPort.postMessage({ kind: 'error', error })
35
+ }
36
+ })
@@ -0,0 +1,45 @@
1
+ const SELECT = require('../../../../lib/ql/SELECT')
2
+ const INSERT = require('../../../../lib/ql/INSERT')
3
+ const UPSERT = require('../../../../lib/ql/UPSERT')
4
+ const UPDATE = require('../../../../lib/ql/UPDATE')
5
+ const DELETE = require('../../../../lib/ql/DELETE')
6
+ const queryExecutor = require('./workerQueryExecutor')
7
+
8
+ class WorkerSELECT extends SELECT {
9
+ // intercept await SELECT.from(...) calls
10
+ then(r, e) {
11
+ return new Promise(queryExecutor.bind(this)).then(r, e)
12
+ }
13
+ }
14
+
15
+ class WorkerINSERT extends INSERT {
16
+ then(r, e) {
17
+ return new Promise(queryExecutor.bind(this)).then(r, e)
18
+ }
19
+ }
20
+
21
+ class WorkerUPSERT extends UPSERT {
22
+ then(r, e) {
23
+ return new Promise(queryExecutor.bind(this)).then(r, e)
24
+ }
25
+ }
26
+
27
+ class WorkerUPDATE extends UPDATE {
28
+ then(r, e) {
29
+ return new Promise(queryExecutor.bind(this)).then(r, e)
30
+ }
31
+ }
32
+
33
+ class WorkerDELETE extends DELETE {
34
+ then(r, e) {
35
+ return new Promise(queryExecutor.bind(this)).then(r, e)
36
+ }
37
+ }
38
+
39
+ Object.defineProperty(WorkerSELECT.prototype, 'cmd', { value: 'SELECT' })
40
+ Object.defineProperty(WorkerINSERT.prototype, 'cmd', { value: 'INSERT' })
41
+ Object.defineProperty(WorkerUPSERT.prototype, 'cmd', { value: 'UPSERT' })
42
+ Object.defineProperty(WorkerUPDATE.prototype, 'cmd', { value: 'UPDATE' })
43
+ Object.defineProperty(WorkerDELETE.prototype, 'cmd', { value: 'DELETE' })
44
+
45
+ module.exports = { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE }
@@ -0,0 +1,33 @@
1
+ const cds = require('../../cds')
2
+ const { parentPort } = require('worker_threads')
3
+ const executorCallbackMap = new Map()
4
+
5
+ parentPort.on('message', function onMessageReceived({ id, kind, result }) {
6
+ if (!executorCallbackMap.has(id)) return
7
+
8
+ switch (kind) {
9
+ case 'responseData':
10
+ executorCallbackMap.get(id)(result)
11
+ executorCallbackMap.delete(id)
12
+ return
13
+
14
+ case 'cleanup':
15
+ executorCallbackMap.delete(id)
16
+ return
17
+ }
18
+ })
19
+
20
+ function queryExecutor(resolve, reject) {
21
+ const id = cds.utils.uuid()
22
+ executorCallbackMap.set(id, result => resolve(result))
23
+ parentPort.postMessage({
24
+ id,
25
+ kind: 'run',
26
+ target: 'srv',
27
+ prop: 'run',
28
+ responseData: true,
29
+ args: [this]
30
+ })
31
+ }
32
+
33
+ module.exports = queryExecutor
@@ -26,7 +26,7 @@ const _targetEntityDoesNotExist = async req => {
26
26
 
27
27
  exports.impl = cds.service.impl(function () {
28
28
  // eslint-disable-next-line complexity
29
- this.on(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', async function (req) {
29
+ this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
30
30
  if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
31
31
  throw getError({
32
32
  code: 501,
@@ -61,6 +61,10 @@ exports.impl = cds.service.impl(function () {
61
61
  req.query.where(singleton)
62
62
  }
63
63
 
64
+ if (req.event === 'READ' && req.query?.SELECT) {
65
+ req.query.SELECT.localized = true
66
+ }
67
+
64
68
  if (!result) {
65
69
  result = await cds.tx(req).run(req.query, req.data)
66
70
  }
@@ -1,5 +1,5 @@
1
1
  const cds = require('../../cds')
2
- const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
2
+ const { getPageSize } = require('../utils/page')
3
3
 
4
4
  const commonGenericPaging = function (req) {
5
5
  // only if http request
@@ -11,13 +11,14 @@ const commonGenericPaging = function (req) {
11
11
  _addPaging(req.query, req.target)
12
12
  }
13
13
 
14
- const _addPaging = function (query, target) {
15
- let { rows, offset } = query.SELECT.limit || {}
16
- rows = rows && 'val' in rows ? rows.val : getDefaultPageSize(target)
17
- offset = offset && 'val' in offset ? offset.val : 0
18
- query.limit(...[Math.min(rows, getMaxPageSize(target)), offset])
14
+ const _addPaging = function ({ SELECT }, target) {
15
+ const { rows } = SELECT.limit || (SELECT.limit = {})
16
+ const conf = getPageSize(target)
17
+ SELECT.limit.rows = {
18
+ val: !rows ? conf.default : Math.min(rows.val ?? rows, conf.max)
19
+ }
19
20
  //Handle nested limits
20
- if (query.SELECT.from.SELECT?.limit) _addPaging(query.SELECT.from, target)
21
+ if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
21
22
  }
22
23
 
23
24
  /**
@@ -8,7 +8,7 @@ const dirs = (cds.env.i18n && cds.env.i18n.folders) || []
8
8
  const i18ns = {}
9
9
 
10
10
  function exists(args, locale) {
11
- const file = path.join(process.cwd(), ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
11
+ const file = path.join(cds.root, ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
12
12
  return fs.existsSync(file) ? file : undefined
13
13
  }
14
14
 
@@ -11,7 +11,7 @@ const search2cqn4sql = require('./search2cqn4sql')
11
11
  const { getEntityNameFromCQN } = require('./entityFromCqn')
12
12
  const getError = require('../../common/error')
13
13
  const { rewriteAsterisks } = require('./rewriteAsterisks')
14
- const { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
14
+ const { getEntityFromPath } = require('../../common/utils/path')
15
15
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
16
16
  const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
17
17
  const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
@@ -445,7 +445,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
445
445
  if (currentTarget) {
446
446
  queryTarget = getEntityFromPath({ ref }, currentTarget)
447
447
  } else {
448
- queryTarget = getEntityFromPath(getPathFromRef(ref), model)
448
+ queryTarget = getEntityFromPath({ ref }, model)
449
449
  outerAlias = as || PARENT_ALIAS + lambdaIteration
450
450
  innerAlias = FOREIGN_ALIAS + lambdaIteration
451
451
  }
@@ -604,14 +604,19 @@ const _convertExpand = expand => {
604
604
  })
605
605
  }
606
606
 
607
- const _convertRefWhereInExpand = columns => {
608
- if (!columns) return
607
+ const _simplifyWhere = col => {
608
+ if (col.ref?.[0].where) {
609
+ col.where = col.ref[0].where
610
+ col.ref[0] = col.ref[0].id
611
+ }
612
+ }
609
613
 
610
- columns.forEach(col => {
611
- if (col.expand && typeof col.expand !== 'string') {
612
- _convertExpand(col.expand)
613
- }
614
- })
614
+ const _simplifyWhereInColumns = columns => {
615
+ if (!columns) return
616
+ for (const col of columns) {
617
+ _simplifyWhere(col)
618
+ if (col.expand) _simplifyWhereInColumns(col.expand)
619
+ }
615
620
  }
616
621
 
617
622
  const _convertPathExpression = (query, model, options = {}) => {
@@ -742,8 +747,8 @@ const _convertSelect = (query, model, _options) => {
742
747
  _convertToOneEqNullInFilter(query.SELECT, target)
743
748
  }
744
749
 
745
- // extract where clause if it is in column expand ref
746
- _convertRefWhereInExpand(query.SELECT.columns)
750
+ // extract where clause if it is in an expand column
751
+ _simplifyWhereInColumns(query.SELECT.columns)
747
752
 
748
753
  // REVISIT: The following operations only work for _one_ entity.
749
754
  // We must also enable them for joins etc.
@@ -804,6 +809,33 @@ const _convertSelect = (query, model, _options) => {
804
809
  return query
805
810
  }
806
811
 
812
+ const _convertUpsert = (query, model) => {
813
+ // resolve path expression
814
+ const resolvedIntoClause = _convertPathExpressionForInsert(query.UPSERT.into, model)
815
+
816
+ const target = model.definitions[resolvedIntoClause]
817
+ if (!target) {
818
+ // if there is no target, just return original query, as a copy is not deep anyways and all the sub items of query.UPSERT are referenced only anyways
819
+ return query
820
+ }
821
+
822
+ // overwrite only .into, foreign keys are already set
823
+ // 'a' added as placeholder since its overwritten by Object.assign below
824
+ const upsert = UPSERT.into('a')
825
+
826
+ // REVISIT flatten structured types, currently its done in SQL builder
827
+
828
+ // We add all previous properties ot the newly created query.
829
+ // Reason is to not lose the query API functionality
830
+ Object.assign(upsert.UPSERT, query.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
831
+
832
+ const resolved = resolveView(upsert, model, cds.db)
833
+ // required for deplyoing of extensions, not used anywhere else except UpsertBuilder
834
+ resolved._target = resolved.UPSERT?._transitions?.[0].target || query._target
835
+ // resolved._target = query._target
836
+ return resolved
837
+ }
838
+
807
839
  const _convertInsert = (query, model) => {
808
840
  // resolve path expression
809
841
  const resolvedIntoClause = _convertPathExpressionForInsert(query.INSERT.into, model)
@@ -950,6 +982,10 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
950
982
  return _convertInsert(query, model)
951
983
  }
952
984
 
985
+ if (query.UPSERT) {
986
+ return _convertUpsert(query, model)
987
+ }
988
+
953
989
  if (query.DELETE) {
954
990
  return _convertDelete(query, model, options)
955
991
  }
@@ -1,41 +1,21 @@
1
1
  const cds = require('../../cds')
2
2
  const { ensureNoDraftsSuffix } = require('./draft')
3
3
 
4
- /*
5
- * returns path like <service>.<entity>:<prop1>.<prop2> for ref = [{ id: '<service>.<entity>' }, '<prop1>', '<prop2>']
6
- */
7
- const getPathFromRef = ref => {
8
- const x = ref.reduce((acc, cur) => {
9
- acc += (acc ? ':' : '') + (cur.id ? cur.id : cur)
10
- return acc
11
- }, '')
12
- const y = x.split(':')
13
- let z = y.shift()
14
- if (y.length) z += ':' + y.join('.')
15
- return z
16
- }
17
-
18
4
  /*
19
5
  * returns the target entity for the given path
20
6
  */
21
7
  const getEntityFromPath = (path, def) => {
22
8
  let current = def.definitions ? { elements: def.definitions } : def
23
- path = typeof path === 'string' ? cds.parse.path(path) : path
24
- const segments = [...path.ref]
25
- while (segments.length) {
26
- let segment = segments.shift()
27
- if (segment.id && typeof segment.id === 'string') {
28
- segment.id = ensureNoDraftsSuffix(segment.id)
29
- } else if (typeof segment === 'string') {
30
- segment = ensureNoDraftsSuffix(segment)
31
- }
32
- current = current.elements[segment.id || segment]
9
+
10
+ let id
11
+ for (const segment of path.ref) {
12
+ id = ensureNoDraftsSuffix(segment.id || segment)
13
+ current = current.elements[id]
33
14
  if (current && current.target) current = current._target
34
15
  }
35
16
  return current
36
17
  }
37
18
 
38
19
  module.exports = {
39
- getPathFromRef,
40
20
  getEntityFromPath
41
21
  }
@@ -679,6 +679,8 @@ const findQueryTarget = q => {
679
679
  ? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
680
680
  : q.UPDATE
681
681
  ? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
682
+ : q.UPSERT
683
+ ? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
682
684
  : q.DELETE
683
685
  ? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
684
686
  : undefined
@@ -15,20 +15,24 @@ const search2cqn4sql = (query, model, options = {}) => {
15
15
  const { search2cqn4sql } = options
16
16
  const { entityName, alias } = _targetFrom(query.SELECT.from, options)
17
17
  const entity = model.definitions[entityName]
18
- const columns = computeColumnsToBeSearched(query, entity, alias)
19
-
18
+ const localizedAssociation = entity.associations?.localized
20
19
  // Call custom (optimized search to cqn for sql implementation) that tries
21
20
  // to optimize the search behavior for a specific database service.
22
21
  // REVISIT: $search query option combined with $count is not currently optimized
23
- if (typeof search2cqn4sql === 'function' && !query.SELECT.count) {
24
- const search2cqnOptions = { columns, locale: options.locale }
22
+ if (
23
+ typeof search2cqn4sql === 'function' &&
24
+ !query.SELECT.count &&
25
+ localizedAssociation &&
26
+ !(query._aggregated || /* new parser */ query.SELECT.groupBy)
27
+ ) {
28
+ const search2cqnOptions = { columns: computeColumnsToBeSearched(query, entity), locale: options.locale }
25
29
  return search2cqn4sql(query, entity, search2cqnOptions)
26
- }
30
+ } else {
31
+ const expression = searchToLike(cqnSearchPhrase, computeColumnsToBeSearched(query, entity, alias))
27
32
 
28
- const expression = searchToLike(cqnSearchPhrase, columns)
29
-
30
- // REVISIT: find out here if where or having must be used
31
- query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
33
+ // REVISIT: find out here if where or having must be used
34
+ query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
35
+ }
32
36
  }
33
37
 
34
38
  module.exports = search2cqn4sql
@@ -40,7 +40,8 @@ function getCqnCopy(readToOneCQN) {
40
40
 
41
41
  class JoinCQNFromExpanded {
42
42
  constructor(cqn, csn, locale) {
43
- this._SELECT = Object.assign({}, cqn.SELECT)
43
+ this._SELECT = {}
44
+ for (const prop in cqn.SELECT) this._SELECT[prop] = cqn.SELECT[prop]
44
45
  this._csn = csn
45
46
  // REVISIT: locale is only passed in case of sqlite -> bad coding
46
47
  if (cds.env.i18n.for_sqlite.includes(locale)) {
@@ -36,6 +36,10 @@ class InsertBuilder extends BaseBuilder {
36
36
  this._csn = csn
37
37
  }
38
38
 
39
+ annotatedColumns(entityName, csn) {
40
+ return getAnnotatedColumns(entityName, csn)
41
+ }
42
+
39
43
  /**
40
44
  * Builds an Object based on the properties of the CQN object.
41
45
  *
@@ -77,7 +81,7 @@ class InsertBuilder extends BaseBuilder {
77
81
  this._findUuidKeys(entityName)
78
82
 
79
83
  this._columnIndexesToDelete = []
80
- const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
84
+ const annotatedColumns = this.annotatedColumns(entityName, this._csn)
81
85
 
82
86
  if (this._obj.INSERT.columns) {
83
87
  this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
@@ -6,40 +6,17 @@ class UpsertBuilder extends InsertBuilder {
6
6
  super(obj, options, csn)
7
7
  }
8
8
 
9
+ annotatedColumns(entityName, csn) {
10
+ const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
11
+ return { insertAnnotatedColumns: updateAnnotatedColumns }
12
+ }
13
+
9
14
  // REVISIT: We need to copy over the implementation for annotation handling
10
15
  build() {
11
- this._outputObj = {
12
- sql: ['UPSERT'],
13
- values: []
14
- }
15
- this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
16
-
17
- const entityName = this._into()
18
-
19
- this._columnIndexesToDelete = []
20
- const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
21
- // hack: treat update annotations as insert because of sql builder impl
22
- if (annotatedColumns) {
23
- annotatedColumns.insertAnnotatedColumns = annotatedColumns.updateAnnotatedColumns
24
- }
25
-
26
- if (this._obj.INSERT.columns) {
27
- this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
28
- this._columns(annotatedColumns)
29
- }
30
-
31
- if (this._obj.INSERT.values || this._obj.INSERT.rows) {
32
- if (annotatedColumns && !this._obj.INSERT.columns) {
33
- // if columns not provided get indexes from csn
34
- this._getAnnotatedColumnIndexes(annotatedColumns)
35
- }
36
-
37
- this._values(annotatedColumns)
38
- } else if (this._obj.INSERT.entries && this._obj.INSERT.entries.length !== 0) {
39
- this._entries(annotatedColumns)
40
- }
41
-
42
- this._outputObj.sql = this._outputObj.sql.join(' ') + ' WITH PRIMARY KEY'
16
+ this._obj = { INSERT: this._obj.UPSERT }
17
+ super.build()
18
+ this._outputObj.sql = this._outputObj.sql.replace('INSERT INTO', 'UPSERT')
19
+ this._outputObj.sql += ' WITH PRIMARY KEY'
43
20
  return this._outputObj
44
21
  }
45
22
  }
@@ -16,7 +16,10 @@ const _getAnnotationNames = column => {
16
16
  const getAnnotatedColumns = (entityName, csn) => {
17
17
  const entityNameWithoutSuffix = ensureNoDraftsSuffix(entityName)
18
18
  if (!csn || !csn.definitions[entityNameWithoutSuffix]) {
19
- return undefined
19
+ return {
20
+ insertAnnotatedColumns: new Map(),
21
+ updateAnnotatedColumns: new Map()
22
+ }
20
23
  }
21
24
  const columns = getColumns(csn.definitions[entityNameWithoutSuffix])
22
25
  const insertAnnotatedColumns = new Map()
@@ -39,8 +42,8 @@ const getAnnotatedColumns = (entityName, csn) => {
39
42
  }
40
43
 
41
44
  return {
42
- insertAnnotatedColumns: insertAnnotatedColumns,
43
- updateAnnotatedColumns: updateAnnotatedColumns
45
+ insertAnnotatedColumns,
46
+ updateAnnotatedColumns
44
47
  }
45
48
  }
46
49
 
@@ -11,7 +11,7 @@ const _redirectXpr = (xpr, localize) => {
11
11
  }
12
12
 
13
13
  if (ele.SELECT) {
14
- redirect(ele.SELECT, localize)
14
+ if (!ele._suppressLocalization) redirect(ele.SELECT, localize)
15
15
  }
16
16
  })
17
17
  }
@@ -174,6 +174,10 @@ const fioriGenericActivate = async function (req) {
174
174
  })
175
175
  ])
176
176
 
177
+ // REVISIT: we need to use okra API here because it must be set in the batched request
178
+ // status code must be set in handler to allow overriding for FE V2
179
+ req?._?.odataRes.setStatusCode(201)
180
+
177
181
  return result
178
182
  }
179
183