@sap/cds 6.5.0 → 6.6.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 (108) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/README.md +5 -0
  3. package/apis/services.d.ts +5 -0
  4. package/bin/build/buildTaskEngine.js +0 -2
  5. package/bin/build/buildTaskFactory.js +1 -1
  6. package/bin/build/buildTaskHandler.js +1 -1
  7. package/bin/build/provider/buildTaskProviderInternal.js +10 -6
  8. package/bin/build/provider/fiori/index.js +5 -10
  9. package/bin/build/provider/hana/2migration.js +11 -2
  10. package/bin/build/provider/hana/index.js +17 -14
  11. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  12. package/bin/build/provider/mtx-extension/index.js +18 -1
  13. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  14. package/bin/build/util.js +1 -1
  15. package/bin/cds.js +1 -5
  16. package/bin/deploy/to-hana/hana.js +10 -3
  17. package/bin/deploy/to-hana/hdiDeployUtil.js +24 -12
  18. package/bin/serve.js +32 -20
  19. package/lib/auth/jwt-auth.js +4 -4
  20. package/lib/compile/for/lean_drafts.js +55 -6
  21. package/lib/dbs/cds-deploy.js +6 -8
  22. package/lib/env/schemas/cds-rc.json +4 -0
  23. package/lib/index.js +4 -2
  24. package/lib/req/cds-context.js +3 -3
  25. package/lib/srv/bindings.js +1 -2
  26. package/lib/srv/cds-serve.js +2 -1
  27. package/lib/srv/middlewares/trace.js +31 -15
  28. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  29. package/lib/srv/srv-handlers.js +26 -7
  30. package/lib/srv/srv-methods.js +2 -2
  31. package/lib/srv/srv-models.js +4 -3
  32. package/lib/utils/cds-test.js +7 -5
  33. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  34. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -0
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +7 -8
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  48. package/libx/_runtime/cds-services/services/Service.js +8 -19
  49. package/libx/_runtime/cds-services/services/utils/columns.js +7 -4
  50. package/libx/_runtime/cds-services/util/assert.js +7 -1
  51. package/libx/_runtime/common/code-ext/WorkerReq.js +3 -1
  52. package/libx/_runtime/common/code-ext/execute.js +9 -2
  53. package/libx/_runtime/common/code-ext/handlers.js +2 -2
  54. package/libx/_runtime/common/code-ext/worker.js +9 -5
  55. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +5 -2
  56. package/libx/_runtime/common/composition/data.js +5 -2
  57. package/libx/_runtime/common/composition/tree.js +2 -0
  58. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  59. package/libx/_runtime/common/generic/etag.js +22 -10
  60. package/libx/_runtime/common/generic/input.js +12 -14
  61. package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -11
  62. package/libx/_runtime/common/utils/path.js +0 -1
  63. package/libx/_runtime/common/utils/search2cqn4sql.js +4 -1
  64. package/libx/_runtime/common/utils/structured.js +1 -0
  65. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  66. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  67. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  68. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  69. package/libx/_runtime/db/generic/input.js +2 -2
  70. package/libx/_runtime/db/generic/integrity.js +1 -0
  71. package/libx/_runtime/db/generic/virtual.js +1 -0
  72. package/libx/_runtime/db/query/read.js +3 -2
  73. package/libx/_runtime/fiori/generic/activate.js +3 -1
  74. package/libx/_runtime/fiori/generic/before.js +1 -0
  75. package/libx/_runtime/fiori/generic/edit.js +6 -1
  76. package/libx/_runtime/fiori/generic/new.js +2 -0
  77. package/libx/_runtime/fiori/generic/patch.js +2 -0
  78. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  79. package/libx/_runtime/fiori/generic/read.js +8 -2
  80. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  81. package/libx/_runtime/fiori/lean-draft.js +498 -245
  82. package/libx/_runtime/fiori/utils/delete.js +2 -0
  83. package/libx/_runtime/messaging/Outbox.js +1 -1
  84. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  85. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  86. package/libx/_runtime/messaging/file-based.js +1 -2
  87. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  88. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  89. package/libx/_runtime/messaging/service.js +0 -1
  90. package/libx/_runtime/remote/Service.js +1 -0
  91. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  92. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  93. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  94. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  95. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  96. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  97. package/libx/odata/afterburner.js +17 -5
  98. package/libx/odata/grammar.pegjs +3 -4
  99. package/libx/odata/index.js +5 -1
  100. package/libx/odata/parseToCqn.js +3 -3
  101. package/libx/odata/parser.js +1 -1
  102. package/libx/odata/utils.js +58 -1
  103. package/package.json +1 -1
  104. package/server.js +1 -1
  105. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  106. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  107. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  108. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -253,8 +253,12 @@ class OData {
253
253
  this._odataService.process(req, res).catch(err => {
254
254
  LOG.warn(err)
255
255
  // REVISIT: use i18n
256
- //do not reply with error, if response already processed (streaming)
257
- if (!res.headersSent) {
256
+ // do not reply with error, if response already processed (streaming)
257
+ // destroy response socket instead
258
+ if (res.headersSent) {
259
+ // REVISIT: temp solution until streaming is switched to express middlewares
260
+ res.socket.destroy()
261
+ } else {
258
262
  const { error, statusCode } = normalizeError(err, req)
259
263
  res.status(statusCode).send({ error })
260
264
  }
@@ -251,6 +251,14 @@ const _readEntityOrProperty = async (tx, req, segments) => {
251
251
  return odataResult
252
252
  }
253
253
 
254
+ const _reliablePagingPossible = req => {
255
+ if (req.target._isDraftEnabled) return false
256
+ if (cds.context?.http.req.query.$apply) return false
257
+ if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
258
+ if (req.query.SELECT.orderBy.some(o => !o.ref)) return false
259
+ return req.query.SELECT.orderBy.every(o => req.query.SELECT.columns.some(c => o.ref[0] === c.ref[0]))
260
+ }
261
+
254
262
  /**
255
263
  * Read an entity collection without including the count of the total amount of entities.
256
264
  *
@@ -260,6 +268,7 @@ const _readEntityOrProperty = async (tx, req, segments) => {
260
268
  * @returns {Promise}
261
269
  * @private
262
270
  */
271
+ // eslint-disable-next-line complexity
263
272
  const _readCollection = async (tx, req, odataReq) => {
264
273
  const result = (await tx.dispatch(req)) || []
265
274
  if (Array.isArray(req.query)) {
@@ -284,7 +293,23 @@ const _readCollection = async (tx, req, odataReq) => {
284
293
  const top = odataReq.getUriInfo().getQueryOption(QueryOptions.TOP)
285
294
  if (limit && limit === result.length && limit !== top && !('$nextLink' in result)) {
286
295
  const token = odataReq.getUriInfo().getQueryOption(QueryOptions.SKIPTOKEN)
287
- result.$nextLink = (token ? parseInt(token) : 0) + limit
296
+ if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
297
+ const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
298
+ const skipToken = {
299
+ r: (decoded?.r || 0) + limit,
300
+ c: req.query.SELECT.orderBy.map(o => ({
301
+ a: o.sort ? o.sort === 'asc' : true,
302
+ k: o.ref[0],
303
+ v: result[result.length - 1][o.ref[0]]
304
+ }))
305
+ }
306
+
307
+ if (limit + (decoded?.r || 0) !== top) {
308
+ result.$nextLink = Buffer.from(JSON.stringify(skipToken)).toString('base64')
309
+ }
310
+ } else {
311
+ result.$nextLink = (token ? parseInt(token) : 0) + limit
312
+ }
288
313
  }
289
314
 
290
315
  const odataResult = toODataResult(result, req)
@@ -11,6 +11,7 @@ const { isReturnMinimal } = require('../utils/handlerUtils')
11
11
  const { readAfterWrite } = require('../utils/readAfterWrite')
12
12
  const { toODataResult, postProcess, postProcessMinimal } = require('../utils/result')
13
13
  const { hasOmitValuesPreference } = require('../utils/omitValues')
14
+ const { isStreaming } = require('../utils/stream')
14
15
 
15
16
  const { getSapMessages } = require('../../../../common/error/frontend')
16
17
 
@@ -187,7 +187,7 @@ class ExpressionToCQN {
187
187
  // ignore
188
188
  }
189
189
  }
190
- return { func: `${operator ? `${operator} ` : ''}${methodName}`, args }
190
+ return operator ? [operator, { func: `${methodName}`, args }] : { func: `${methodName}`, args }
191
191
  }
192
192
 
193
193
  /* eslint-disable complexity */
@@ -41,6 +41,8 @@ const { isAsteriskColumn } = require('../../../../common/utils/rewriteAsterisks'
41
41
 
42
42
  const { ensureUnlocalized } = require('../../../../fiori/utils/handler')
43
43
 
44
+ const { skipToken: handleSkipToken } = require('../../../../../odata/utils')
45
+
44
46
  const _applyOnlyContainsFilter = apply => Object.keys(apply).length === 1 && apply.filter
45
47
 
46
48
  /**
@@ -136,9 +138,9 @@ const _apply = (uriInfo, queryOptions, entity, model) => {
136
138
  }
137
139
 
138
140
  const _topSkip = (queryOptions, target, cqn) => {
139
- if (queryOptions && (queryOptions.$top || queryOptions.$skip || queryOptions.$skiptoken)) {
141
+ if (queryOptions && (queryOptions.$top || queryOptions.$skip)) {
140
142
  const top = queryOptions.$top ? parseInt(queryOptions.$top) : getDefaultPageSize(target)
141
- const skip = parseInt(queryOptions.$skip || 0) + parseInt(queryOptions.$skiptoken || 0)
143
+ const skip = parseInt(queryOptions.$skip || 0)
142
144
  cqn.limit(Math.min(top, getMaxPageSize(target)), skip)
143
145
  }
144
146
  }
@@ -324,6 +326,10 @@ const _handleApply = (apply, select) => {
324
326
  select.push(...mergedArray)
325
327
  }
326
328
 
329
+ const _skipToken = (token, cqn) => {
330
+ handleSkipToken(token, cqn)
331
+ }
332
+
327
333
  /**
328
334
  * Transform odata READ request into a CQN object.
329
335
  *
@@ -393,6 +399,9 @@ const readToCQN = (service, target, odataReq) => {
393
399
  if (isCollectionOrToMany) {
394
400
  _topSkip(queryOptions, target, cqn)
395
401
  _orderby(uriInfo, cqn)
402
+
403
+ const skipToken = queryOptions?.$skiptoken
404
+ if (skipToken) _skipToken(skipToken, cqn)
396
405
  }
397
406
 
398
407
  if (!isCollectionOrToMany || entity._isSingleton) cqn.SELECT.one = true
@@ -166,7 +166,7 @@ class PrimitiveValueDecoder {
166
166
  throw new IllegalArgumentError(
167
167
  'Invalid value ' +
168
168
  value +
169
- ' (JavaScript ' +
169
+ ' (' +
170
170
  typeof value +
171
171
  '). ' +
172
172
  'A JSON string must be specified as value for type ' +
@@ -180,7 +180,7 @@ class PrimitiveValueDecoder {
180
180
  throw new IllegalArgumentError(
181
181
  'Invalid value ' +
182
182
  value +
183
- ' (JavaScript ' +
183
+ ' (' +
184
184
  typeof value +
185
185
  ') ' +
186
186
  'as value for type ' +
@@ -209,7 +209,7 @@ class PrimitiveValueDecoder {
209
209
  throw new IllegalArgumentError(
210
210
  'Invalid value ' +
211
211
  value +
212
- ' (JavaScript ' +
212
+ ' (' +
213
213
  typeof value +
214
214
  ') ' +
215
215
  'as value for type ' +
@@ -226,7 +226,7 @@ class PrimitiveValueDecoder {
226
226
  throw new IllegalArgumentError(
227
227
  'Invalid value ' +
228
228
  value +
229
- ' (JavaScript ' +
229
+ ' (' +
230
230
  typeof value +
231
231
  ') ' +
232
232
  'as value for type ' +
@@ -247,7 +247,7 @@ class PrimitiveValueDecoder {
247
247
  throw new IllegalArgumentError(
248
248
  'Invalid value ' +
249
249
  value +
250
- ' (JavaScript ' +
250
+ ' (' +
251
251
  typeof value +
252
252
  '). A JSON string must be specified ' +
253
253
  'as value for type ' +
@@ -262,7 +262,7 @@ class PrimitiveValueDecoder {
262
262
  throw new IllegalArgumentError(
263
263
  'Invalid value ' +
264
264
  value +
265
- ' (JavaScript ' +
265
+ ' (' +
266
266
  typeof value +
267
267
  ') ' +
268
268
  'as value for type ' +
@@ -311,7 +311,7 @@ class PrimitiveValueDecoder {
311
311
  throw new IllegalArgumentError(
312
312
  'Invalid value ' +
313
313
  value +
314
- ' (JavaScript ' +
314
+ ' (' +
315
315
  typeof value +
316
316
  '). A JSON ' +
317
317
  kind +
@@ -339,7 +339,7 @@ class PrimitiveValueDecoder {
339
339
  throw new IllegalArgumentError(
340
340
  'Invalid value ' +
341
341
  value +
342
- ' (JavaScript ' +
342
+ ' (' +
343
343
  typeof value +
344
344
  ') for enumeration type ' +
345
345
  type.getFullQualifiedName() +
@@ -544,7 +544,7 @@ class ValueConverter {
544
544
  throw new IllegalArgumentError(
545
545
  'Invalid value ' +
546
546
  value +
547
- ' (JavaScript ' +
547
+ ' (' +
548
548
  typeof value +
549
549
  ') ' +
550
550
  'for enumeration type ' +
@@ -260,7 +260,7 @@ class ValueValidator {
260
260
  throw new IllegalArgumentError(
261
261
  'Invalid value ' +
262
262
  value +
263
- ' (JavaScript ' +
263
+ ' (' +
264
264
  typeof value +
265
265
  ')' +
266
266
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -350,7 +350,7 @@ class ValueValidator {
350
350
  throw new IllegalArgumentError(
351
351
  'Invalid value ' +
352
352
  value +
353
- ' (JavaScript ' +
353
+ ' (' +
354
354
  typeof value +
355
355
  ')' +
356
356
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -383,7 +383,7 @@ class ValueValidator {
383
383
  throw new IllegalArgumentError(
384
384
  'Invalid value ' +
385
385
  value +
386
- ' (JavaScript ' +
386
+ ' (' +
387
387
  typeof value +
388
388
  ')' +
389
389
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -406,7 +406,7 @@ class ValueValidator {
406
406
  throw new IllegalArgumentError(
407
407
  'Invalid value ' +
408
408
  value +
409
- ' (JavaScript ' +
409
+ ' (' +
410
410
  typeof value +
411
411
  ')' +
412
412
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -424,7 +424,7 @@ class ValueValidator {
424
424
  throw new IllegalArgumentError(
425
425
  'Invalid value ' +
426
426
  value +
427
- ' (JavaScript ' +
427
+ ' (' +
428
428
  typeof value +
429
429
  ')' +
430
430
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -442,7 +442,7 @@ class ValueValidator {
442
442
  throw new IllegalArgumentError(
443
443
  'Invalid value ' +
444
444
  value +
445
- ' (JavaScript ' +
445
+ ' (' +
446
446
  typeof value +
447
447
  ')' +
448
448
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -510,7 +510,7 @@ class ValueValidator {
510
510
  throw this._valueError(
511
511
  value,
512
512
  'Edm.GeographyPoint or Edm.GeometryPoint',
513
- 'JavaScript object with type and coordinates'
513
+ 'Object with type and coordinates'
514
514
  )
515
515
  }
516
516
  }
@@ -528,7 +528,7 @@ class ValueValidator {
528
528
  throw this._valueError(
529
529
  value,
530
530
  'Edm.GeographyLineString or Edm.GeometryLineString',
531
- 'JavaScript object with type and coordinates'
531
+ 'Object with type and coordinates'
532
532
  )
533
533
  }
534
534
  }
@@ -543,7 +543,7 @@ class ValueValidator {
543
543
  throw this._valueError(
544
544
  value,
545
545
  'Edm.GeographyPolygon or Edm.GeometryPolygon',
546
- 'JavaScript object with type and coordinates'
546
+ 'Object with type and coordinates'
547
547
  )
548
548
  }
549
549
  }
@@ -561,7 +561,7 @@ class ValueValidator {
561
561
  throw this._valueError(
562
562
  value,
563
563
  'Edm.GeographyMultiPoint or Edm.GeometryMultiPoint',
564
- 'JavaScript object with type and coordinates'
564
+ 'Object with type and coordinates'
565
565
  )
566
566
  }
567
567
  }
@@ -579,7 +579,7 @@ class ValueValidator {
579
579
  throw this._valueError(
580
580
  value,
581
581
  'Edm.GeographyMultiLineString or Edm.GeometryMultiLineString',
582
- 'JavaScript object with type and coordinates'
582
+ 'Object with type and coordinates'
583
583
  )
584
584
  }
585
585
  }
@@ -597,7 +597,7 @@ class ValueValidator {
597
597
  throw this._valueError(
598
598
  value,
599
599
  'Edm.GeographyMultiPolygon or Edm.GeometryMultiPolygon',
600
- 'JavaScript object with type and coordinates'
600
+ 'Object with type and coordinates'
601
601
  )
602
602
  }
603
603
  }
@@ -629,7 +629,7 @@ class ValueValidator {
629
629
  throw this._valueError(
630
630
  value,
631
631
  'Edm.GeographyCollection or Edm.GeometryCollection',
632
- 'JavaScript object with type and geometries'
632
+ 'Object with type and geometries'
633
633
  )
634
634
  }
635
635
  }
@@ -725,7 +725,7 @@ class ValueValidator {
725
725
  const msg =
726
726
  'Invalid value ' +
727
727
  (typeName.includes('Geo') ? JSON.stringify(value) : value) +
728
- ' (JavaScript ' +
728
+ ' (' +
729
729
  typeof value +
730
730
  ')' +
731
731
  (this.property ? ' for property "' + this.property + '"' : '') +
@@ -127,15 +127,14 @@ class DeserializerFactory {
127
127
  static createBinaryDeserializer () {
128
128
  return (request, next) => {
129
129
  let type = request.getUriInfo() && request.getUriInfo().getFinalEdmType()
130
- if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
130
+ if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
131
131
  if (type && type === EdmPrimitiveTypeKind.Stream) {
132
- next(
133
- null,
134
- request
135
- .getIncomingRequest()
136
- .on('error', next)
137
- .pipe(new stream.PassThrough())
138
- )
132
+ if (request.getIncomingRequest().complete) { // empty or NULL
133
+ next(null, request.getIncomingRequest())
134
+ } else {
135
+ const streamPipeline = stream.pipeline(request.getIncomingRequest(), new stream.PassThrough(), () => {})
136
+ next(null, streamPipeline)
137
+ }
139
138
  } else {
140
139
  request
141
140
  .getIncomingRequest()
@@ -950,6 +950,7 @@ class ResourceJsonSerializer {
950
950
  const [identifier, star, annotation] = this._getPropertyNameAndAnnotation(entityProp)
951
951
 
952
952
  if (identifier && !type.getProperty(identifier)) {
953
+ if (identifier === 'DraftAdministrativeData_DraftUUID') continue
953
954
  throw new SerializationError(
954
955
  "The entity contains data for '" +
955
956
  entityProp +
@@ -1042,6 +1043,8 @@ class ResourceJsonSerializer {
1042
1043
  _serializeNullValue (propertyOrReturnType) {
1043
1044
  const nullable = propertyOrReturnType.isNullable()
1044
1045
  if (nullable === undefined || nullable) return null
1046
+ const name = propertyOrReturnType.getName()
1047
+ if (name === 'IsActiveEntity' || name === 'HasActiveEntity' || name === 'HasDraftEntity') return
1045
1048
  throw new SerializationError(
1046
1049
  'Not nullable value ' +
1047
1050
  (propertyOrReturnType.getName ? "for '" + propertyOrReturnType.getName() + "' " : '') +
@@ -83,10 +83,11 @@ class UriHelper {
83
83
  const property = keyPropertyRef.getProperty()
84
84
  for (const pathElement of keyPropertyRef.getName().split('/')) {
85
85
  value = value[pathElement]
86
- if (value === undefined) {
86
+ if (value === undefined && name !== 'IsActiveEntity') {
87
87
  throw new IllegalArgumentError(`The key '${pathElement}' does not exist in the given entity`)
88
88
  }
89
89
  }
90
+ if (value === undefined && name === 'IsActiveEntity') continue
90
91
  value = primitiveValueEncoder.encodeText(value, property)
91
92
  keys.push({ type: property.getType(), name, value })
92
93
  }
@@ -306,9 +306,10 @@ const _getPathInfo = (query, model) => {
306
306
  } else {
307
307
  returnType = target
308
308
  isCollection = Array.isArray(query) || (query.SELECT && !query.SELECT.one)
309
- propertyName = !isCollection && last.kind !== 'entity' && last.name
309
+ propertyName = query._propertyAccess && query.SELECT.columns[0].ref[query.SELECT.columns[0].ref.length - 1]
310
+ if (propertyName) returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
310
311
  }
311
- const isStream = last['@Core.MediaType']
312
+ const isStream = propertyName && target.elements[propertyName]?.['@Core.MediaType']
312
313
  return {
313
314
  path,
314
315
  target,
@@ -38,6 +38,12 @@ const _getOperationQueryColumns = urlQueryOptions => {
38
38
  const _isDraftAction = req => req.event in { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
39
39
  const _isActionOrFunction = req => !(req.event in CDS_EVENTS) || _isDraftAction(req)
40
40
  const _isWriteWithResponse = req => req.event in WRITE_EVENTS && !(req.event in { CANCEL: 1, DELETE: 1 })
41
+ const _ensureKeysAreSelected = query => {
42
+ if (!query.SELECT.columns || query.SELECT.columns.some(c => c === '*')) return
43
+ for (const key in query._target.keys) {
44
+ if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
45
+ }
46
+ }
41
47
 
42
48
  const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: false }) => {
43
49
  let query
@@ -55,6 +61,7 @@ const readAfterWrite = async (req, srv, { operation, isBefore } = { isBefore: fa
55
61
  query = getDeepSelect(req)
56
62
  }
57
63
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
64
+ _ensureKeysAreSelected(query)
58
65
  // gracefully set location and no body if no read auth or not readable capability
59
66
  let result
60
67
  try {
@@ -108,9 +108,6 @@ const getStreamProperties = (req, model) => {
108
108
  // used cloned path
109
109
  let select = SELECT.one.from({ ref: deepCopyArray(req.query.SELECT.from.ref) }).columns(columns)
110
110
 
111
- // new parser has media property as last ref element -> remove
112
- if (req._metaInfo && req._metaInfo.propertyName) select.SELECT.from.ref.pop()
113
-
114
111
  const pathToDraft = isPathToDraft(select.SELECT.from.ref, model)
115
112
  if (req.target._isDraftEnabled && pathToDraft) {
116
113
  select = cqn2cqn4sql(select, model)
@@ -54,26 +54,14 @@ class ApplicationService extends cds.Service {
54
54
 
55
55
  registerFioriHandlers() {
56
56
  if (cds.env.features.lean_draft) {
57
- const {
58
- onNewDraft,
59
- onDraftPrepare,
60
- onDraftActivate,
61
- onPatch,
62
- onDraftEdit,
63
- onDelete
64
- } = require('../../fiori/lean-draft')
65
- const LOG = cds.log('fiori|drafts')
66
-
67
- for (let each of this.entities)
57
+ const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
58
+
59
+ for (const each of this.entities)
68
60
  if (each.drafts) {
69
- LOG.debug('serving drafts for', { entity: each.name })
70
- this.on('NEW', each, onNewDraft)
71
- this.on('PATCH', each, onPatch)
72
- this.on('EDIT', each, onDraftEdit)
73
- this.on('draftPrepare', each, onDraftPrepare)
74
- this.on('draftActivate', each, onDraftActivate)
75
- this.on('draftActivate', each, onDraftActivate)
76
- this.on(['CANCEL', 'DELETE'], each, onDelete)
61
+ this.on('NEW', each.drafts, onNew)
62
+ this.on('EDIT', each, onEdit)
63
+ this.on('CANCEL', each.drafts, onCancel)
64
+ this.on('draftPrepare', each.drafts, onPrepare)
77
65
  }
78
66
  } else return require('../../fiori/generic').impl.call(this)
79
67
  }
@@ -148,4 +136,5 @@ class ApplicationService extends cds.Service {
148
136
  }
149
137
  }
150
138
 
139
+ ApplicationService.prototype.isAppService = true
151
140
  module.exports = ApplicationService
@@ -4,6 +4,8 @@ const LOG = cds.log()
4
4
 
5
5
  const { DRAFT_COLUMNS_UNION_MAP } = require('../../../common/constants/draft')
6
6
 
7
+ const defaultSearchableType = 'cds.String'
8
+
7
9
  // REVISIT: Can we combine that with db/utils/columns.js?
8
10
  /**
9
11
  * This method gets all columns for an entity.
@@ -43,7 +45,10 @@ const _getSearchableColumns = entity => {
43
45
  const columnsOptions = { removeIgnore: true, filterVirtual: true }
44
46
  const columns = getColumns(entity, columnsOptions)
45
47
  const cdsSearchTerm = '@cds.search'
46
- const cdsSearchKeys = Object.keys(entity).filter(key => key.startsWith(cdsSearchTerm))
48
+ const cdsSearchKeys = []
49
+ for (const key in entity) {
50
+ if (key.startsWith(cdsSearchTerm)) cdsSearchKeys.push(key)
51
+ }
47
52
  const cdsSearchColumnMap = new Map()
48
53
  let atLeastOneColumnIsSearchable = false
49
54
 
@@ -63,9 +68,6 @@ const _getSearchableColumns = entity => {
63
68
  cdsSearchColumnMap.set(columnName, annotationValue)
64
69
  }
65
70
 
66
- // For performance reasons, by default, only elements typed as strings are searchable unless
67
- // the @cds.search annotation is specified.
68
- const defaultSearchableType = 'cds.String'
69
71
  const searchableColumns = columns.filter(column => {
70
72
  const annotatedColumnValue = cdsSearchColumnMap.get(column.name)
71
73
 
@@ -144,6 +146,7 @@ const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, a
144
146
 
145
147
  const columnRef = column.ref
146
148
  if (columnRef) {
149
+ if (entity.elements[columnRef[columnRef.length - 1]]?._type !== defaultSearchableType) return
147
150
  column = { ref: [...column.ref] }
148
151
  if (alias) column.ref.unshift(alias)
149
152
  toBeSearched.push(column)
@@ -25,7 +25,6 @@ const ASSERT_FORMAT = 'ASSERT_FORMAT'
25
25
  const ASSERT_DATA_TYPE = 'ASSERT_DATA_TYPE'
26
26
  const ASSERT_ENUM = 'ASSERT_ENUM'
27
27
  const ASSERT_NOT_NULL = 'ASSERT_NOT_NULL'
28
- const ASSERT_DEEP_ASSOCIATION = 'ASSERT_DEEP_ASSOCIATION'
29
28
 
30
29
  const _enumValues = element => {
31
30
  return Object.keys(element).map(enumKey => {
@@ -221,11 +220,18 @@ const _isNotFilled = value => {
221
220
  }
222
221
 
223
222
  const _checkMandatoryElement = (element, value, errors, key, pathSegments) => {
223
+ if (element.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, element.name))) return
224
224
  if (element._isMandatory && !element.default && _isNotFilled(value)) {
225
225
  errors.push(assertError(ASSERT_NOT_NULL, element, value, key, pathSegments))
226
226
  }
227
227
  }
228
228
 
229
+ const _isNavigationColumn = (column, searched) => {
230
+ return (
231
+ column.ref && column.ref.length > 1 && (column.as === searched || column.ref[column.ref.length - 1] === searched)
232
+ )
233
+ }
234
+
229
235
  const _getEnumElement = element => {
230
236
  return (element['@assert.range'] && element.enum) || element['@assert.enum'] ? element.enum : undefined
231
237
  }
@@ -2,7 +2,8 @@ const { parentPort } = require('worker_threads')
2
2
  const { Responses, Errors } = require('../../../../lib/req/response')
3
3
 
4
4
  class WorkerReq {
5
- constructor(reqData) {
5
+ constructor(contextId, reqData) {
6
+ this.contextId = contextId
6
7
  Object.assign(this, reqData)
7
8
  this.postMessages = []
8
9
  this.messages = this.messages ?? []
@@ -64,6 +65,7 @@ class WorkerReq {
64
65
 
65
66
  reject(...args) {
66
67
  parentPort.postMessage({
68
+ contextId: this.contextId,
67
69
  kind: 'run',
68
70
  target: 'req',
69
71
  prop: 'reject',
@@ -4,6 +4,7 @@ const path = require('node:path')
4
4
  const { timeout, resourceLimits } = require('./config')
5
5
  const workerPath = path.resolve(__dirname, 'worker.js')
6
6
  const { Errors } = require('../../../../lib/req/response')
7
+ const LOG = cds.log()
7
8
 
8
9
  const _getReqData = req => {
9
10
  return {
@@ -17,10 +18,11 @@ const _getReqData = req => {
17
18
 
18
19
  module.exports = async function executeCode(code, req) {
19
20
  const reqData = _getReqData(req)
21
+ const srv = this
20
22
  const _getTarget = target => {
21
23
  switch (target) {
22
24
  case 'srv':
23
- return this
25
+ return srv
24
26
 
25
27
  case 'req':
26
28
  return req
@@ -29,6 +31,7 @@ module.exports = async function executeCode(code, req) {
29
31
  }
30
32
  }
31
33
  const workerId = cds.utils.uuid()
34
+ const contextId = cds.utils.uuid()
32
35
  const worker = new Worker(workerPath, {
33
36
  workerData: { id: workerId },
34
37
  resourceLimits
@@ -50,6 +53,8 @@ module.exports = async function executeCode(code, req) {
50
53
  }
51
54
 
52
55
  function onMessageReceived(message) {
56
+ if (LOG._debug)
57
+ LOG.debug(`Post message received on main thread (code-ext/execute.js) from worker thread`, message)
53
58
  switch (message.kind) {
54
59
  case 'run':
55
60
  run(message)
@@ -89,6 +94,7 @@ module.exports = async function executeCode(code, req) {
89
94
  if (typeof result?.then === 'function') result = await result
90
95
  if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
91
96
  } catch (error) {
97
+ if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
92
98
  worker.postMessage({ id: message.id, kind: 'cleanup' })
93
99
  reject(error)
94
100
  }
@@ -101,6 +107,7 @@ module.exports = async function executeCode(code, req) {
101
107
  })
102
108
 
103
109
  // triggers execution of the code in the worker thread
104
- worker.postMessage({ id: workerId, code, reqData })
110
+ const message = { contextId, workerId, kind: 'start', code, reqData }
111
+ worker.postMessage(message)
105
112
  return executePromise
106
113
  }
@@ -35,14 +35,14 @@ module.exports = cds.service.impl(function () {
35
35
  let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
36
36
  if (!fqn) {
37
37
  if (req.target) return next()
38
- fqn = this.model.definitions[`${this.name}.${req.event}`] // check for unbound action/function or event
38
+ fqn = this.model.definitions[`${this.name}.${req.event}`] // check for bound action/function or event
39
39
  }
40
40
 
41
41
  // REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
42
42
  // REVISIT: check whether action/function or event is part of an extension
43
43
  if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
44
44
  const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
45
- if (!code) return
45
+ if (!code) return next()
46
46
  return await executeCode.call(this, code, req)
47
47
  }
48
48
  })
@@ -1,14 +1,18 @@
1
+ const cds = require('../../cds')
2
+ const LOG = cds.log()
1
3
  const { parentPort, workerData } = require('worker_threads')
2
4
  const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
3
5
  const WorkerReq = require('./WorkerReq')
4
6
  const { timeout } = require('./config')
5
7
 
6
- parentPort.once('message', function onMessageReceived({ id, code, reqData }) {
7
- if (id !== workerData.id) return
8
+ parentPort.once('message', function onWorkerMessageReceived(message) {
9
+ const { contextId, workerId, kind, code, reqData } = message
10
+ if (LOG._debug) LOG.debug(`Post message received on worker thread (worker.js) from main thread`, message)
11
+ if (kind !== 'start' || workerId !== workerData.id) return
8
12
 
9
13
  // eslint-disable-next-line cds/no-missing-dependencies
10
14
  const { VM } = require('vm2')
11
- const workerReq = new WorkerReq(reqData)
15
+ const workerReq = new WorkerReq(contextId, reqData)
12
16
  const vm = new VM({
13
17
  console: 'inherit',
14
18
  timeout, // specifies the number of milliseconds to execute code before terminating execution
@@ -28,9 +32,9 @@ parentPort.once('message', function onMessageReceived({ id, code, reqData }) {
28
32
  try {
29
33
  ;(async function () {
30
34
  const result = await vm.run(code)
31
- parentPort.postMessage({ kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
35
+ parentPort.postMessage({ contextId, kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
32
36
  })()
33
37
  } catch (error) {
34
- parentPort.postMessage({ kind: 'error', error })
38
+ parentPort.postMessage({ contextId, kind: 'error', error })
35
39
  }
36
40
  })