@sap/cds 5.4.3 → 5.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 (228) hide show
  1. package/CHANGELOG.md +239 -2
  2. package/apis/ql.d.ts +17 -15
  3. package/app/index.js +1 -1
  4. package/bin/build/buildTaskEngine.js +26 -42
  5. package/bin/build/buildTaskFactory.js +6 -10
  6. package/bin/build/buildTaskHandler.js +2 -4
  7. package/bin/build/buildTaskProvider.js +3 -1
  8. package/bin/build/buildTaskProviderFactory.js +9 -15
  9. package/bin/build/constants.js +15 -3
  10. package/bin/build/index.js +5 -4
  11. package/bin/build/mtaUtil.js +8 -11
  12. package/bin/build/provider/buildTaskHandlerEdmx.js +63 -6
  13. package/bin/build/provider/buildTaskHandlerInternal.js +2 -34
  14. package/bin/build/provider/buildTaskProviderInternal.js +16 -42
  15. package/bin/build/provider/fiori/index.js +13 -24
  16. package/bin/build/provider/hana/2migration.js +17 -15
  17. package/bin/build/provider/hana/2tabledata.js +52 -48
  18. package/bin/build/provider/hana/index.js +27 -25
  19. package/bin/build/provider/hana/migrationtable.js +91 -67
  20. package/bin/build/provider/java-cf/index.js +14 -24
  21. package/bin/build/provider/mtx/index.js +12 -14
  22. package/bin/build/provider/node-cf/index.js +18 -32
  23. package/bin/cds.js +5 -5
  24. package/bin/serve.js +29 -23
  25. package/bin/version.js +0 -1
  26. package/lib/compile/etc/_localized.js +4 -9
  27. package/lib/compile/for/sql.js +5 -2
  28. package/lib/compile/parse.js +25 -17
  29. package/lib/compile/to/srvinfo.js +2 -1
  30. package/lib/connect/bindings.js +2 -1
  31. package/lib/connect/index.js +48 -49
  32. package/lib/core/classes.js +1 -1
  33. package/lib/core/reflect.js +10 -2
  34. package/lib/deploy.js +26 -23
  35. package/lib/env/defaults.js +13 -6
  36. package/lib/env/index.js +73 -78
  37. package/lib/env/requires.js +38 -19
  38. package/lib/index.js +9 -10
  39. package/lib/lazy.js +2 -2
  40. package/lib/log/index.js +33 -45
  41. package/lib/log/service/index.js +2 -2
  42. package/lib/ql/CREATE.js +14 -9
  43. package/lib/ql/DELETE.js +6 -5
  44. package/lib/ql/DROP.js +12 -9
  45. package/lib/ql/INSERT.js +40 -16
  46. package/lib/ql/Query.js +67 -40
  47. package/lib/ql/SELECT.js +162 -127
  48. package/lib/ql/UPDATE.js +74 -42
  49. package/lib/ql/Whereable.js +77 -87
  50. package/lib/ql/index.js +36 -24
  51. package/lib/ql/parse.js +35 -0
  52. package/lib/req/context.js +44 -8
  53. package/lib/req/locale.js +7 -7
  54. package/lib/serve/Service-api.js +21 -14
  55. package/lib/serve/Service-dispatch.js +28 -12
  56. package/lib/serve/Transaction.js +22 -10
  57. package/lib/serve/index.js +16 -11
  58. package/lib/utils/axios.js +23 -16
  59. package/lib/utils/data.js +35 -0
  60. package/lib/utils/tests.js +27 -18
  61. package/libx/_runtime/audit/generic/personal/access.js +81 -0
  62. package/libx/_runtime/audit/generic/personal/constants.js +4 -0
  63. package/libx/_runtime/audit/generic/personal/index.js +50 -0
  64. package/libx/_runtime/audit/generic/personal/modification.js +138 -0
  65. package/libx/_runtime/audit/generic/personal/utils.js +186 -0
  66. package/libx/_runtime/audit/utils/v2.js +10 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +5 -5
  68. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -7
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +5 -7
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +5 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +2 -3
  72. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +4 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +7 -4
  74. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +59 -8
  75. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +11 -1
  76. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +6 -10
  77. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +3 -46
  78. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +2 -5
  79. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/createToCQN.js +2 -2
  80. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +4 -3
  81. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  82. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
  83. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +1 -1
  84. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +2 -2
  85. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +16 -18
  86. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/EdmEntityType.js +6 -3
  87. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/format/RepresentationKind.js +4 -1
  88. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  89. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/SerializerFactory.js +15 -2
  90. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/OperationValidator.js +1 -0
  91. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  92. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +8 -1
  93. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +6 -1
  94. package/libx/_runtime/cds-services/adapter/odata-v4/utils/omitValues.js +12 -5
  95. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -7
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +7 -7
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +14 -18
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +13 -13
  99. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +0 -1
  100. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +2 -1
  101. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +2 -2
  102. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -4
  103. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +4 -2
  104. package/libx/_runtime/cds-services/services/Service.js +40 -5
  105. package/libx/_runtime/cds-services/services/utils/columns.js +13 -7
  106. package/libx/_runtime/cds-services/services/utils/compareJson.js +88 -4
  107. package/libx/_runtime/cds-services/services/utils/differ.js +24 -6
  108. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +2 -2
  109. package/libx/_runtime/common/composition/data.js +66 -63
  110. package/libx/_runtime/common/composition/delete.js +97 -71
  111. package/libx/_runtime/common/composition/index.js +2 -1
  112. package/libx/_runtime/common/composition/insert.js +34 -11
  113. package/libx/_runtime/common/composition/tree.js +119 -92
  114. package/libx/_runtime/common/composition/update.js +12 -1
  115. package/libx/_runtime/common/composition/utils.js +1 -3
  116. package/libx/_runtime/common/constants/draft.js +12 -1
  117. package/libx/_runtime/common/generic/auth.js +53 -31
  118. package/libx/_runtime/common/generic/crud.js +14 -13
  119. package/libx/_runtime/common/generic/input.js +23 -26
  120. package/libx/_runtime/common/generic/put.js +1 -1
  121. package/libx/_runtime/common/generic/sorting.js +16 -16
  122. package/libx/_runtime/common/i18n/index.js +1 -1
  123. package/libx/_runtime/common/i18n/messages.properties +4 -0
  124. package/libx/_runtime/common/utils/backlinks.js +12 -5
  125. package/libx/_runtime/common/utils/cqn.js +6 -1
  126. package/libx/_runtime/common/utils/cqn2cqn4sql.js +123 -108
  127. package/libx/_runtime/common/utils/csn.js +56 -4
  128. package/libx/_runtime/common/utils/data.js +0 -37
  129. package/libx/_runtime/common/utils/enrichWithKeysFromWhere.js +1 -1
  130. package/libx/_runtime/common/utils/entityFromCqn.js +7 -24
  131. package/libx/_runtime/common/utils/foreignKeyPropagations.js +39 -7
  132. package/libx/_runtime/common/utils/generateOnCond.js +11 -12
  133. package/libx/_runtime/common/utils/onlyKeysRemain.js +10 -0
  134. package/libx/_runtime/common/utils/path.js +35 -0
  135. package/libx/_runtime/common/utils/postProcessing.js +86 -0
  136. package/libx/_runtime/common/utils/quotingStyles.js +37 -26
  137. package/libx/_runtime/common/utils/resolveView.js +227 -173
  138. package/libx/_runtime/common/utils/rewriteAsterisk.js +46 -26
  139. package/libx/_runtime/common/utils/structured.js +13 -13
  140. package/libx/_runtime/common/utils/template.js +10 -5
  141. package/libx/_runtime/common/utils/templateDelimiter.js +1 -0
  142. package/libx/_runtime/common/utils/templateProcessor.js +28 -72
  143. package/libx/_runtime/common/utils/union.js +31 -0
  144. package/libx/_runtime/common/utils/unionCqnTemplate.js +184 -0
  145. package/libx/_runtime/db/Service.js +1 -1
  146. package/libx/_runtime/db/data-conversion/timestamp.js +2 -9
  147. package/libx/_runtime/db/expand/expandCQNToJoin.js +204 -297
  148. package/libx/_runtime/db/expand/index.js +3 -3
  149. package/libx/_runtime/db/expand/rawToExpanded.js +36 -7
  150. package/libx/_runtime/db/generic/index.js +1 -1
  151. package/libx/_runtime/db/generic/input.js +5 -7
  152. package/libx/_runtime/db/generic/integrity.js +1 -1
  153. package/libx/_runtime/db/generic/rewrite.js +2 -10
  154. package/libx/_runtime/db/generic/update.js +13 -5
  155. package/libx/_runtime/db/generic/virtual.js +22 -58
  156. package/libx/_runtime/db/query/delete.js +7 -4
  157. package/libx/_runtime/db/query/insert.js +6 -4
  158. package/libx/_runtime/db/query/read.js +21 -8
  159. package/libx/_runtime/db/query/run.js +4 -1
  160. package/libx/_runtime/db/query/update.js +5 -4
  161. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +35 -2
  162. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +17 -2
  163. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -5
  164. package/libx/_runtime/db/sql-builder/ReferenceBuilder.js +10 -0
  165. package/libx/_runtime/db/sql-builder/SelectBuilder.js +35 -24
  166. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +14 -4
  167. package/libx/_runtime/db/sql-builder/arrayed.js +4 -0
  168. package/libx/_runtime/db/utils/deep.js +8 -0
  169. package/libx/_runtime/db/utils/generateAliases.js +2 -1
  170. package/libx/_runtime/fiori/generic/activate.js +19 -15
  171. package/libx/_runtime/fiori/generic/before.js +3 -11
  172. package/libx/_runtime/fiori/generic/cancel.js +1 -1
  173. package/libx/_runtime/fiori/generic/delete.js +3 -1
  174. package/libx/_runtime/fiori/generic/edit.js +12 -2
  175. package/libx/_runtime/fiori/generic/new.js +5 -5
  176. package/libx/_runtime/fiori/generic/patch.js +0 -18
  177. package/libx/_runtime/fiori/generic/read.js +261 -205
  178. package/libx/_runtime/fiori/utils/delete.js +36 -7
  179. package/libx/_runtime/fiori/utils/handler.js +43 -44
  180. package/libx/_runtime/fiori/utils/where.js +30 -15
  181. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +4 -6
  182. package/libx/_runtime/hana/execute.js +3 -3
  183. package/libx/_runtime/hana/localized.js +4 -4
  184. package/libx/_runtime/hana/pool.js +29 -14
  185. package/libx/_runtime/hana/search2cqn4sql.js +2 -1
  186. package/libx/_runtime/hana/searchToContains.js +18 -14
  187. package/libx/_runtime/index.js +0 -5
  188. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +13 -5
  189. package/libx/_runtime/messaging/common-utils/naming-conventions.js +4 -1
  190. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +31 -19
  191. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  192. package/libx/_runtime/messaging/enterprise-messaging.js +6 -4
  193. package/libx/_runtime/messaging/service.js +7 -6
  194. package/libx/_runtime/odata/cqn2odata.js +110 -43
  195. package/libx/_runtime/odata/index.js +26 -48
  196. package/libx/_runtime/odata/odata2cqn.js +1 -6154
  197. package/libx/_runtime/odata/odata2cqn.pegjs +559 -0
  198. package/libx/_runtime/odata/readToCqn.js +94 -64
  199. package/libx/_runtime/remote/Service.js +74 -21
  200. package/libx/_runtime/remote/cqn2odata/index.js +1 -5
  201. package/libx/_runtime/remote/utils/client.js +24 -101
  202. package/libx/_runtime/remote/utils/dataConversion.js +27 -12
  203. package/libx/_runtime/sqlite/Service.js +3 -5
  204. package/libx/_runtime/sqlite/execute.js +33 -27
  205. package/libx/_runtime/sqlite/localized.js +12 -7
  206. package/libx/_runtime/types/api.js +10 -0
  207. package/package.json +2 -2
  208. package/server.js +16 -2
  209. package/lib/ql/grammar.pegjs +0 -208
  210. package/lib/ql/parser.js +0 -1
  211. package/lib/ql/rt/DELETE.js +0 -29
  212. package/lib/ql/rt/INSERT.js +0 -23
  213. package/lib/ql/rt/Query.js +0 -84
  214. package/lib/ql/rt/SELECT.js +0 -174
  215. package/lib/ql/rt/UPDATE.js +0 -119
  216. package/lib/ql/rt/_helpers.js +0 -91
  217. package/lib/ql/rt/index.js +0 -32
  218. package/libx/_runtime/audit/generic/personal.js +0 -260
  219. package/libx/_runtime/cds-services/statements/BaseStatement.js +0 -72
  220. package/libx/_runtime/cds-services/statements/Create.js +0 -57
  221. package/libx/_runtime/cds-services/statements/Delete.js +0 -33
  222. package/libx/_runtime/cds-services/statements/Drop.js +0 -42
  223. package/libx/_runtime/cds-services/statements/Insert.js +0 -201
  224. package/libx/_runtime/cds-services/statements/Select.js +0 -826
  225. package/libx/_runtime/cds-services/statements/Update.js +0 -181
  226. package/libx/_runtime/cds-services/statements/Where.js +0 -726
  227. package/libx/_runtime/cds-services/statements/index.js +0 -25
  228. package/libx/_runtime/common/generic/resolve-mock.js +0 -9
