@sap/cds 5.8.4 → 5.9.2

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 (248) hide show
  1. package/CHANGELOG.md +198 -77
  2. package/app/fiori/preview.js +16 -11
  3. package/app/fiori/routes.js +15 -8
  4. package/app/index.js +1 -1
  5. package/bin/build/buildTaskFactory.js +3 -3
  6. package/bin/build/buildTaskProviderFactory.js +1 -1
  7. package/bin/build/constants.js +1 -1
  8. package/bin/build/provider/buildTaskHandlerEdmx.js +12 -7
  9. package/bin/build/provider/buildTaskHandlerInternal.js +1 -1
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -2
  11. package/bin/build/provider/hana/2migration.js +27 -24
  12. package/bin/build/provider/hana/index.js +17 -18
  13. package/bin/build/provider/hana/migrationtable.js +9 -10
  14. package/bin/build/provider/java-cf/index.js +4 -5
  15. package/bin/build/provider/node-cf/index.js +99 -6
  16. package/bin/cds.js +17 -18
  17. package/bin/deploy/to-hana/cfUtil.js +16 -19
  18. package/bin/deploy/to-hana/hana.js +7 -24
  19. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -4
  20. package/bin/mtx/in-cds.js +2 -2
  21. package/bin/serve.js +10 -3
  22. package/bin/utils/modules.js +7 -0
  23. package/bin/version.js +56 -3
  24. package/lib/compile/cdsc.js +7 -2
  25. package/lib/compile/etc/_localized.js +37 -25
  26. package/lib/compile/etc/csv.js +8 -8
  27. package/lib/compile/for/drafts.js +9 -0
  28. package/lib/compile/for/java.js +16 -0
  29. package/lib/compile/for/nodejs.js +12 -0
  30. package/lib/compile/index.js +3 -0
  31. package/lib/compile/minify.js +16 -2
  32. package/lib/compile/parse.js +2 -2
  33. package/lib/compile/resolve.js +35 -18
  34. package/lib/compile/to/json.js +3 -1
  35. package/lib/compile/to/sql.js +2 -2
  36. package/lib/compile/to/srvinfo.js +4 -2
  37. package/lib/connect/bindings.js +1 -1
  38. package/lib/connect/index.js +3 -4
  39. package/lib/core/entities.js +15 -14
  40. package/lib/core/index.js +39 -36
  41. package/lib/core/reflect.js +4 -2
  42. package/lib/deploy.js +114 -127
  43. package/lib/env/defaults.js +1 -0
  44. package/lib/env/index.js +165 -165
  45. package/lib/env/presets.js +1 -0
  46. package/lib/env/requires.js +121 -50
  47. package/lib/index.js +2 -0
  48. package/lib/log/format/kibana.js +2 -2
  49. package/lib/ql/SELECT.js +10 -0
  50. package/lib/ql/parse.js +1 -0
  51. package/lib/req/cds-context.js +4 -1
  52. package/lib/req/context.js +50 -56
  53. package/lib/req/event.js +1 -6
  54. package/lib/req/locale.js +6 -5
  55. package/lib/req/request.js +2 -0
  56. package/lib/req/user.js +7 -5
  57. package/lib/serve/Service-api.js +10 -7
  58. package/lib/serve/Service-dispatch.js +9 -11
  59. package/lib/serve/Service-methods.js +30 -41
  60. package/lib/serve/Transaction.js +10 -7
  61. package/lib/serve/adapters.js +11 -9
  62. package/lib/serve/factory.js +14 -9
  63. package/lib/serve/index.js +28 -15
  64. package/lib/utils/data.js +1 -1
  65. package/lib/utils/index.js +27 -30
  66. package/lib/utils/resources/index.js +101 -0
  67. package/lib/utils/resources/tar.js +71 -0
  68. package/lib/utils/resources/utils.js +11 -0
  69. package/libx/_runtime/audit/Service.js +36 -39
  70. package/libx/_runtime/audit/generic/personal/access.js +3 -4
  71. package/libx/_runtime/audit/generic/personal/modification.js +3 -4
  72. package/libx/_runtime/audit/utils/v2.js +1 -2
  73. package/libx/_runtime/auth/index.js +126 -84
  74. package/libx/_runtime/auth/strategies/JWT.js +12 -19
  75. package/libx/_runtime/auth/strategies/dummy.js +1 -5
  76. package/libx/_runtime/auth/strategies/dwc.js +11 -9
  77. package/libx/_runtime/auth/strategies/mock.js +0 -4
  78. package/libx/_runtime/auth/strategies/{utils/xssec.js → xssecUtils.js} +7 -4
  79. package/libx/_runtime/auth/strategies/xsuaa.js +12 -19
  80. package/libx/_runtime/auth/utils.js +22 -1
  81. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +104 -98
  82. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +8 -3
  83. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  84. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  85. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/language.js +2 -8
  86. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +4 -29
  87. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -1
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +3 -2
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +2 -2
  90. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +4 -6
  91. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +24 -21
  92. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +8 -2
  93. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +2 -0
  94. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -6
  95. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -12
  96. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +33 -9
  97. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +56 -0
  98. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -2
  99. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +10 -3
  100. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +9 -11
  101. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +6 -3
  102. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +4 -2
  103. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/utils.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/rest/utils/binary.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/rest/utils/key-value-utils.js +2 -3
  106. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +6 -4
  107. package/libx/_runtime/cds-services/adapter/rest/utils/result.js +1 -0
  108. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +8 -5
  109. package/libx/_runtime/cds-services/services/Service.js +40 -0
  110. package/libx/_runtime/cds-services/services/utils/columns.js +4 -3
  111. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -4
  112. package/libx/_runtime/cds-services/services/utils/differ.js +3 -3
  113. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +4 -4
  114. package/libx/_runtime/cds-services/util/assert.js +20 -14
  115. package/libx/_runtime/cds.js +9 -1
  116. package/libx/_runtime/common/aspects/any.js +5 -0
  117. package/libx/_runtime/common/aspects/entity.js +25 -7
  118. package/libx/_runtime/common/aspects/utils.js +2 -2
  119. package/libx/_runtime/common/composition/data.js +6 -0
  120. package/libx/_runtime/common/composition/insert.js +3 -2
  121. package/libx/_runtime/common/composition/tree.js +4 -10
  122. package/libx/_runtime/common/composition/update.js +4 -4
  123. package/libx/_runtime/common/constants/draft.js +29 -26
  124. package/libx/_runtime/common/error/constants.js +2 -2
  125. package/libx/_runtime/common/error/frontend.js +7 -15
  126. package/libx/_runtime/common/generic/auth/capabilities.js +59 -0
  127. package/libx/_runtime/common/generic/auth/constants.js +20 -0
  128. package/libx/_runtime/common/generic/auth/expand.js +54 -0
  129. package/libx/_runtime/common/generic/auth/index.js +32 -0
  130. package/libx/_runtime/common/generic/auth/insertOnly.js +15 -0
  131. package/libx/_runtime/common/generic/auth/readOnly.js +26 -0
  132. package/libx/_runtime/common/generic/auth/requires.js +34 -0
  133. package/libx/_runtime/common/generic/auth/restrict.js +298 -0
  134. package/libx/_runtime/common/generic/auth/restrictions.js +85 -0
  135. package/libx/_runtime/common/generic/auth/utils.js +213 -0
  136. package/libx/_runtime/common/generic/crud.js +8 -6
  137. package/libx/_runtime/common/generic/etag.js +1 -1
  138. package/libx/_runtime/common/generic/input.js +35 -35
  139. package/libx/_runtime/common/generic/sorting.js +2 -3
  140. package/libx/_runtime/common/generic/temporal.js +2 -2
  141. package/libx/_runtime/common/i18n/messages.properties +1 -1
  142. package/libx/_runtime/common/toggles/handler.js +21 -0
  143. package/libx/_runtime/common/utils/copy.js +10 -1
  144. package/libx/_runtime/common/utils/cqn2cqn4sql.js +111 -35
  145. package/libx/_runtime/common/utils/csn.js +63 -1
  146. package/libx/_runtime/common/utils/dollar.js +10 -1
  147. package/libx/_runtime/common/utils/draft.js +46 -7
  148. package/libx/_runtime/common/utils/entityFromCqn.js +13 -9
  149. package/libx/_runtime/common/utils/extensibilityUtils.js +18 -0
  150. package/libx/_runtime/common/utils/foreignKeyPropagations.js +88 -104
  151. package/libx/_runtime/common/utils/generateOnCond.js +4 -1
  152. package/libx/_runtime/common/utils/quotingStyles.js +2 -0
  153. package/libx/_runtime/common/utils/resolveStructured.js +25 -9
  154. package/libx/_runtime/common/utils/resolveView.js +4 -1
  155. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -16
  156. package/libx/_runtime/common/utils/structured.js +33 -37
  157. package/libx/_runtime/common/utils/template.js +17 -8
  158. package/libx/_runtime/common/utils/templateProcessor.js +28 -28
  159. package/libx/_runtime/db/data-conversion/post-processing.js +118 -412
  160. package/libx/_runtime/db/expand/expandCQNToJoin.js +45 -41
  161. package/libx/_runtime/db/expand/rawToExpanded.js +29 -8
  162. package/libx/_runtime/db/generic/index.js +1 -3
  163. package/libx/_runtime/db/generic/input.js +5 -10
  164. package/libx/_runtime/db/generic/rewrite.js +5 -2
  165. package/libx/_runtime/db/generic/structured.js +2 -2
  166. package/libx/_runtime/db/query/delete.js +2 -2
  167. package/libx/_runtime/db/query/insert.js +1 -1
  168. package/libx/_runtime/db/query/update.js +9 -14
  169. package/libx/_runtime/db/sql-builder/CreateBuilder.js +4 -3
  170. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
  171. package/libx/_runtime/db/sql-builder/InsertBuilder.js +14 -1
  172. package/libx/_runtime/db/sql-builder/SelectBuilder.js +3 -2
  173. package/libx/_runtime/db/sql-builder/dataTypes.js +3 -3
  174. package/libx/_runtime/db/utils/columns.js +3 -3
  175. package/libx/_runtime/db/utils/normalizeTimeData.js +2 -2
  176. package/libx/_runtime/db/utils/propagateForeignKeys.js +6 -2
  177. package/libx/_runtime/extensibility/mps/index.js +5 -0
  178. package/libx/_runtime/extensibility/mps/service.js +111 -0
  179. package/libx/_runtime/extensibility/mps/tar.js +42 -0
  180. package/libx/_runtime/extensibility/mps/utils.js +11 -0
  181. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformREAD.js +0 -0
  182. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformRESULT.js +17 -5
  183. package/libx/_runtime/{fiori → extensibility}/uiflex/handler/transformWRITE.js +1 -0
  184. package/libx/_runtime/extensibility/uiflex/index.js +54 -0
  185. package/libx/_runtime/extensibility/uiflex/service.js +276 -0
  186. package/libx/_runtime/{fiori → extensibility}/uiflex/utils.js +22 -7
  187. package/libx/_runtime/fiori/generic/activate.js +2 -2
  188. package/libx/_runtime/fiori/generic/before.js +4 -4
  189. package/libx/_runtime/fiori/generic/new.js +3 -3
  190. package/libx/_runtime/fiori/generic/patch.js +1 -1
  191. package/libx/_runtime/fiori/generic/read.js +58 -66
  192. package/libx/_runtime/fiori/generic/readOverDraft.js +74 -16
  193. package/libx/_runtime/fiori/utils/handler.js +6 -13
  194. package/libx/_runtime/fiori/utils/where.js +6 -5
  195. package/libx/_runtime/hana/Service.js +4 -10
  196. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +1 -1
  197. package/libx/_runtime/hana/driver.js +2 -2
  198. package/libx/_runtime/hana/execute.js +45 -75
  199. package/libx/_runtime/hana/pool.js +1 -1
  200. package/libx/_runtime/hana/streaming.js +2 -1
  201. package/libx/_runtime/index.js +6 -6
  202. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +5 -21
  203. package/libx/_runtime/messaging/Outbox.js +2 -2
  204. package/libx/_runtime/messaging/common-utils/AMQPClient.js +4 -14
  205. package/libx/_runtime/messaging/common-utils/connections.js +5 -7
  206. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +30 -0
  207. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -1
  208. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +36 -30
  209. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -12
  210. package/libx/_runtime/messaging/enterprise-messaging.js +8 -8
  211. package/libx/_runtime/messaging/file-based.js +5 -5
  212. package/libx/_runtime/messaging/message-queuing.js +14 -12
  213. package/libx/_runtime/messaging/outbox/utils.js +18 -19
  214. package/libx/_runtime/messaging/redis-messaging.js +91 -0
  215. package/libx/_runtime/messaging/service.js +8 -6
  216. package/libx/_runtime/remote/Service.js +44 -8
  217. package/libx/_runtime/remote/utils/client.js +24 -19
  218. package/libx/_runtime/remote/utils/data.js +11 -11
  219. package/libx/_runtime/sqlite/Service.js +6 -9
  220. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +5 -2
  221. package/libx/_runtime/types/api.js +10 -2
  222. package/libx/common/utils/ucsn.js +109 -0
  223. package/libx/gql/resolvers/crud/update.js +5 -0
  224. package/libx/gql/resolvers/parse/ast2cqn/columns.js +3 -1
  225. package/libx/gql/schema/typeDefMap.js +2 -2
  226. package/libx/odata/afterburner.js +110 -16
  227. package/libx/odata/cqn2odata.js +24 -27
  228. package/libx/odata/grammar.pegjs +9 -1
  229. package/libx/odata/parseToCqn.js +39 -0
  230. package/libx/odata/parser.js +1 -1
  231. package/libx/rest/RestAdapter.js +9 -1
  232. package/libx/rest/middleware/input.js +54 -0
  233. package/libx/rest/middleware/operation.js +14 -1
  234. package/libx/rest/middleware/parse.js +11 -7
  235. package/package.json +2 -2
  236. package/server.js +34 -19
  237. package/srv/audit-log.cds +2 -2
  238. package/srv/flex.cds +8 -2
  239. package/srv/flex.js +1 -1
  240. package/srv/mps.cds +23 -0
  241. package/srv/mps.js +1 -0
  242. package/libx/_runtime/auth/strategies/utils/uaa.js +0 -21
  243. package/libx/_runtime/common/generic/auth.js +0 -874
  244. package/libx/_runtime/common/toggles/alpha.js +0 -43
  245. package/libx/_runtime/db/generic/arrayed.js +0 -33
  246. package/libx/_runtime/fiori/uiflex/index.js +0 -35
  247. package/libx/_runtime/fiori/uiflex/service.js +0 -150
  248. package/libx/rest/utils/data.js +0 -60
