@sap/cds 5.6.1 → 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.
Files changed (184) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/_i18n/i18n_fr.properties +4 -4
  3. package/apis/cds.d.ts +7 -10
  4. package/apis/connect.d.ts +3 -3
  5. package/apis/core.d.ts +2 -4
  6. package/apis/models.d.ts +2 -3
  7. package/apis/ql.d.ts +0 -1
  8. package/apis/services.d.ts +7 -3
  9. package/bin/build/buildTaskFactory.js +16 -10
  10. package/bin/build/buildTaskProviderFactory.js +3 -3
  11. package/bin/build/constants.js +2 -1
  12. package/bin/build/provider/buildTaskProviderInternal.js +14 -14
  13. package/bin/build/provider/hana/2migration.js +2 -3
  14. package/bin/build/provider/hana/index.js +34 -0
  15. package/bin/build/provider/hana/migrationtable.js +90 -22
  16. package/bin/build/provider/hana/template/undeploy.json +5 -0
  17. package/bin/build/provider/node-cf/index.js +9 -2
  18. package/bin/serve.js +16 -18
  19. package/lib/compile/cdsc.js +15 -5
  20. package/lib/compile/etc/_localized.js +4 -4
  21. package/lib/compile/extend.js +8 -0
  22. package/lib/compile/index.js +3 -1
  23. package/lib/compile/minify.js +61 -0
  24. package/lib/compile/resolve.js +4 -1
  25. package/lib/compile/to/gql.js +9 -0
  26. package/lib/compile/to/sql.js +26 -30
  27. package/lib/connect/index.js +1 -1
  28. package/lib/core/entities.js +0 -3
  29. package/lib/core/infer.js +1 -0
  30. package/lib/core/reflect.js +1 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +13 -4
  34. package/lib/env/presets.js +38 -0
  35. package/lib/env/requires.js +16 -11
  36. package/lib/index.js +13 -11
  37. package/lib/log/format/kibana.js +4 -2
  38. package/lib/log/index.js +2 -2
  39. package/lib/ql/Whereable.js +1 -0
  40. package/lib/req/cds-context.js +79 -0
  41. package/lib/req/context.js +5 -77
  42. package/lib/req/request.js +1 -1
  43. package/lib/serve/Service-api.js +8 -4
  44. package/lib/serve/Service-dispatch.js +0 -7
  45. package/lib/serve/Service-methods.js +6 -8
  46. package/lib/serve/Transaction.js +35 -30
  47. package/lib/serve/adapters.js +1 -4
  48. package/lib/utils/axios.js +1 -1
  49. package/libx/_runtime/audit/Service.js +44 -20
  50. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  51. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  52. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  53. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  54. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  56. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  57. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  59. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  60. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js +1 -0
  78. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  79. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +18 -15
  80. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  81. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  86. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  87. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  88. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  89. package/libx/_runtime/cds-services/services/Service.js +0 -6
  90. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  91. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  92. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  93. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  94. package/libx/_runtime/cds-services/util/assert.js +1 -262
  95. package/libx/_runtime/cds.js +6 -9
  96. package/libx/_runtime/common/aspects/entity.js +1 -1
  97. package/libx/_runtime/common/composition/delete.js +4 -2
  98. package/libx/_runtime/common/composition/update.js +27 -35
  99. package/libx/_runtime/common/composition/utils.js +3 -7
  100. package/libx/_runtime/common/error/standardError.js +11 -0
  101. package/libx/_runtime/common/generic/auth.js +61 -30
  102. package/libx/_runtime/common/generic/crud.js +11 -23
  103. package/libx/_runtime/common/generic/input.js +20 -0
  104. package/libx/_runtime/common/generic/paging.js +2 -2
  105. package/libx/_runtime/common/generic/put.js +4 -10
  106. package/libx/_runtime/common/generic/sorting.js +12 -30
  107. package/libx/_runtime/common/perf/index.js +24 -0
  108. package/libx/_runtime/common/utils/cqn.js +58 -1
  109. package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
  110. package/libx/_runtime/common/utils/csn.js +38 -56
  111. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  112. package/libx/_runtime/common/utils/resolveView.js +4 -5
  113. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  114. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  115. package/libx/_runtime/common/utils/structured.js +35 -25
  116. package/libx/_runtime/db/Service.js +0 -6
  117. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  118. package/libx/_runtime/db/expand/expandCQNToJoin.js +82 -61
  119. package/libx/_runtime/db/expand/index.js +3 -1
  120. package/libx/_runtime/db/generic/arrayed.js +14 -27
  121. package/libx/_runtime/db/generic/input.js +52 -10
  122. package/libx/_runtime/db/generic/integrity.js +367 -26
  123. package/libx/_runtime/db/generic/virtual.js +51 -13
  124. package/libx/_runtime/db/query/update.js +9 -3
  125. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  126. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  127. package/libx/_runtime/fiori/generic/activate.js +1 -0
  128. package/libx/_runtime/fiori/generic/before.js +2 -1
  129. package/libx/_runtime/fiori/generic/edit.js +2 -1
  130. package/libx/_runtime/fiori/generic/patch.js +1 -1
  131. package/libx/_runtime/fiori/generic/read.js +151 -57
  132. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  133. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  134. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  135. package/libx/_runtime/fiori/utils/delete.js +7 -1
  136. package/libx/_runtime/hana/Service.js +1 -8
  137. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  138. package/libx/_runtime/hana/execute.js +10 -4
  139. package/libx/_runtime/hana/pool.js +55 -45
  140. package/libx/_runtime/hana/search.js +7 -6
  141. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  142. package/libx/_runtime/hana/searchToContains.js +3 -1
  143. package/libx/_runtime/index.js +5 -5
  144. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  145. package/libx/_runtime/messaging/Outbox.js +53 -0
  146. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  147. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  148. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  149. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  150. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  151. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  152. package/libx/_runtime/messaging/file-based.js +5 -5
  153. package/libx/_runtime/messaging/message-queuing.js +2 -3
  154. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  155. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  156. package/libx/_runtime/messaging/service.js +16 -30
  157. package/libx/_runtime/remote/Service.js +21 -2
  158. package/libx/_runtime/remote/utils/client.js +15 -3
  159. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  160. package/libx/_runtime/sqlite/Service.js +7 -10
  161. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  162. package/libx/_runtime/sqlite/execute.js +18 -12
  163. package/libx/_runtime/types/api.js +2 -1
  164. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  165. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  166. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
  167. package/libx/odata/index.js +18 -15
  168. package/libx/odata/parser.js +1 -0
  169. package/libx/odata/utils.js +57 -0
  170. package/libx/rest/RestAdapter.js +2 -6
  171. package/libx/rest/utils/data.js +1 -6
  172. package/package.json +4 -3
  173. package/server.js +13 -10
  174. package/srv/audit-log.cds +87 -0
  175. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  176. package/srv/flex.js +1 -0
  177. package/srv/outbox.cds +11 -0
  178. package/srv/outbox.js +0 -0
  179. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  180. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  181. package/libx/odata/odata2cqn/index.js +0 -3
  182. package/libx/odata/odata2cqn/parser.js +0 -1
  183. package/libx/odata/readme.md +0 -1
  184. package/libx/odata/utils/index.js +0 -64