@@ -18,11 +18,11 @@ const BASE_PATH = '/messaging/enterprise-messaging'
18
18
  const _checkAppURL = appURL => {
19
19
  if (!appURL)
20
20
  throw new Error(
21
- 'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-shared`.'
21
+ 'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
22
22
  )
23
23
  if (appURL.startsWith('https://localhost'))
24
24
  throw new Error(
25
- 'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-shared`.'
25
+ 'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
26
26
  )
27
27
  }
28
28
 
@@ -78,8 +78,10 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
78
78
  }
79
79
 
80
80
  startListening() {
81
- super.startListening()
82
- if (this.subscribedTopics.size) {
81
+ const doNotDeploy = cds._mtxEnabled && !this.options.deployForProvider
82
+ if (doNotDeploy) LOG._info && LOG.info('Skipping deployment of messaging artifacts for provider account')
83
+ super.startListening({ doNotDeploy })
84
+ if (!doNotDeploy && this.subscribedTopics.size) {
83
85
  const management = this.getManagement()
84
86
  // Webhooks will perform an OPTIONS call on creation to check the availability of the app.
85
87
  // On systems like Cloud Foundry the app URL will only be advertised once
@@ -62,7 +62,7 @@ class MessagingService extends cds.Service {
62
62
  const { emit } = this
63
63
  this.emit = function (...args) {
64
64
  const context = this.context || cds.context
65
- if (context)
65
+ if (context && typeof context.on === 'function')
66
66
  return context.on('succeeded', () =>
67
67
  emit.call(this, ...args).catch(e => {
68
68
  LOG._error && LOG.error(e)
@@ -95,8 +95,8 @@ class MessagingService extends cds.Service {
95
95
  }
96
96
 
97
97
  prepareTopic(topic, _inbound) {
98
- // In local-messaging there's a 'short curcuit' so we must not modify the topic
99
- if (this.options.kind === 'local-messaging') return topic
98
+ // In local messaging there's a 'short curcuit' so we must not modify the topic
99
+ if (this.options.local) return topic
100
100
  let res = topic
101
101
  if (!_inbound && this.options.publishPrefix) res = this.options.publishPrefix + res
102
102
  if (_inbound && this.options.subscribePrefix) res = this.options.subscribePrefix + res
@@ -123,10 +123,11 @@ class MessagingService extends cds.Service {
123
123
  this.prepareHeaders(msg.headers, msg.event)
124
124
  msg.event = this.prepareTopic(msg.event, false)
125
125
  } else if (this.subscribedTopics) {
126
- msg.event =
126
+ const subscribedEvent =
127
127
  this.subscribedTopics.get(msg.event) ||
128
- (this.wildcarded && this.subscribedTopics.get(this.wildcarded(msg.event))) ||
129
- msg.event
128
+ (this.wildcarded && this.subscribedTopics.get(this.wildcarded(msg.event)))
129
+ if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${msg.event}' found.`)
130
+ msg.event = subscribedEvent
130
131
  }
131
132
  return msg
132
133
  }
@@ -18,7 +18,9 @@ const getSafeNumber = str => {
18
18
  const needArrayProps = Object.fromEntries(
19
19
  ['where', 'search', 'xpr', 'columns', 'expand', 'orderBy', 'ref', 'args'].map(propName => [
20
20
  propName,
21
- cur => Array.isArray(cur) && (cur.length !== 0 || propName === 'expand' || propName === 'ref')
21
+ cur =>
22
+ (Array.isArray(cur) && (cur.length !== 0 || propName === 'expand' || propName === 'ref')) ||
23
+ (propName === 'expand' && cur === '*')
22
24
  ])
23
25
  )
24
26
 
@@ -111,6 +113,7 @@ function getProp(obj, propName) {
111
113
  if (isValid) {
112
114
  return obj[propName]
113
115
  }
116
+
114
117
  throw new Error(`Invalid property '${propName}' provided`)
115
118
  }
116
119
 
@@ -118,10 +121,12 @@ function hasValidProps(obj, ...names) {
118
121
  for (const propName of names) {
119
122
  const validate = validators[propName]
120
123
  const isValid = validate && validate(obj[propName])
124
+
121
125
  if (!isValid) {
122
126
  return false
123
127
  }
124
128
  }
129
+
125
130
  return true
126
131
  }
127
132
 
@@ -133,16 +138,20 @@ function _args(args) {
133
138
  res.push(cur)
134
139
  continue
135
140
  }
141
+
136
142
  if (hasValidProps(cur, 'func', 'args')) {
137
143
  res.push(`${cur.func}(${_args(cur.args)})`)
138
144
  }
145
+
139
146
  if (hasValidProps(cur, 'ref')) {
140
147
  res.push(cur.ref.join('/'))
141
148
  }
149
+
142
150
  if (hasValidProps(cur, 'val')) {
143
151
  res.push(formatVal(cur.val))
144
152
  }
145
153
  }
154
+
146
155
  return res.join(',')
147
156
  }
148
157
 
@@ -169,17 +178,20 @@ const _format = (cur, element, target, kind, isLambda) => {
169
178
  const _isLambda = (cur, next) => {
170
179
  if (cur !== 'exists') return
171
180
  const last = Array.isArray(next.ref) && next.ref.slice(-1)[0]
172
- return last && hasValidProps(last, 'id') && Object.prototype.hasOwnProperty.call(last, 'where')
181
+ return last && hasValidProps(last, 'id')
173
182
  }
174
183
 
175
184
  function _xpr(expr, target, kind, isLambda) {
176
185
  const res = []
177
186
  const openBrackets = []
187
+
178
188
  for (let i = 0; i < expr.length; i++) {
179
189
  const cur = expr[i]
190
+
180
191
  if (typeof cur === 'string') {
181
192
  // REVISIT: will it be fixed with a new odata2cqn and follow-ups?
182
193
  const isOrIsNotValue = cur.match(/^is\s(not)?\s*(.+)$/)
194
+
183
195
  if (cur === '(') {
184
196
  openBrackets.push(res.length)
185
197
  continue
@@ -221,6 +233,7 @@ function _xpr(expr, target, kind, isLambda) {
221
233
  if (formatted !== undefined) res.push(formatted)
222
234
  }
223
235
  }
236
+
224
237
  return res.join(' ')
225
238
  }
226
239
 
@@ -229,7 +242,6 @@ const _keysOfWhere = (where, kind, target) => {
229
242
 
230
243
  if (kind === 'rest') {
231
244
  const keys = where.length === 1 ? getProp(where[0], 'val') : getProp(where[2], 'val')
232
-
233
245
  return `/${keys}`
234
246
  }
235
247
 
@@ -247,6 +259,7 @@ const _keysOfWhere = (where, kind, target) => {
247
259
  res.push(cur)
248
260
  }
249
261
  }
262
+
250
263
  return `(${res.join('')})`
251
264
  }
252
265
 
@@ -274,6 +287,7 @@ function _from(from, kind, model) {
274
287
 
275
288
  const path = []
276
289
  let queryTarget
290
+
277
291
  for (const curRef of ref) {
278
292
  if (hasValidProps(curRef, 'where', 'id')) {
279
293
  const { where, id } = curRef
@@ -285,6 +299,7 @@ function _from(from, kind, model) {
285
299
  path.push(curRef)
286
300
  }
287
301
  }
302
+
288
303
  return { url: _entityUrl(path.join('/')), queryTarget }
289
304
  }
290
305
 
@@ -292,62 +307,83 @@ const _parseColumnsV2 = (columns, prefix = []) => {
292
307
  const select = []
293
308
  const expand = []
294
309
 
295
- for (const col of columns) {
296
- if (hasValidProps(col, 'ref')) {
297
- const ref = [...prefix, ...col.ref].join('/')
298
- if (hasValidProps(col, 'expand')) {
299
- const parsed = _parseColumnsV2(col.expand, [ref])
300
- expand.push(ref, ...parsed.expand)
310
+ for (const column of columns) {
311
+ if (hasValidProps(column, 'ref')) {
312
+ const refName = [...prefix, ...column.ref].join('/')
313
+
314
+ if (hasValidProps(column, 'expand')) {
315
+ const parsed = _parseColumnsV2(column.expand, [refName])
316
+ expand.push(refName, ...parsed.expand)
301
317
  select.push(...parsed.select)
302
318
  } else {
303
- select.push(ref)
319
+ select.push(refName)
304
320
  }
305
321
  }
306
- if (col === '*') {
322
+
323
+ if (column === '*') {
307
324
  select.push(`${prefix.join('/')}/*`)
308
325
  }
309
326
  }
327
+
310
328
  return { select, expand }
311
329
  }
312
330
 
313
- const _parseColumns = columns => {
331
+ const _parseColumns = (columns, options) => {
332
+ const isExpand = options.expand
314
333
  const select = []
315
334
  const expand = []
316
335
 
317
- for (const col of columns) {
318
- if (hasValidProps(col, 'ref')) {
319
- let ref = col.ref.join('/')
320
- if (hasValidProps(col, 'expand')) {
321
- const curOptions = getOptions(col).join(';')
322
- ref += curOptions ? `(${curOptions})` : ''
323
- expand.push(ref)
336
+ if (columns === '*') {
337
+ return { select, expand }
338
+ }
339
+
340
+ const isSelectAll = select =>
341
+ select.length === 1 && (select[0] === '*' || (select[0].ref && select[0].ref[0] === '*'))
342
+
343
+ for (const column of columns) {
344
+ if (hasValidProps(column, 'ref')) {
345
+ const refName = column.ref.join('/')
346
+ let refNameWithOptions = refName
347
+
348
+ if (hasValidProps(column, 'expand')) {
349
+ const curOptions = getOptions(column).join(';')
350
+ refNameWithOptions += curOptions ? `(${curOptions})` : ''
351
+ expand.push(refNameWithOptions)
352
+ if (!isSelectAll(select) && !isExpand) select.push(refName)
324
353
  // REVISIT: expand to one & limit in options
325
- // > const expanded = $expand(col.expand)
326
- // > expand.push(expanded ? `${ref}(${expanded})` : ref)
354
+ // > const expanded = $expand(column.expand)
355
+ // > expand.push(expanded ? `${refNameWithOptions}(${expanded})` : ref)
327
356
  // see xtest('READ with expand'... in custom handler test
328
357
  } else {
329
- select.push(ref)
358
+ select.push(refNameWithOptions)
330
359
  }
331
360
  }
332
- if (col === '*') {
333
- select.push('*')
361
+
362
+ if (column === '*') {
363
+ select.push(column)
334
364
  }
335
365
  }
336
- // omit '$select' option if contains only '*'
337
- if (select.length === 1 && (select[0] === '*' || (select[0].ref && select[0].ref[0] === '*'))) {
366
+
367
+ // omit $select query option if it only contains '*'
368
+ if (isSelectAll(select)) {
338
369
  select.pop()
339
370
  }
340
- return { select, expand }
371
+
372
+ const uniqueSelect = [...new Set(select)]
373
+ return { select: uniqueSelect, expand }
341
374
  }
342
375
 
343
- function $select(columns, kind, separator = '&') {
344
- const { select, expand } = kind === 'odata-v2' ? _parseColumnsV2(columns) : _parseColumns(columns)
376
+ function $select(columns, kind, separator = '&', isExpand = false) {
377
+ const { select, expand } =
378
+ kind === 'odata-v2' ? _parseColumnsV2(columns) : _parseColumns(columns, { expand: isExpand })
379
+
345
380
  const res = []
346
381
  if (expand.length) res.unshift('$expand=' + expand.join(','))
347
382
  if (select.length) res.unshift('$select=' + select.join(','))
348
383
  return res.join(separator)
349
384
  }
350
- const $expand = columns => $select(columns, 'odata', ';')
385
+
386
+ const $expand = columns => $select(columns, 'odata', ';', true)
351
387
 
352
388
  function $count(count, kind) {
353
389
  if (count !== true) return ''
@@ -357,26 +393,32 @@ function $count(count, kind) {
357
393
 
358
394
  function $limit(limit) {
359
395
  const res = []
396
+
360
397
  if (hasValidProps(limit, 'rows')) {
361
398
  res.push('$top=' + getProp(limit.rows, 'val'))
362
399
  }
400
+
363
401
  if (hasValidProps(limit, 'offset')) {
364
402
  res.push('$skip=' + getProp(limit.offset, 'val'))
365
403
  }
404
+
366
405
  return res
367
406
  }
368
407
 
369
408
  function $orderBy(orderBy) {
370
409
  const res = []
410
+
371
411
  for (const cur of orderBy) {
372
412
  if (hasValidProps(cur, 'ref', 'sort')) {
373
413
  res.push(cur.ref.join('/') + ' ' + cur.sort)
374
414
  continue
375
415
  }
416
+
376
417
  if (hasValidProps(cur, 'ref')) {
377
418
  res.push(cur.ref.join('/'))
378
419
  }
379
420
  }
421
+
380
422
  return '$orderby=' + res.join(',')
381
423
  }
382
424
 
@@ -388,17 +430,21 @@ function parseSearch(search) {
388
430
  // search term must not be formatted
389
431
  res.push('(', ...parseSearch(cur.xpr), ')')
390
432
  }
433
+
391
434
  if (hasValidProps(cur, 'val')) {
392
435
  // search term must not be formatted
393
436
  res.push(`"${cur.val}"`)
394
437
  }
438
+
395
439
  if (typeof cur === 'string') {
396
440
  const upperCur = cur.toUpperCase()
441
+
397
442
  if (upperCur === 'OR' || upperCur === 'AND' || upperCur === 'NOT') {
398
443
  res.push(upperCur)
399
444
  }
400
445
  }
401
446
  }
447
+
402
448
  return res
403
449
  }
404
450
 
@@ -428,33 +474,48 @@ function $one(one, url, kind) {
428
474
  const _isOdataUrlWithKeys = (url, kind) => kind !== 'rest' && /^[\w\.]+\(.*\)/.test(url)
429
475
 
430
476
  const parsers = {
431
- columns: (cqnPart, url, kind) => $select(cqnPart, kind),
432
- expand: (cqnPart, url, kind) => $expand(cqnPart),
433
- where: (cqnPart, url, kind, target) => $where(cqnPart, target, kind),
434
- search: (cqnPart, url, kind) => $search(cqnPart, kind),
435
- orderBy: cqnPart => $orderBy(cqnPart),
436
- count: (cqnPart, url, kind) => $count(cqnPart, kind),
437
- limit: cqnPart => $limit(cqnPart),
438
- one: (cqnPart, url, kind) => $one(cqnPart, url, kind)
477
+ columns: (cqnPart, url, kind, target, isCount) => !isCount && $select(cqnPart, kind),
478
+ expand: (cqnPart, url, kind, target, isCount) => !isCount && $expand(cqnPart),
479
+ where: (cqnPart, url, kind, target, isCount) => $where(cqnPart, target, kind),
480
+ search: (cqnPart, url, kind, target, isCount) => $search(cqnPart, kind),
481
+ orderBy: (cqnPart, url, kind, target, isCount) => !isCount && $orderBy(cqnPart),
482
+ count: (cqnPart, url, kind, target, isCount) => !isCount && $count(cqnPart, kind),
483
+ limit: (cqnPart, url, kind, target, isCount) => !isCount && $limit(cqnPart),
484
+ one: (cqnPart, url, kind, target, isCount) => !isCount && $one(cqnPart, url, kind)
439
485
  }
440
486
 
441
- function getOptions(cqnPart, url, kind, target) {
487
+ function getOptions(cqnPart, url, kind, target, isCount) {
442
488
  const options = []
489
+
443
490
  for (const opt in cqnPart) {
444
- if (cqnPart[opt] === undefined) continue
491
+ const cqnPartOpt = cqnPart[opt]
492
+ if (cqnPartOpt === undefined) continue
445
493
  if (!hasValidProps(cqnPart, opt)) throw new Error(`Feature not supported: SELECT statement with .${opt}`)
446
- const parsed = parsers[opt] && parsers[opt](cqnPart[opt], url, kind, target)
494
+ const parser = parsers[opt]
495
+ const parsed = parser && parser(cqnPartOpt, url, kind, target, isCount)
447
496
  const parsedOpts = (Array.isArray(parsed) && parsed) || (parsed && [parsed]) || []
448
497
  options.push(...parsedOpts)
449
498
  }
499
+
450
500
  return options
451
501
  }
452
502
 
503
+ const _isCount = SELECT => {
504
+ if (SELECT.columns) {
505
+ const columns = getProp(SELECT, 'columns')
506
+ return columns.some(
507
+ c => c.func === 'count' && c.as === '$count' && c.args && c.args.length === 1 && c.args[0] === '*'
508
+ )
509
+ }
510
+ return false
511
+ }
512
+
453
513
  const _select = (cqn, kind, model) => {
454
514
  const SELECT = getProp(cqn, 'SELECT')
455
515
  const { url, queryTarget } = _from(getProp(SELECT, 'from'), kind, model)
456
- const queryOptions = getOptions(SELECT, url, kind, queryTarget).join('&')
457
- const path = queryOptions ? `${url}?${queryOptions}` : `${url}`
516
+ const isCount = _isCount(SELECT)
517
+ const queryOptions = getOptions(SELECT, url, kind, queryTarget, isCount).join('&')
518
+ const path = `${url}${isCount ? '/$count' : ''}${queryOptions ? `?${queryOptions}` : ''}`
458
519
  return { method: 'GET', path }
459
520
  }
460
521
 
@@ -474,6 +535,7 @@ const _copyData = data => {
474
535
  ? data[property].val
475
536
  : data[property]
476
537
  }
538
+
477
539
  return copied
478
540
  }
479
541
 
@@ -481,12 +543,14 @@ const _update = (cqn, kind, model) => {
481
543
  const UPDATE = getProp(cqn, 'UPDATE')
482
544
  const { url, queryTarget } = _from(getProp(UPDATE, 'entity'), kind, model)
483
545
  let keys = ''
546
+
484
547
  if (UPDATE.where) {
485
548
  if (_isOdataUrlWithKeys(url, kind)) {
486
549
  throw new Error('Cannot generate URL for UPDATE CQN. Conflicting .from and .where')
487
550
  }
488
551
  keys = _keysOfWhere(getProp(UPDATE, 'where'), kind, queryTarget)
489
552
  }
553
+
490
554
  // TODO: support for .set as well
491
555
  const body = _copyData(UPDATE.data)
492
556
  return { method: 'PATCH', path: `${url}${keys}`, body }
@@ -496,12 +560,15 @@ const _delete = (cqn, kind, model) => {
496
560
  const DELETE = getProp(cqn, 'DELETE')
497
561
  const { url, queryTarget } = _from(getProp(DELETE, 'from'), kind, model)
498
562
  let keys = ''
563
+
499
564
  if (DELETE.where) {
500
565
  if (_isOdataUrlWithKeys(url, kind)) {
501
566
  throw new Error('Cannot generate URL for DELETE CQN. Conflicting .from and .where')
502
567
  }
568
+
503
569
  keys = _keysOfWhere(getProp(DELETE, 'where'), kind, queryTarget)
504
570
  }
571
+
505
572
  return { method: 'DELETE', path: `${url}${keys}` }
506
573
  }
507
574
 
@@ -1,40 +1,4 @@
1
- const cds = require('../cds')
2
- const LOG = cds.log('odata')
3
-
4
- const fs = require('fs')
5
- const path = require('path')
6
- const odataPegGrammarPath = path.join(__dirname, '../../../.internal/odata2cqn/odata2cqn.pegjs')
7
- let odataParser, peg
8
-
9
- const _pegjsExists = () => {
10
- try {
11
- peg = require('pegjs')
12
- return true
13
- } catch (e) {
14
- return false
15
- }
16
- }
17
-
18
- // generate parser if grammar and pegjs are available
19
- if (fs.existsSync(odataPegGrammarPath) && _pegjsExists() && !process.env.TRAVIS_PULL_REQUEST) {
20
- const odataPegGrammar = fs.readFileSync(odataPegGrammarPath, { encoding: 'utf8' })
21
-
22
- // generate parser object
23
- odataParser = peg.generate(odataPegGrammar)
24
-
25
- // generate parser string and asynchronously write to file
26
- const parserString = peg.generate(odataPegGrammar, { output: 'source', format: 'commonjs' })
27
- fs.writeFile(path.join(__dirname, 'odata2cqn.js'), parserString, err => {
28
- if (err && LOG._warn) {
29
- err.message = 'Unable to store generated odata2cqn grammar: ' + err.message
30
- LOG.warn(err)
31
- }
32
- })
33
- } else {
34
- odataParser = require('./odata2cqn')
35
- }
36
-
37
- const { cqn2odata, getSafeNumber } = require('./cqn2odata')
1
+ const { cqn2odata, getSafeNumber: safeNumber } = require('./cqn2odata')
38
2
 
39
3
  const strict = {
40
4
  functions: {
@@ -59,19 +23,33 @@ const strict = {
59
23
  }
60
24
  }
61
25
 
62
- const odata = (module.exports = {
26
+ let parser = require('./odata2cqn')
27
+ const odata = {
63
28
  parse: {
64
- url: (url, o) => {
65
- const options = { safeNumber: getSafeNumber }
66
- if (o === 'strict' || (o && o.strict)) options.strict = strict
67
- return odataParser.parse(decodeURIComponent(url), options)
68
- },
69
- cqn: (cqn, kind, model) => {
70
- return cqn2odata(cqn, kind, model)
29
+ url(url, o = {}) {
30
+ o = o === 'strict' ? { strict } : o.strict ? { ...o, strict } : o
31
+
32
+ const uri = decodeURIComponent(url)
33
+ let parsed = parser.parse(uri, { ...o, safeNumber })
34
+
35
+ if (typeof o.afterburner === 'function') parsed = o.afterburner(parsed)
36
+ return parsed
71
37
  }
72
38
  },
39
+
40
+ urlify: cqn2odata,
41
+
73
42
  to: {
74
- cqn: (url, o) => odata.parse.url(url, o),
75
- url: (cqn, kind, model) => odata.parse.cqn(cqn, kind, model)
43
+ cqn: (url, o) => odata.parse(url, o),
44
+ url: (cqn, kind, model) => odata.urlify(cqn, kind, model)
45
+ },
46
+
47
+ genParser() {
48
+ const source = __filename.replace('index.js', 'odata2cqn.pegjs')
49
+ const grammar = require('fs').readFileSync(source, 'utf8')
50
+ parser = require('pegjs').generate(grammar)
51
+ return parser
76
52
  }
77
- })
53
+ }
54
+
55
+ module.exports = odata