@@ -34,9 +34,8 @@ const getSimpleCategory = category => {
34
34
  }
35
35
 
36
36
  const rowKeysGenerator = eventName => {
37
+ if (eventName === 'UPDATE') return
37
38
  return (keyNames, row, template) => {
38
- if (eventName === 'UPDATE') return
39
-
40
39
  for (const keyName of keyNames) {
41
40
  if (Object.prototype.hasOwnProperty.call(row, keyName)) {
42
41
  continue
@@ -59,7 +58,10 @@ const _isDraftCoreComputed = (req, element, event) =>
59
58
  element['@Core.Computed'] &&
60
59
  !((event === 'CREATE' && element['@cds.on.insert']) || element['@cds.on.update'])
61
60
 
62
- const _getMediaTypeProperty = element => element['@Core.MediaType'] && element['@Core.MediaType']['=']
61
+ const _isStreamingProperty = (elements, row, property) =>
62
+ Object.values(elements).some(
63
+ element => element['@Core.MediaType'] && element['@Core.MediaType']['='] === property && row[element.name]
64
+ )
63
65
 
64
66
  const _getMediaTypeValue = req =>
65
67
  req._.req &&
@@ -101,15 +103,15 @@ const _processCategory = ({ row, key, category, isRoot, event, value, req, eleme
101
103
  }
102
104
 
103
105
  // set media type from content-type header if streaming
104
- const mtProperty = _getMediaTypeProperty(element)
106
+ const isStreaming = _isStreamingProperty(element.parent.elements, row, key)
105
107
  const mtValue = _getMediaTypeValue(req)
106
- if (category === 'stream' && row[key] && mtProperty && mtValue) row[mtProperty] = mtValue
108
+ if (category === 'stream' && isStreaming && mtValue) row[key] = mtValue
107
109
  }
108
110
 
109
111
  const processorFn = (errors, req) => {
110
112
  const { event } = req
111
113
 
112
- return ({ row, key, element, plain, isRoot, pathSegments }) => {
114
+ return ({ row, key, element, plain, isRoot, path }) => {
113
115
  const categories = plain.categories
114
116
  // ugly pointer passing for sonar
115
117
  const value = { mandatory: false, val: row && row[key] }
@@ -123,7 +125,7 @@ const processorFn = (errors, req) => {
123
125
  }
124
126
 
125
127
  // REVISIT: Convert checkInputConstraints to template mechanism
126
- checkInputConstraints({ element, value: value.val, errors, pathSegments, event })
128
+ checkInputConstraints({ element, value: value.val, errors, path, event })
127
129
  }
128
130
  }
129
131
 
@@ -153,11 +155,11 @@ const _pick = element => {
153
155
  categories.push('associationEffective')
154
156
  }
155
157
 
156
- if (element.key && !DRAFT_COLUMNS_MAP[element.name] && element.type === 'cds.UUID') {
158
+ if (element.key && !DRAFT_COLUMNS_MAP[element.name] && element.isUUID) {
157
159
  categories.push('uuid')
158
160
  }
159
161
 
160
- if (element['@Core.MediaType']) categories.push('stream')
162
+ if (element['@Core.IsMediaType']) categories.push('stream')
161
163
 
162
164
  if (categories.length) return { categories }
163
165
  }
@@ -188,38 +190,34 @@ function _handler(req) {
188
190
  if (template.elements.size === 0) return
189
191
 
190
192
  const errors = []
193
+ const args = {
194
+ processFn: processorFn(errors, req),
195
+ template,
196
+ pathOptions: {
197
+ rowKeysGenerator: rowKeysGenerator(req.event),
198
+ includeKeyValues: true,
199
+ path: []
200
+ }
201
+ }
202
+ if (_isBoundAction(req)) {
203
+ const pathSegment = _getBoundActionBindingParameter(req)
204
+ const keys = req._ && req._.params && req._.params[0]
205
+ if (pathSegment) {
206
+ args.pathOptions.path.push({ key: pathSegment, url: pathSegment })
207
+ }
208
+
209
+ if (keys && 'IsActiveEntity' in keys) {
210
+ args.pathOptions.extraKeys = { IsActiveEntity: keys.IsActiveEntity }
211
+ }
212
+ }
213
+
191
214
  const data = getDataFromCQN(req.query) // REVISIT: req.data should point into req.query
192
215
 
193
216
  enrichDataWithKeysFromWhere(data, req, this)
194
217
 
195
218
  const arrayData = Array.isArray(data) ? data : [data]
196
219
  for (const row of arrayData) {
197
- let pathSegments
198
- let extraKeys
199
-
200
- if (_isBoundAction(req)) {
201
- const pathSegment = _getBoundActionBindingParameter(req)
202
- const keys = req._ && req._.params && req._.params[0]
203
- pathSegments = pathSegment ? [pathSegment] : []
204
-
205
- if (keys && 'IsActiveEntity' in keys) {
206
- extraKeys = { IsActiveEntity: keys.IsActiveEntity }
207
- }
208
- }
209
-
210
- const args = {
211
- processFn: processorFn(errors, req),
212
- row,
213
- template,
214
- pathOptions: {
215
- extraKeys,
216
- rowKeysGenerator: rowKeysGenerator(req.event),
217
- segments: pathSegments,
218
- includeKeyValues: true
219
- }
220
- }
221
-
222
- templateProcessor(args)
220
+ templateProcessor(Object.assign(args, { row }))
223
221
  }
224
222
 
225
223
  setDataFromCQN(req) // REVISIT: req.data should point into req.query
@@ -257,6 +255,7 @@ const _processActionFunctionRow = (row, param, key, errors, event, service) => {
257
255
  const _processActionFunction = (row, eventParams, errors, event, service) => {
258
256
  for (const key in eventParams) {
259
257
  let param = eventParams[key]
258
+ // .type of action/function behaves different to .type of other csn elements
260
259
  const _type = param.type
261
260
  if (!_type && param.items) param = param.items
262
261
  _processActionFunctionRow(row, param, key, errors, event, service)
@@ -292,6 +291,7 @@ function _actionFunctionHandler(req) {
292
291
  // REVISIT: find better solution, maybe compiler?
293
292
  // resolve enums like format, range, etc.
294
293
  for (const param of Object.values(eventParams)) {
294
+ // .type of action/function behaves different to .type of other csn elements
295
295
  const _type = param.type && this.model && this.model.definitions[param.type]
296
296
  if (_type) {
297
297
  param.enum = _type.enum
@@ -1,8 +1,7 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('app')
3
3
  const { getAllKeys } = require('../../cds-services/adapter/odata-v4/odata-to-cqn/utils')
4
-
5
- const DRAFT_COLUMNS = ['IsActiveEntity', 'HasDraftEntity', 'HasActiveEntity']
4
+ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
6
5
 
7
6
  const _getStaticOrders = req => {
8
7
  const { target: entity, query } = req
@@ -20,7 +19,7 @@ const _getStaticOrders = req => {
20
19
  if (cds.env.features.implicit_sorting !== false && (req.target._isSingleton || query.SELECT.limit)) {
21
20
  const keys = getAllKeys(entity, true)
22
21
  for (const key of keys) {
23
- if (!DRAFT_COLUMNS.includes(key) && !defaultOrders.some(o => o.by['='] === key)) {
22
+ if (!(key in DRAFT_COLUMNS_MAP) && !defaultOrders.some(o => o.by['='] === key)) {
24
23
  ordersFromKeys.push({ by: { '=': key } })
25
24
  }
26
25
  }
@@ -21,14 +21,14 @@ const _getTimeDelta = (target, queryOption) => {
21
21
 
22
22
  if (
23
23
  _isDate(queryOption) ||
24
- Object.values(target.elements).some(el => el['@cds.valid.from'] && el.type === 'cds.Date')
24
+ Object.values(target.elements).some(el => el['@cds.valid.from'] && el._type === 'cds.Date')
25
25
  ) {
26
26
  return 1000 * 60 * 60 * 24
27
27
  }
28
28
 
29
29
  if (
30
30
  _isTimestamp(queryOption) &&
31
- Object.values(target.elements).some(el => el['@cds.valid.from'] && el.type === 'cds.Timestamp')
31
+ Object.values(target.elements).some(el => el['@cds.valid.from'] && el._type === 'cds.Timestamp')
32
32
  ) {
33
33
  return 1
34
34
  }
@@ -71,7 +71,7 @@ ENTITY_IS_INSERT_ONLY=Entity "{0}" is insert-only
71
71
  ENTITY_IS_READ_ONLY=Entity "{0}" is read-only
72
72
  ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
73
73
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via association "{2}"
74
- ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitely exposed as part of the service
74
+ ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
75
75
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
76
76
  EXPAND_COUNT_UNSUPPORTED="$count" is not supported for expand operation
77
77
  ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
@@ -0,0 +1,21 @@
1
+ /*
2
+ * addition for feature toggles
3
+ */
4
+ module.exports = cds => {
5
+ if (!cds.requires.toggles) return (req, res, next) => next()
6
+
7
+ return (req, res, next) => {
8
+ // inject features from dwc header
9
+ const fth = req.headers['dwc-product-configuration']
10
+ if (fth) {
11
+ const { features } = JSON.parse(Buffer.from(fth, 'base64').toString('utf-8'))
12
+ req.features = features
13
+ .filter(f => f.enabled)
14
+ .map(f => f.name)
15
+ .sort((a, b) => a.localeCompare(b))
16
+ Object.freeze(req.features)
17
+ }
18
+
19
+ next()
20
+ }
21
+ }
@@ -29,7 +29,16 @@ const deepCopyObject = obj => {
29
29
  return clone
30
30
  }
31
31
 
32
+ const deepCopy = data => {
33
+ if (Array.isArray(data)) {
34
+ return deepCopyArray(data)
35
+ }
36
+
37
+ return deepCopyObject(data)
38
+ }
39
+
32
40
  module.exports = {
33
41
  deepCopyObject,
34
- deepCopyArray
42
+ deepCopyArray,
43
+ deepCopy
35
44
  }
@@ -1,9 +1,11 @@
1
+ /* eslint-disable complexity */
2
+
1
3
  const cds = require('../../cds')
2
4
  const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
3
5
  const Query = require('../../../../lib/ql/Query')
4
6
 
5
7
  const { resolveView } = require('./resolveView')
6
- const { ensureNoDraftsSuffix } = require('./draft')
8
+ const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft } = require('./draft')
7
9
  const { flattenStructuredSelect } = require('./structured')
8
10
  const search2cqn4sql = require('./search2cqn4sql')
9
11
  const { getEntityNameFromCQN } = require('./entityFromCqn')
@@ -14,8 +16,12 @@ const { addToWhere } = require('../../common/utils/cqn')
14
16
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
15
17
  const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
16
18
  const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
19
+ const { getColumns } = require('../../cds-services/services/utils/columns')
17
20
 
18
- const OPERATIONS = ['=', '>', '<', '!=', '<>', '>=', '<=', 'like', 'between', 'in', 'not in']
21
+ const OPERATIONS_MAP = ['=', '>', '<', '!=', '<>', '>=', '<=', 'like', 'between', 'in', 'not in'].reduce((acc, cur) => {
22
+ acc[cur] = 1
23
+ return acc
24
+ }, {})
19
25
 
20
26
  const _elementFromRef = (name, entity) => {
21
27
  if (!entity) return
@@ -104,7 +110,7 @@ const convertPathExpressionToWhere = (fromClause, model, options) => {
104
110
  }
105
111
  }
106
112
 
107
- const _convertPathExpressionForInsertOrDelete = (intoClause, model) => {
113
+ const _convertPathExpressionForInsert = (intoClause, model) => {
108
114
  // .into is plain string or csn entity
109
115
  if (typeof intoClause === 'string' || intoClause.name) {
110
116
  return intoClause
@@ -177,10 +183,7 @@ const _getWindColumns = (columns, groupBy, bottomTop) => {
177
183
  return [].concat(columns, _getWindowXpr(groupBy, bottomTop))
178
184
  }
179
185
 
180
- const _convertCountNavigation = (SELECT, model) => {
181
- const entityName = SELECT.from.ref[0].id || SELECT.from.ref[0]
182
- const entity = model.definitions[entityName]
183
-
186
+ const _convertCountNavigation = (SELECT, target) => {
184
187
  const newWhere = []
185
188
  for (let i = 0; i < SELECT.where.length; i++) {
186
189
  const element = SELECT.where[i]
@@ -192,8 +195,8 @@ const _convertCountNavigation = (SELECT, model) => {
192
195
 
193
196
  const navigations = element.args[0].ref
194
197
  const navigationName = navigations[0].id || navigations[0]
195
- if (entity.elements[navigationName]) {
196
- let currentEntity = entity
198
+ if (target.elements[navigationName]) {
199
+ let currentEntity = target
197
200
  let topQuery
198
201
  let lastQuery
199
202
 
@@ -344,7 +347,7 @@ const _getWhereExistsSubSelect = (queryTarget, outerAlias, innerAlias, ref, mode
344
347
  }
345
348
 
346
349
  subSelect.where(queryTarget._relations[navName].join(innerAlias, outerAlias))
347
- if (cds.env.effective.odata.structs) {
350
+ if (cds.env.effective.odata.structs || cds.env.features.ucsn_struct_conversion) {
348
351
  flattenStructuredSelect(subSelect, model)
349
352
  }
350
353
  subSelect.columns([{ val: 1 }])
@@ -459,7 +462,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
459
462
  }
460
463
 
461
464
  if (element.xpr) {
462
- convertWhereExists({ ...query, where: element.xpr }, model, options) // > recursing into nested {xpr}
465
+ convertWhereExists({ ...query, where: element.xpr }, model, options, currentTarget) // > recursing into nested {xpr}
463
466
  } else if (element === 'exists' && where[i + 1].ref) {
464
467
  if (query.from) {
465
468
  query.from.as = outerAlias
@@ -498,8 +501,9 @@ const _convertNotEqual = (container, partName = 'where') => {
498
501
  }
499
502
  }
500
503
 
501
- if (el && el.SELECT) {
502
- _convertNotEqual(el.SELECT, partName)
504
+ if (el) {
505
+ if (el.SELECT) _convertNotEqual(el.SELECT, partName)
506
+ if (el.xpr) _convertNotEqual(el, 'xpr')
503
507
  }
504
508
  })
505
509
 
@@ -551,7 +555,7 @@ const _convertOrderByIfSkip = (orderByCQN, index) => {
551
555
 
552
556
  const _convertWhereIfSkip = (whereCQN, index) => {
553
557
  whereCQN.splice(index, 1, '1 = 1')
554
- OPERATIONS.includes(whereCQN[index + 1]) ? whereCQN.splice(index + 1, 2) : whereCQN.splice(index - 2, 2)
558
+ whereCQN[index + 1] in OPERATIONS_MAP ? whereCQN.splice(index + 1, 2) : whereCQN.splice(index - 2, 2)
555
559
  }
556
560
 
557
561
  const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, processFn) => {
@@ -564,7 +568,7 @@ const _convertOrderByOrWhereCQN = (orderByOrWhereCQN, target, model, alias, proc
564
568
 
565
569
  const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
566
570
  const alias = cqn.SELECT.from.as
567
- if (cqn.SELECT.orderBy && cqn.SELECT.orderBy.length > 1) {
571
+ if (cqn.SELECT.orderBy && cqn.SELECT.orderBy.length) {
568
572
  _convertOrderByOrWhereCQN(cqn.SELECT.orderBy, target, model, alias, _convertOrderByIfSkip)
569
573
  }
570
574
 
@@ -659,7 +663,11 @@ const _convertPathExpression = (SELECT, model, options = {}) => {
659
663
  // Okra always wants to have the key values, remove once we relax this requirement
660
664
  if (model.definitions[target] && model.definitions[target].keys) {
661
665
  SELECT.columns = Object.keys(model.definitions[target].keys)
662
- .filter(k => !model.definitions[target].keys[k].isAssociation)
666
+ .filter(
667
+ k =>
668
+ !model.definitions[target].keys[k].isAssociation &&
669
+ !columns.find(element => element.ref && element.ref[element.ref.length - 1] === k)
670
+ )
663
671
  .map(k => ({ ref: [k] }))
664
672
  } else SELECT.columns = []
665
673
  }
@@ -679,8 +687,39 @@ const _convertPathExpression = (SELECT, model, options = {}) => {
679
687
  }
680
688
  }
681
689
 
690
+ const _convertToOneEqNullInFilter = (query, target) => {
691
+ // we do not handle join or union
692
+ if (!target) return
693
+
694
+ for (let i = 0; i < query.where.length; i++) {
695
+ const w = query.where[i]
696
+ const w2 = query.where[i + 2]
697
+ if (!w2 || !w.ref || w2.val !== null) {
698
+ continue
699
+ }
700
+ const element = target.elements[w.ref[w.ref.length - 1]]
701
+ if (element && element.is2one && !element.on) {
702
+ const foreignKeys = Object.values(element.parent.elements).filter(e => e._foreignKey4 === element.name)
703
+ const replacedKeys = foreignKeys.reduce((arr, e, idx) => {
704
+ arr.push({ ref: [...w.ref.slice(0, w.ref.length - 1), e.name] }, query.where[i + 1], query.where[i + 2])
705
+ if (idx < foreignKeys.length - 1) {
706
+ arr.push('and')
707
+ }
708
+ return arr
709
+ }, [])
710
+
711
+ query.where.splice(i, 3, '(', ...replacedKeys, ')')
712
+ i += replacedKeys.length + 2
713
+ }
714
+ }
715
+ }
716
+
682
717
  // eslint-disable-next-line complexity
683
718
  const _convertSelect = (query, model, _options) => {
719
+ const { _initial } = _options
720
+ if (_initial) {
721
+ delete _options._initial
722
+ }
684
723
  const options = Object.assign(
685
724
  {
686
725
  _4db: _options.service instanceof cds.DatabaseService,
@@ -688,6 +727,7 @@ const _convertSelect = (query, model, _options) => {
688
727
  },
689
728
  _options
690
729
  )
730
+
691
731
  // ensure query is ql enabled
692
732
  if (!(query instanceof Query)) Object.setPrototypeOf(query, Object.getPrototypeOf(SELECT()))
693
733
  if (query.SELECT.from && query.SELECT.from.SELECT) {
@@ -695,9 +735,6 @@ const _convertSelect = (query, model, _options) => {
695
735
  query.SELECT.from = _convertSelect(query.SELECT.from, model, options)
696
736
  }
697
737
 
698
- // REVISIT: a temporary workaround for xpr from new parser
699
- if (cds.env.features.odata_new_parser) _flattenCQN(query)
700
-
701
738
  // lambda functions
702
739
  convertWhereExists(query.SELECT, model, options)
703
740
 
@@ -709,8 +746,14 @@ const _convertSelect = (query, model, _options) => {
709
746
 
710
747
  _convertPathExpression(query.SELECT, model, options)
711
748
  rewriteAsterisks(query, model, options)
712
- if (query.SELECT.where && _isCountNavigation(query.SELECT.where)) {
713
- _convertCountNavigation(query.SELECT, model)
749
+ if (query.SELECT.where) {
750
+ const entityName =
751
+ (query.SELECT.from.ref && (query.SELECT.from.ref[0].id || query.SELECT.from.ref[0])) || query.SELECT.from
752
+ const target = model.definitions[entityName]
753
+ if (_isCountNavigation(query.SELECT.where)) {
754
+ _convertCountNavigation(query.SELECT, target)
755
+ }
756
+ _convertToOneEqNullInFilter(query.SELECT, target)
714
757
  }
715
758
 
716
759
  // extract where clause if it is in column expand ref
@@ -727,7 +770,7 @@ const _convertSelect = (query, model, _options) => {
727
770
  search2cqn4sql(query, model, { ...query._searchOptions, ...{ entityName, alias } })
728
771
  }
729
772
 
730
- if (query.SELECT.columns && cds.env.effective.odata.structs) {
773
+ if (query.SELECT.columns && (cds.env.effective.odata.structs || cds.env.features.ucsn_struct_conversion)) {
731
774
  flattenStructuredSelect(query, model)
732
775
  }
733
776
 
@@ -736,30 +779,46 @@ const _convertSelect = (query, model, _options) => {
736
779
  _createWindowCQN(query.SELECT, model)
737
780
  }
738
781
 
782
+ // best-effort ensure columns
783
+ if (options._4db && !query.SELECT.columns) {
784
+ let target = query._target
785
+ if (target && target._unresolved && typeof target.name === 'string') {
786
+ target = model.definitions[ensureNoDraftsSuffix(target.name)] || target
787
+ }
788
+ if (target && !Object.prototype.hasOwnProperty.call(target, '_unresolved')) {
789
+ const cols = getColumns(target, { onlyNames: true, filterVirtual: true })
790
+ query.columns(cols)
791
+ if (target._isDraftEnabled && query._target._unresolved) {
792
+ query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target))
793
+ query.SELECT.columns.push({ ref: ['DraftAdministrativeData_DraftUUID'] })
794
+ }
795
+ }
796
+ }
797
+
798
+ // temporary workaround for xpr - cds v5.9.2 only
799
+ if (_initial) _flattenCQN(query)
800
+
739
801
  return query
740
802
  }
741
803
 
742
804
  const _convertInsert = (query, model, options) => {
743
805
  // resolve path expression
744
- const resolvedIntoClause = _convertPathExpressionForInsertOrDelete(query.INSERT.into, model)
806
+ const resolvedIntoClause = _convertPathExpressionForInsert(query.INSERT.into, model)
745
807
 
746
808
  // overwrite only .into, foreign keys are already set
747
- const insert = INSERT.into(resolvedIntoClause)
809
+ // 'a' added as placeholder since its overwritten by Object.assign below
810
+ const insert = INSERT.into('a')
748
811
 
749
812
  // REVISIT flatten structured types, currently its done in SQL builder
750
813
 
751
814
  // We add all previous properties ot the newly created query.
752
815
  // Reason is to not lose the query API functionality
753
- Object.assign(insert.INSERT, query.INSERT, { into: resolvedIntoClause })
754
-
755
- const targetName = insert.INSERT.into.name || insert.INSERT.into
816
+ Object.assign(insert.INSERT, query.INSERT, { into: { ref: [resolvedIntoClause], as: query.INSERT.into.as } })
756
817
 
757
- const target = model.definitions[targetName]
818
+ const target = model.definitions[resolvedIntoClause]
758
819
  if (!target) return insert
759
820
 
760
- const resolvedView = resolveView(insert, model, cds.db)
761
-
762
- return resolvedView
821
+ return resolveView(insert, model, cds.db)
763
822
  }
764
823
 
765
824
  function _modifyNavigationInWhere(whereClause, target) {
@@ -783,6 +842,11 @@ function _modifyNavigationInWhere(whereClause, target) {
783
842
  const _plainDelete = (cqn, model) => {
784
843
  const name = cqn.DELETE.from.name || (cqn.DELETE.from.ref && cqn.DELETE.from.ref[0]) || cqn.DELETE.from
785
844
  const target = model.definitions[name]
845
+
846
+ if (cqn.DELETE.where) {
847
+ _convertToOneEqNullInFilter(cqn.DELETE, target)
848
+ }
849
+
786
850
  if (!target) return cqn
787
851
 
788
852
  return resolveView(cqn, model, cds.db)
@@ -804,9 +868,13 @@ const _convertDelete = (query, model, options) => {
804
868
 
805
869
  if (alias) deleet.DELETE.from = { ref: [target], as: alias }
806
870
  if (where) deleet.where(where)
807
- if (query.DELETE.where) deleet.where(addAliasToExpression(query.DELETE.where, alias))
808
871
 
809
872
  const targetEntity = model.definitions[target]
873
+ if (query.DELETE.where) {
874
+ deleet.where(addAliasToExpression(query.DELETE.where, alias))
875
+ _convertToOneEqNullInFilter(deleet.DELETE, targetEntity)
876
+ }
877
+
810
878
  if (!targetEntity) return deleet
811
879
 
812
880
  return resolveView(deleet, model, cds.db)
@@ -815,6 +883,11 @@ const _convertDelete = (query, model, options) => {
815
883
  function _plainUpdate(cqn, model) {
816
884
  const name = cqn.UPDATE.entity.name || (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) || cqn.UPDATE.entity
817
885
  const target = model.definitions[name]
886
+
887
+ if (cqn.UPDATE.where) {
888
+ _convertToOneEqNullInFilter(cqn.UPDATE, target)
889
+ }
890
+
818
891
  if (!target) return cqn
819
892
 
820
893
  return resolveView(cqn, model, cds.db)
@@ -841,9 +914,12 @@ const _convertUpdate = (query, model, options) => {
841
914
 
842
915
  if (alias) update.UPDATE.entity = { ref: [target], as: alias }
843
916
  if (where) update.where(where)
844
- if (query.UPDATE.where) update.where(addAliasToExpression(query.UPDATE.where, alias))
845
-
846
917
  const targetEntity = model.definitions[target]
918
+ if (query.UPDATE.where) {
919
+ update.where(addAliasToExpression(query.UPDATE.where, alias))
920
+ _convertToOneEqNullInFilter(update.UPDATE, targetEntity)
921
+ }
922
+
847
923
  if (!targetEntity) return update
848
924
 
849
925
  return resolveView(update, model, cds.db)
@@ -861,7 +937,7 @@ const _convertUpdate = (query, model, options) => {
861
937
  */
862
938
  const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
863
939
  if (query.SELECT) {
864
- return _convertSelect(query, model, options)
940
+ return _convertSelect(query, model, Object.assign(options, { _initial: true }))
865
941
  }
866
942
 
867
943
  if (query.UPDATE) {
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../cds')
2
+ const resolveStructured = require('../../common/utils/resolveStructured')
2
3
 
3
4
  const { ensureNoDraftsSuffix } = require('./draft')
4
5
 
@@ -140,6 +141,7 @@ const findCsnTargetFor = (edmName, model, namespace) => {
140
141
  // probably, a combination of '_' and '.', resolving
141
142
  const finding = _findRootEntity(model, edmName, namespace)
142
143
  target = finding.target
144
+
143
145
  // something left in navigation path => x4 navigation
144
146
  // resolving within found entity
145
147
  if (target && finding.left > 0) {
@@ -151,10 +153,12 @@ const findCsnTargetFor = (edmName, model, namespace) => {
151
153
  }
152
154
  }
153
155
  }
156
+
154
157
  // remember edm <-> csn
155
158
  if (target) {
156
159
  mapping[edmName] = target
157
160
  }
161
+
158
162
  return mapping[edmName]
159
163
  }
160
164
 
@@ -191,7 +195,40 @@ const isRootEntity = (definitions, entityName) => {
191
195
  return true
192
196
  }
193
197
 
198
+ function _alias2RefRest(service) {
199
+ for (const each of Object.values(service.entities)) {
200
+ each._alias2ref = {}
201
+ const keys = each.keys
202
+ for (const key in keys) {
203
+ if (keys[key].elements) {
204
+ const structKeys = resolveStructured({ structName: key, structProperties: [] }, keys[key].elements, false, true)
205
+ for (const structKey of structKeys) {
206
+ if (each._alias2ref[structKey.key] != null) {
207
+ // key clash, aliasing not possible
208
+ each._alias2ref = {}
209
+ return
210
+ }
211
+ each._alias2ref[structKey.key] = structKey.resolved
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ const prefixForStruct = element => {
219
+ const prefixes = []
220
+ let parent = element.parent
221
+ while (parent && parent.kind !== 'entity') {
222
+ prefixes.push(parent.name)
223
+ parent = parent.parent
224
+ }
225
+ return prefixes.length ? prefixes.reverse().join('_') + '_' : ''
226
+ }
227
+
194
228
  function alias2ref(service, edm) {
229
+ if (!edm) {
230
+ return _alias2RefRest(service)
231
+ }
195
232
  const defs = edm[service.definition.name]
196
233
  for (const each of Object.values(service.entities)) {
197
234
  const def = defs[each.name.replace(service.definition.name + '.', '').replace(/\./g, '_')]
@@ -205,6 +242,29 @@ function alias2ref(service, edm) {
205
242
  }
206
243
  }
207
244
 
245
+ function getDraftTreeRoot(entity, model) {
246
+ if (entity.own('__draftTreeRoot')) return entity.__draftTreeRoot
247
+
248
+ let parent
249
+ let current = entity
250
+ while (current && !current['@Common.DraftRoot.ActivationAction']) {
251
+ const parents = []
252
+ for (const k in model.definitions) {
253
+ const e = model.definitions[k]
254
+ if (e.kind !== 'entity' || !e.compositions) continue
255
+ for (const c in e.compositions) if (e.compositions[c].target === current.name) parents.push(e)
256
+ }
257
+ if (parents.length > 1 && parents.some(p => p !== parents[0])) {
258
+ // > unable to determine single parent
259
+ parent = undefined
260
+ break
261
+ }
262
+ current = parent = parents[0]
263
+ }
264
+
265
+ return entity.set('__draftTreeRoot', parent)
266
+ }
267
+
208
268
  module.exports = {
209
269
  getEtagElement,
210
270
  findCsnTargetFor,
@@ -212,5 +272,7 @@ module.exports = {
212
272
  isRootEntity,
213
273
  getDataSubject,
214
274
  alias2ref,
215
- getComp2oneParents
275
+ getComp2oneParents,
276
+ prefixForStruct,
277
+ getDraftTreeRoot
216
278
  }
@@ -11,7 +11,16 @@ module.exports = (entryOrRow, keyOrIndex, user, timestamp) => {
11
11
  else if (entryOrRow[keyOrIndex] === '$now') entryOrRow[keyOrIndex] = timestamp
12
12
  else if (entryOrRow[keyOrIndex] === '$uuid') entryOrRow[keyOrIndex] = cds.utils.uuid()
13
13
  else if (typeof entryOrRow[keyOrIndex] === 'string') {
14
+ // NOTE: with xsuaa, user attributes are always arrays
14
15
  const attr = entryOrRow[keyOrIndex].match(/^\$user\.(.*)/)
15
- if (attr && attr.length > 1) entryOrRow[keyOrIndex] = (user.attr && user.attr[attr[1]]) || null
16
+ if (attr && attr.length > 1) {
17
+ const val = (user.attr && user.attr[attr[1]]) || null
18
+ if (Array.isArray(val)) {
19
+ if (val.length > 1) entryOrRow[keyOrIndex] = JSON.stringify(val)
20
+ else entryOrRow[keyOrIndex] = val.length > 0 ? val[0] : null
21
+ } else {
22
+ entryOrRow[keyOrIndex] = val
23
+ }
24
+ }
16
25
  }
17
26
  }