@@ -6,54 +6,13 @@ const getEtagElement = entity => {
6
6
  return Object.values(entity.elements).find(element => element['@odata.etag'])
7
7
  }
8
8
 
9
- const _isDependent = (assoc, parent, target) => {
10
- return (
11
- assoc._isAssociationStrict &&
12
- assoc.is2one &&
13
- !assoc.on &&
14
- !parent['@cds.persistence.skip'] &&
15
- assoc['@assert.integrity'] !== false &&
16
- parent['@assert.integrity'] !== false &&
17
- (!parent._service || parent._service['@assert.integrity'] !== false) &&
18
- !assoc._isCompositionBacklink
19
- )
20
- }
21
-
22
- /*
23
- * this modifies the csn on purpose for caching effect!
24
- * doing as aspect is difficult due to no global definitons per tenant
25
- */
26
- const getDependents = (entity, model) => {
27
- if (entity.own('__dependents')) return entity.__dependents
28
-
29
- /** @type {Array|boolean} */
30
- let dependents = []
31
- for (const def of Object.values(model.definitions)) {
32
- if (def.kind !== 'entity') continue
33
- if (!def.associations) continue
34
-
35
- for (const assoc of Object.values(def.associations)) {
36
- if (assoc.target !== entity.name) continue
37
-
38
- const parent = assoc.parent
39
- const target = model.definitions[assoc.target]
40
- if (_isDependent(assoc, parent, target)) {
41
- dependents.push({ element: assoc, parent, target })
42
- }
43
- }
44
- }
45
-
46
- if (dependents.length === 0) dependents = false
47
- return entity.set('__dependents', dependents)
48
- }
49
-
50
9
  const _getUps = (entity, model) => {
51
10
  const ups = []
52
11
  for (const def of Object.values(model.definitions)) {
53
- if (def.kind !== 'entity') continue
54
- if (!def.associations) continue
12
+ if (def.kind !== 'entity' || !def.associations) continue
55
13
  for (const element of Object.values(def.associations)) {
56
14
  if (element.target !== entity.name || element._isBacklink) continue
15
+ if (element.name === 'SiblingEntity') continue
57
16
  ups.push(element)
58
17
  }
59
18
  }
@@ -64,26 +23,50 @@ const _ifDataSubject = (entity, role) => {
64
23
  return entity['@PersonalData.EntitySemantics'] === 'DataSubject' && entity['@PersonalData.DataSubjectRole'] === role
65
24
  }
66
25
 
67
- const _getDataSubjectUp = (role, model, element, first = element) => {
68
- const upElements = _getUps(element.parent, model)
69
- for (const element of upElements) {
26
+ const _getDataSubjectUp = (role, model, entity, prev, next, result) => {
27
+ for (const element of _getUps(entity, model)) {
28
+ const me = { entity, relative: element.parent, element }
29
+ if (prev) prev.next = me
70
30
  if (_ifDataSubject(element.parent, role)) {
71
- return { element: first, up: { element }, entity: element.parent }
31
+ if (!result) result = { dataSubjectEntity: element.parent, subs: [] }
32
+ result.subs.push(next || me)
33
+ return result
34
+ } else {
35
+ // dfs is a must here
36
+ result = _getDataSubjectUp(role, model, element.parent, me, next || me, result)
72
37
  }
73
- // dfs is a must here
74
- const dataSubject = _getDataSubjectUp(role, model, element, first)
75
- if (dataSubject) {
76
- dataSubject.up = { element, up: dataSubject.up }
77
- return dataSubject
38
+ }
39
+ return result
40
+ }
41
+
42
+ const _getDataSubjectDown = (role, entity, prev, next) => {
43
+ const associations = Object.values(entity.associations).filter(e => !e._isBacklink)
44
+ for (const element of associations) {
45
+ const me = { entity, relative: entity, element }
46
+ if (_ifDataSubject(element._target, role)) {
47
+ if (prev) prev.next = me
48
+ return { dataSubjectEntity: element._target, subs: [next || me] }
78
49
  }
79
50
  }
51
+ // bfs makes more sense here
52
+ for (const element of associations) {
53
+ const me = { entity, relative: entity, element }
54
+ if (prev) prev.next = me
55
+ const dataSubject = _getDataSubjectDown(role, element._target, me, next || me)
56
+ if (dataSubject) return dataSubject
57
+ }
80
58
  }
81
59
 
82
- const getDataSubject = (entity, model, role, element) => {
60
+ const getDataSubject = (entity, model, role) => {
83
61
  const hash = '__dataSubject4' + role
84
62
  if (entity.own(hash)) return entity[hash]
85
- if (_ifDataSubject(element.parent, role)) return entity.set(hash, { element, entity: element.parent })
86
- return entity.set(hash, _getDataSubjectUp(role, model, element))
63
+ // entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
64
+ // be always below or always above 'DataSubject' entity in CSN tree
65
+ let dataSubject = _getDataSubjectUp(role, model, entity)
66
+ if (!dataSubject) {
67
+ dataSubject = _getDataSubjectDown(role, entity)
68
+ }
69
+ return entity.set(hash, dataSubject)
87
70
  }
88
71
 
89
72
  const _resolve = (name, model, namespace) =>
@@ -204,7 +187,6 @@ module.exports = {
204
187
  getEtagElement,
205
188
  findCsnTargetFor,
206
189
  getElementDeep,
207
- getDependents,
208
190
  isRootEntity,
209
191
  getDataSubject,
210
192
  alias2ref
@@ -5,26 +5,26 @@ const getEntityNameFromCQN = cqn => {
5
5
 
6
6
  // Do the most likely first -> {ref}
7
7
  if (cqn.ref) {
8
- return cqn.ref[0].id || cqn.ref[0]
8
+ return { entityName: cqn.ref[0].id || cqn.ref[0], alias: cqn.as }
9
9
  }
10
10
 
11
11
  // TODO cleanup
12
12
  // REVISIT infer should do this for req.target
13
13
  // REVISIT2 No, req.target doesn't make sense for joins
14
14
  if (cqn.SET) {
15
- return cqn.SET.args.map(getEntityNameFromCQN).find(n => n !== 'DRAFT.DraftAdministrativeData')
15
+ return cqn.SET.args.map(getEntityNameFromCQN).find(n => n.entityName !== 'DRAFT.DraftAdministrativeData')
16
16
  }
17
17
  if (cqn.join) {
18
- return cqn.args.map(getEntityNameFromCQN).find(n => n !== 'DRAFT.DraftAdministrativeData')
18
+ return cqn.args.map(getEntityNameFromCQN).find(n => n.entityName !== 'DRAFT.DraftAdministrativeData')
19
19
  }
20
+ return {}
20
21
  }
21
22
 
22
23
  // Note: This also works for the common draft scenarios
23
24
  const getEntityFromCQN = (req, service) => {
24
25
  if (!req.target || req.target._unresolved) {
25
- const entity = getEntityNameFromCQN(req.query)
26
- if (!entity) return
27
- return service.model.definitions[ensureNoDraftsSuffix(entity)]
26
+ const { entityName } = getEntityNameFromCQN(req.query)
27
+ return entityName && service.model.definitions[ensureNoDraftsSuffix(entityName)]
28
28
  }
29
29
  return req.target
30
30
  }
@@ -98,17 +98,16 @@ const _newNestedData = (queryTarget, newData, ref, value) => {
98
98
  }
99
99
  }
100
100
 
101
+ // eslint-disable-next-line complexity
101
102
  const _newData = (data, transition, inverse, service) => {
103
+ if (data === null) return null
102
104
  // no transition -> nothing to do
103
105
  if (transition.target && transition.target.name === transition.queryTarget.name) return data
104
106
 
107
+ // REVISIT this does not copy deep
105
108
  const newData = { ...data }
106
109
  const queryTarget = transition.queryTarget
107
110
 
108
- /*
109
- * REVISIT: the current impl results in {} instead of keeping null for compo to one.
110
- * unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
111
- */
112
111
  for (const key in newData) {
113
112
  const el = queryTarget && queryTarget.elements && queryTarget.elements[key]
114
113
  const isAssoc = el && el.isAssociation
@@ -339,7 +338,7 @@ const _newSelect = (query, transitions, service) => {
339
338
  if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
340
339
  if (newSelect.columns) {
341
340
  const isDB = service instanceof cds.DatabaseService
342
- rewriteAsterisks({ SELECT: newSelect }, targetTransition.queryTarget, isDB)
341
+ rewriteAsterisks({ SELECT: newSelect }, service.model, isDB)
343
342
  newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
344
343
  }
345
344
  if (newSelect.having) newSelect.having = _newColumns(newSelect.having, targetTransition)
@@ -1,6 +1,7 @@
1
1
  const { getNavigationIfStruct } = require('./structured')
2
2
  const getColumns = require('../../db/utils/columns')
3
- const { ensureDraftsSuffix } = require('./draft')
3
+ const { ensureDraftsSuffix, ensureNoDraftsSuffix } = require('./draft')
4
+ const { getEntityNameFromCQN } = require('./entityFromCqn')
4
5
 
5
6
  const isAsteriskColumn = col => col === '*' || (col.ref && col.ref[0] === '*' && !col.expand)
6
7
 
@@ -75,15 +76,55 @@ const _rewriteAsterisks = (cqn, target, db, isRoot) => {
75
76
  return columns
76
77
  }
77
78
 
78
- const rewriteAsterisks = (query, target, db = false, isDraft = false, onlyKeys = false) => {
79
+ const _targetOfQueryIfNotDraft = (query, model) => {
80
+ const { entityName } = getEntityNameFromCQN(query)
81
+ const target = model.definitions[entityName]
79
82
  if (!target || target.name.endsWith('_drafts')) return
80
- if (!query.SELECT.columns || (query.SELECT.columns && !query.SELECT.columns.length)) {
83
+ return target
84
+ }
85
+
86
+ const rewriteAsterisks = (query, model, db = false, isDraft = false, onlyKeys = false) => {
87
+ if (!query.SELECT.columns || !query.SELECT.columns.length) {
81
88
  if (isDraft || db) {
82
- query.SELECT.columns = getColumns(target, { db, onlyKeys }).map(col => ({ ref: [col.name] }))
83
- if (db && target._isDraftEnabled) query.SELECT.columns.push(..._cqlDraftColumns(target))
89
+ if (
90
+ query.SELECT.from.SET &&
91
+ query.SELECT.from.SET.args[0] &&
92
+ query.SELECT.from.SET.args[0].SELECT &&
93
+ query.SELECT.from.SET.args[0].SELECT.columns
94
+ ) {
95
+ // > best-effort derive column list from first join element if given
96
+ query.SELECT.columns = query.SELECT.from.SET.args[0].SELECT.columns.map(c => ({
97
+ ref: [c.as || c.ref[c.ref.length - 1]]
98
+ }))
99
+ } else if (query.SELECT.from.join && query.SELECT.from.args) {
100
+ if (!query.SELECT.columns) query.SELECT.columns = []
101
+ for (const arg of query.SELECT.from.args) {
102
+ const _targetName = arg.ref[0].id || arg.ref[0]
103
+ const _target = model.definitions[ensureNoDraftsSuffix(_targetName)]
104
+ const columns = getColumns(_target, { db, onlyKeys })
105
+ .filter(
106
+ c =>
107
+ !query.SELECT.columns.some(
108
+ existing => (existing.as || existing.ref[existing.ref.length - 1]) === c.name
109
+ )
110
+ )
111
+ .map(col => ({
112
+ ref: [arg.as || _targetName, col.name]
113
+ }))
114
+ columns.forEach(c => query.SELECT.columns.push(c))
115
+ }
116
+ } else {
117
+ const target = _targetOfQueryIfNotDraft(query, model)
118
+ if (!target) return
119
+ query.SELECT.columns = getColumns(target, { db, onlyKeys }).map(col => ({ ref: [col.name] }))
120
+ if (db && target._isDraftEnabled) query.SELECT.columns.push(..._cqlDraftColumns(target))
121
+ }
84
122
  }
85
123
  return
86
124
  }
125
+ const target = _targetOfQueryIfNotDraft(query, model)
126
+ if (!target) return
127
+ // REVISIT: Also support JOINs/SETs here
87
128
  query.SELECT.columns = _rewriteAsterisks(query.SELECT, target, db, true)
88
129
  }
89
130
 
@@ -1,12 +1,20 @@
1
1
  const { computeColumnsToBeSearched } = require('../../../../libx/_runtime/cds-services/services/utils/columns')
2
2
  const searchToLike = require('./searchToLike')
3
+ const { ensureNoDraftsSuffix } = require('./draft')
4
+ const { getEntityNameFromCQN } = require('./entityFromCqn')
3
5
 
4
- // convert $search system query option to WHERE/HAVING clause using
5
- // the operator LIKE or CONTAINS
6
- const search2cqn4sql = (query, model, options) => {
7
- const { search2cqn4sql, targetName = query.SELECT.from.ref[0] } = options
8
- const entity = model.definitions[targetName]
9
- const columns = computeColumnsToBeSearched(query, entity)
6
+ const _targetFrom = (cqn, options) => {
7
+ if (options && options.entityName) return options
8
+ return getEntityNameFromCQN(cqn)
9
+ }
10
+
11
+ const _search2cqn4sql = (query, model, options = {}) => {
12
+ const cqnSearchPhrase = query.SELECT.search
13
+ if (!cqnSearchPhrase) return
14
+ const { search2cqn4sql } = options
15
+ const { entityName, alias } = _targetFrom(query.SELECT.from, options)
16
+ const entity = model.definitions[ensureNoDraftsSuffix(entityName)]
17
+ const columns = computeColumnsToBeSearched(query, entity, alias)
10
18
 
11
19
  // Call custom (optimized search to cqn for sql implementation) that tries
12
20
  // to optimize the search behavior for a specific database service.
@@ -16,11 +24,15 @@ const search2cqn4sql = (query, model, options) => {
16
24
  return search2cqn4sql(query, entity, search2cqnOptions)
17
25
  }
18
26
 
19
- const cqnSearchPhrase = query.SELECT.search
20
27
  const expression = searchToLike(cqnSearchPhrase, columns)
21
28
 
22
29
  // REVISIT: find out here if where or having must be used
23
- query._aggregated ? query.having(expression) : query.where(expression)
30
+ query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
24
31
  }
25
32
 
26
- module.exports = search2cqn4sql
33
+ // convert $search system query option to WHERE/HAVING clause using
34
+ // the operator LIKE or CONTAINS
35
+ module.exports = (query, model, options) => {
36
+ if (query.SELECT.from.SET) return query.SELECT.from.SET.args.forEach(arg => _search2cqn4sql(arg, model, options))
37
+ return _search2cqn4sql(query, model, options)
38
+ }
@@ -204,36 +204,43 @@ const _structFromRef = (ref, csnEntity, model) => {
204
204
  }
205
205
 
206
206
  const flattenStructuredWhereHaving = (filterArray, csnEntity, model) => {
207
- if (filterArray) {
208
- const newFilterArray = []
209
- for (let i = 0; i < filterArray.length; i++) {
210
- if (OPERATIONS.includes(filterArray[i + 1])) {
211
- const refElement = filterArray[i].ref ? filterArray[i] : filterArray[i + 2]
212
- // copy for processing
213
- const ref = refElement.ref && refElement.ref.map(ele => ele)
214
- // is ref[0] an alias? -> remove
215
- const isAliased = ref && ref.length > 1 && !csnEntity.elements[ref[0]]
216
- if (isAliased) ref.shift()
217
- const { element, idx } = _structFromRef(ref, csnEntity, model)
218
- // REVISIT: We cannot make the simple distinction between ref and others
219
- // for xpr, subselect, we need to call this method recursively
220
- if (element) {
221
- if (isAliased) refElement.ref.shift()
222
- // REVISIT: This does not support operator like "between", "in" or a different order of elements like val,op,ref or expressions like ref,op,val+val
223
- _transformStructToFlatWhereHaving(filterArray.slice(i, i + 3), newFilterArray, element, idx)
224
- i += 2 // skip next two entries e.g. ('=', '{struct:{int:1}}')
225
- continue
226
- }
227
- }
207
+ if (!filterArray) return
208
+
209
+ const newFilterArray = []
210
+ for (let i = 0; i < filterArray.length; i++) {
211
+ if (OPERATIONS.includes(filterArray[i + 1])) {
212
+ const refElement = filterArray[i].ref ? filterArray[i] : filterArray[i + 2]
213
+
214
+ // copy for processing
215
+ const ref = refElement.ref && refElement.ref.map(ele => ele)
216
+
217
+ // is ref[0] an alias? -> remove
218
+ const isAliased = ref && ref.length > 1 && !csnEntity.elements[ref[0]]
219
+ if (isAliased) ref.shift()
220
+ const { element, idx } = _structFromRef(ref, csnEntity, model)
221
+
222
+ // REVISIT: We cannot make the simple distinction between ref and others
223
+ // for xpr, subselect, we need to call this method recursively
224
+ if (element) {
225
+ if (isAliased) refElement.ref.shift()
228
226
 
229
- newFilterArray.push(filterArray[i])
227
+ // REVISIT: This does not support operator like "between", "in" or a different order of elements like val,op,ref or expressions like ref,op,val+val
228
+ _transformStructToFlatWhereHaving(filterArray.slice(i, i + 3), newFilterArray, element, idx)
229
+ i += 2 // skip next two entries e.g. ('=', '{struct:{int:1}}')
230
+ continue
231
+ }
230
232
  }
231
- return newFilterArray
233
+
234
+ newFilterArray.push(filterArray[i])
232
235
  }
236
+
237
+ return newFilterArray
233
238
  }
239
+
234
240
  const _entityFromRef = ref => {
235
241
  if (ref) return ref[0].id || ref[0]
236
242
  }
243
+
237
244
  const getNavigationIfStruct = (entity, ref) => {
238
245
  const element = entity && entity.elements && entity.elements[_entityFromRef(ref)]
239
246
  if (!element) return
@@ -275,8 +282,11 @@ const flattenStructuredSelect = ({ SELECT }, model) => {
275
282
  const flattenedElements = []
276
283
  const toBeDeleted = []
277
284
  _flattenColumns(SELECT, flattenedElements, toBeDeleted, entity)
278
- SELECT.columns = SELECT.columns.filter(e => (e.ref && !toBeDeleted.includes(e.ref[0])) || e.func || e.expand) // TODO aliases?
279
- SELECT.columns.push(...flattenedElements)
285
+ SELECT.columns = SELECT.columns.filter(column => {
286
+ const columnName = column.ref ? column.ref[0] : column.as
287
+ return (columnName && !toBeDeleted.includes(columnName)) || column.func || column.expand
288
+ })
289
+ if (flattenedElements.length) SELECT.columns.push(...flattenedElements)
280
290
  }
281
291
  if (SELECT.from.args) {
282
292
  for (const arg of SELECT.from.args) {
@@ -38,12 +38,6 @@ class DatabaseService extends cds.Service {
38
38
  // REVISIT: how to generic handler registration?
39
39
  }
40
40
 
41
- set model(m) {
42
- // Ensure the model we get has unfolded entities for localized data, drafts, etc.
43
- // Note: cds.deploy and some tests set the model of cds.db outside the constructor
44
- super.model = m && 'definitions' in m ? cds.compile.for.odata(m) : m
45
- }
46
-
47
41
  /*
48
42
  * tx
49
43
  */
@@ -0,0 +1,130 @@
1
+ const cds = require('../../cds')
2
+ const getColumns = require('../utils/columns')
3
+
4
+ const _identifierForRow = (row, prefix, keys) => {
5
+ return keys.map(k => row[`${prefix}${k}`]).join(',')
6
+ }
7
+
8
+ const _removeParentKeysFromRow = (row, prefix, keys) => {
9
+ for (const k of keys) {
10
+ delete row[`${prefix}${k}`]
11
+ }
12
+ }
13
+
14
+ const _autoExpandNavsAndAttachToResult = async (entity, previousResult, depth) => {
15
+ for (const nav in entity._associations) {
16
+ const navigation = entity._associations[nav]
17
+
18
+ // do not expand backlinks
19
+ if (navigation._isBacklink) continue
20
+
21
+ const childAlias = 'child'
22
+ const parentAlias = 'parent'
23
+
24
+ const cqnQuery = SELECT.from(`${navigation.target} as ${childAlias}`)
25
+ .join(`${entity.name} as ${parentAlias}`)
26
+ .on(entity._relations[navigation.name].join(childAlias, parentAlias))
27
+
28
+ // set alias for expanded columns already
29
+ const childColumns = getColumns(navigation._target).map(c => ({ ref: [childAlias, c.name] }))
30
+ const parentKeys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation)
31
+ // mark parent key with prefix in alias
32
+ const parentKeysWithAlias = parentKeys.map(pk => ({ ref: [parentAlias, pk], as: `$$pk_${pk}` }))
33
+ cqnQuery.columns(...childColumns, ...parentKeysWithAlias)
34
+
35
+ // add tuple comparison for where clause
36
+ cqnQuery.where([
37
+ { list: parentKeys.map(pk => ({ ref: [parentAlias, pk] })) },
38
+ 'in',
39
+ { list: previousResult.map(row => ({ list: parentKeys.map(pk => ({ val: row[pk] })) })) }
40
+ ])
41
+
42
+ // sort by primary keys of parent, required in future
43
+ cqnQuery.orderBy(parentKeys.map(pk => ({ ref: [parentAlias, pk] })))
44
+
45
+ const result = await cds.db.run(cqnQuery)
46
+
47
+ // TODO: Is there a more efficient/stable way to handle compound keys?
48
+ const map = new Map()
49
+ for (const row of result) {
50
+ const identifier = _identifierForRow(row, '$$pk_', parentKeys)
51
+ _removeParentKeysFromRow(row, '$$pk_', parentKeys)
52
+ if (map.has(identifier)) {
53
+ map.get(identifier).push(row)
54
+ } else {
55
+ map.set(identifier, [row])
56
+ }
57
+ }
58
+
59
+ // link previous result with current result
60
+ previousResult.forEach(row => {
61
+ const identifier = _identifierForRow(row, '', parentKeys)
62
+ if (map.has(identifier)) {
63
+ const entry = map.get(identifier)
64
+ row[nav] = navigation.is2one ? entry[0] : entry
65
+ } else {
66
+ row[nav] = navigation.is2one ? null : []
67
+ }
68
+ })
69
+
70
+ // expand next level if needed
71
+ if (depth - 1 !== 0 && result.length && navigation._target._associations) {
72
+ await _autoExpandNavsAndAttachToResult(navigation._target, result, depth - 1)
73
+ }
74
+ }
75
+
76
+ return previousResult
77
+ }
78
+
79
+ const _foreignKeysOfTopLevelNavs = entity => {
80
+ const requiredFks = new Set()
81
+ for (const nav in entity._associations) {
82
+ const onCond = entity._relations[nav].join('child', 'parent')
83
+ for (const ele of onCond) {
84
+ if (ele.ref && ele.ref[0] === 'parent') {
85
+ requiredFks.add(ele.ref.slice(1).join('_'))
86
+ }
87
+ }
88
+ }
89
+ return [...requiredFks]
90
+ }
91
+
92
+ /**
93
+ * 1. Creates flattened SQL statements for each expand layer
94
+ * 2. Mixes in foreign keys if needed
95
+ * 3. Merges results
96
+ * 4. Cleans result if needed
97
+ *
98
+ * @returns object
99
+ */
100
+ const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSelectCQN) => {
101
+ // remove expand columns from query without modifying
102
+ const topLevelSelect = query.clone().columns(query.SELECT.columns.filter(c => !c.expand))
103
+
104
+ const entity = model.definitions[topLevelSelect.SELECT.from.ref[0]]
105
+
106
+ // ensure foreign keys are selected if needed
107
+ const fks = _foreignKeysOfTopLevelNavs(entity)
108
+ fks.forEach(fk => {
109
+ if (!topLevelSelect.SELECT.columns.some(c => c.ref[0] === fk)) {
110
+ topLevelSelect.SELECT.columns.push({ ref: [fk] })
111
+ }
112
+ })
113
+
114
+ const result = await executeSelectCQN(model, dbc, topLevelSelect, user, locale, txTimestamp)
115
+
116
+ if (!result || (Array.isArray(result) && !result.length)) {
117
+ return result
118
+ }
119
+
120
+ // _associations contains compositions and associations
121
+ if (entity._associations) {
122
+ const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand === 'string')
123
+ const depth = expandColumn.expand === '**' ? -1 : Number(expandColumn.expand.replace('*', ''))
124
+ await _autoExpandNavsAndAttachToResult(entity, Array.isArray(result) ? result : [result], depth)
125
+ }
126
+
127
+ return result
128
+ }
129
+
130
+ module.exports = expandV2