@sap/cds 6.4.1 → 6.6.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 (139) hide show
  1. package/CHANGELOG.md +79 -6
  2. package/README.md +5 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +8 -8
  5. package/apis/services.d.ts +37 -65
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -14
  8. package/bin/build/buildTaskFactory.js +1 -1
  9. package/bin/build/buildTaskHandler.js +3 -14
  10. package/bin/build/index.js +8 -2
  11. package/bin/build/provider/buildTaskProviderInternal.js +18 -13
  12. package/bin/build/provider/fiori/index.js +5 -10
  13. package/bin/build/provider/hana/2migration.js +11 -2
  14. package/bin/build/provider/hana/index.js +17 -14
  15. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  16. package/bin/build/provider/hana/template/package.json +3 -0
  17. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  18. package/bin/build/provider/mtx-extension/index.js +57 -37
  19. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  20. package/bin/build/util.js +18 -1
  21. package/bin/cds.js +1 -5
  22. package/bin/deploy/to-hana/hana.js +10 -3
  23. package/bin/serve.js +36 -20
  24. package/common.cds +7 -0
  25. package/lib/auth/jwt-auth.js +8 -7
  26. package/lib/compile/for/lean_drafts.js +55 -6
  27. package/lib/compile/minify.js +3 -3
  28. package/lib/dbs/cds-deploy.js +18 -17
  29. package/lib/env/cds-requires.js +1 -1
  30. package/lib/env/defaults.js +5 -1
  31. package/lib/env/schemas/cds-rc.json +74 -3
  32. package/lib/index.js +4 -2
  33. package/lib/lazy.js +6 -8
  34. package/lib/log/cds-error.js +2 -2
  35. package/lib/ql/Whereable.js +22 -11
  36. package/lib/ql/cds-ql.js +1 -1
  37. package/lib/req/cds-context.js +3 -3
  38. package/lib/req/response.js +8 -3
  39. package/lib/req/user.js +12 -2
  40. package/lib/srv/bindings.js +1 -2
  41. package/lib/srv/cds-serve.js +2 -1
  42. package/lib/srv/middlewares/trace.js +31 -15
  43. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  44. package/lib/srv/srv-handlers.js +26 -7
  45. package/lib/srv/srv-methods.js +2 -2
  46. package/lib/srv/srv-models.js +8 -3
  47. package/lib/utils/cds-test.js +7 -5
  48. package/lib/utils/cds-utils.js +3 -1
  49. package/lib/utils/tar.js +6 -3
  50. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  51. package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
  52. package/libx/_runtime/auth/strategies/mock.js +12 -1
  53. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  54. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  62. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  70. package/libx/_runtime/cds-services/services/Service.js +11 -19
  71. package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
  72. package/libx/_runtime/cds-services/util/assert.js +7 -1
  73. package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
  74. package/libx/_runtime/common/code-ext/config.js +13 -0
  75. package/libx/_runtime/common/code-ext/execute.js +113 -0
  76. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  77. package/libx/_runtime/common/code-ext/worker.js +40 -0
  78. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  79. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
  80. package/libx/_runtime/common/composition/data.js +5 -2
  81. package/libx/_runtime/common/composition/tree.js +2 -0
  82. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  83. package/libx/_runtime/common/generic/crud.js +4 -0
  84. package/libx/_runtime/common/generic/etag.js +3 -1
  85. package/libx/_runtime/common/generic/input.js +12 -14
  86. package/libx/_runtime/common/i18n/index.js +1 -1
  87. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
  88. package/libx/_runtime/common/utils/path.js +5 -26
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
  90. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  91. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
  93. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  94. package/libx/_runtime/db/generic/input.js +2 -2
  95. package/libx/_runtime/db/generic/integrity.js +1 -0
  96. package/libx/_runtime/db/generic/virtual.js +1 -0
  97. package/libx/_runtime/db/query/read.js +3 -2
  98. package/libx/_runtime/db/utils/localized.js +1 -1
  99. package/libx/_runtime/fiori/generic/activate.js +7 -1
  100. package/libx/_runtime/fiori/generic/before.js +9 -1
  101. package/libx/_runtime/fiori/generic/edit.js +8 -1
  102. package/libx/_runtime/fiori/generic/new.js +2 -0
  103. package/libx/_runtime/fiori/generic/patch.js +2 -0
  104. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  105. package/libx/_runtime/fiori/generic/read.js +16 -5
  106. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  107. package/libx/_runtime/fiori/lean-draft.js +505 -241
  108. package/libx/_runtime/fiori/utils/delete.js +2 -0
  109. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  110. package/libx/_runtime/hana/pool.js +1 -1
  111. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  112. package/libx/_runtime/messaging/Outbox.js +1 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  114. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  115. package/libx/_runtime/messaging/file-based.js +1 -2
  116. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  117. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  118. package/libx/_runtime/messaging/service.js +0 -1
  119. package/libx/_runtime/remote/Service.js +1 -0
  120. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  121. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  122. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  123. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  124. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  125. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  126. package/libx/odata/afterburner.js +23 -8
  127. package/libx/odata/cqn2odata.js +1 -1
  128. package/libx/odata/grammar.pegjs +3 -4
  129. package/libx/odata/index.js +5 -1
  130. package/libx/odata/parseToCqn.js +3 -3
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +58 -1
  133. package/libx/rest/middleware/parse.js +26 -4
  134. package/package.json +1 -1
  135. package/server.js +1 -1
  136. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  137. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  138. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  139. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -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 + '"' : '') +
@@ -135,6 +135,7 @@ class DeserializerFactory {
135
135
  .getIncomingRequest()
136
136
  .on('error', next)
137
137
  .pipe(new stream.PassThrough())
138
+ .on('error', next)
138
139
  )
139
140
  } else {
140
141
  request
@@ -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)
@@ -22,6 +22,9 @@ class ApplicationService extends cds.Service {
22
22
  require('../../common/generic/temporal').call(this, this)
23
23
  require('../../common/generic/paging').call(this, this) // > paging must be executed before sorting
24
24
  require('../../common/generic/sorting').call(this, this)
25
+
26
+ if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this, this)
27
+
25
28
  this.registerFioriHandlers(this)
26
29
  this.registerPersonalDataHandlers(this)
27
30
  this.registerCrudHandlers(this) // default .on handlers, have to go last
@@ -51,26 +54,14 @@ class ApplicationService extends cds.Service {
51
54
 
52
55
  registerFioriHandlers() {
53
56
  if (cds.env.features.lean_draft) {
54
- const {
55
- onNewDraft,
56
- onDraftPrepare,
57
- onDraftActivate,
58
- onPatch,
59
- onDraftEdit,
60
- onDelete
61
- } = require('../../fiori/lean-draft')
62
- const LOG = cds.log('fiori|drafts')
63
-
64
- for (let each of this.entities)
57
+ const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
58
+
59
+ for (const each of this.entities)
65
60
  if (each.drafts) {
66
- LOG.debug('serving drafts for', { entity: each.name })
67
- this.on('NEW', each, onNewDraft)
68
- this.on('PATCH', each, onPatch)
69
- this.on('EDIT', each, onDraftEdit)
70
- this.on('draftPrepare', each, onDraftPrepare)
71
- this.on('draftActivate', each, onDraftActivate)
72
- this.on('draftActivate', each, onDraftActivate)
73
- 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)
74
65
  }
75
66
  } else return require('../../fiori/generic').impl.call(this)
76
67
  }
@@ -145,4 +136,5 @@ class ApplicationService extends cds.Service {
145
136
  }
146
137
  }
147
138
 
139
+ ApplicationService.prototype.isAppService = true
148
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
 
@@ -122,44 +124,44 @@ const _getSearchableColumns = entity => {
122
124
  * @returns {import('../../../types/api').ColumnRefs}
123
125
  */
124
126
  const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
125
- // if there is a group by clause, only columns in it may be searched
126
- let toBeSearched =
127
- entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
128
-
129
- if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
130
- toBeSearched = toBeSearched.map(c => {
131
- const col = { ref: [c] }
132
- if (alias) col.ref.unshift(alias)
133
- return col
134
- })
135
-
136
- // add aggregations
137
- cqn.SELECT.columns &&
138
- cqn.SELECT.columns.forEach(column => {
139
- if (column.func) {
140
- // exclude $count by SELECT of number of Items in a Collection
141
- if (
142
- cqn.SELECT.columns.length === 1 &&
143
- column.func === 'count' &&
144
- (column.as === '_counted_' || column.as === '$count')
145
- )
127
+ let toBeSearched = []
128
+
129
+ // aggregations case
130
+ // in the new parser groupBy is moved to sub select.
131
+ if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
132
+ cqn.SELECT.columns &&
133
+ cqn.SELECT.columns.forEach(column => {
134
+ if (column.func) {
135
+ // exclude $count by SELECT of number of Items in a Collection
136
+ if (
137
+ cqn.SELECT.columns.length === 1 &&
138
+ column.func === 'count' &&
139
+ (column.as === '_counted_' || column.as === '$count')
140
+ )
141
+ return
142
+
143
+ toBeSearched.push(column)
146
144
  return
147
-
148
- toBeSearched.push(column)
149
- return
150
- }
151
-
152
- const columnRef = column.ref
153
- if (columnRef) {
154
- const columnName = columnRef[columnRef.length - 1]
155
- const csnColumn = entity.elements[columnName]
156
- if (csnColumn) return
157
- const col = { ref: [columnName] }
158
- if (alias) col.ref.unshift(alias)
159
- toBeSearched.push(col)
160
- }
145
+ }
146
+
147
+ const columnRef = column.ref
148
+ if (columnRef) {
149
+ if (entity.elements[columnRef[columnRef.length - 1]]?._type !== defaultSearchableType) return
150
+ column = { ref: [...column.ref] }
151
+ if (alias) column.ref.unshift(alias)
152
+ toBeSearched.push(column)
153
+ }
154
+ })
155
+ } else {
156
+ toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
157
+
158
+ if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
159
+ toBeSearched = toBeSearched.map(c => {
160
+ const col = { ref: [c] }
161
+ if (alias) col.ref.unshift(alias)
162
+ return col
161
163
  })
162
-
164
+ }
163
165
  return toBeSearched
164
166
  }
165
167
 
@@ -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
  }
@@ -0,0 +1,81 @@
1
+ const { parentPort } = require('worker_threads')
2
+ const { Responses, Errors } = require('../../../../lib/req/response')
3
+
4
+ class WorkerReq {
5
+ constructor(contextId, reqData) {
6
+ this.contextId = contextId
7
+ Object.assign(this, reqData)
8
+ this.postMessages = []
9
+ this.messages = this.messages ?? []
10
+ this.errors = this.errors ?? new Errors()
11
+ }
12
+
13
+ #push(args) {
14
+ this.postMessages.push({
15
+ kind: 'run',
16
+ target: 'req',
17
+ ...args
18
+ })
19
+ }
20
+
21
+ notify(...args) {
22
+ this.#push({
23
+ prop: 'notify',
24
+ args
25
+ })
26
+
27
+ const notify = Responses.get(1, ...args)
28
+ this.messages.push(notify)
29
+ return notify
30
+ }
31
+
32
+ info(...args) {
33
+ this.#push({
34
+ prop: 'info',
35
+ args
36
+ })
37
+
38
+ const info = Responses.get(2, ...args)
39
+ this.messages.push(info)
40
+ return info
41
+ }
42
+
43
+ warn(...args) {
44
+ this.#push({
45
+ prop: 'warn',
46
+ args
47
+ })
48
+
49
+ const warn = Responses.get(3, ...args)
50
+ this.messages.push(warn)
51
+ return warn
52
+ }
53
+
54
+ error(...args) {
55
+ this.#push({
56
+ prop: 'error',
57
+ args
58
+ })
59
+
60
+ let error = Responses.get(4, ...args)
61
+ if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.error)
62
+ this.errors.push(error)
63
+ return error
64
+ }
65
+
66
+ reject(...args) {
67
+ parentPort.postMessage({
68
+ contextId: this.contextId,
69
+ kind: 'run',
70
+ target: 'req',
71
+ prop: 'reject',
72
+ args
73
+ })
74
+
75
+ let error = Responses.get(4, ...args)
76
+ if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.reject)
77
+ throw error
78
+ }
79
+ }
80
+
81
+ module.exports = WorkerReq
@@ -0,0 +1,13 @@
1
+ const os = require('os')
2
+ const totalMemory = os.totalmem() // total amount of system memory in bytes
3
+ const maxOldGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of the main heap in MB
4
+ const maxYoungGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of a heap space for recently created objects
5
+
6
+ module.exports = {
7
+ timeout: 10000,
8
+ resourceLimits: {
9
+ maxOldGenerationSizeMb,
10
+ maxYoungGenerationSizeMb,
11
+ stackSizeMb: 4 // default
12
+ }
13
+ }
@@ -0,0 +1,113 @@
1
+ const cds = require('../../cds')
2
+ const { Worker } = require('worker_threads')
3
+ const path = require('node:path')
4
+ const { timeout, resourceLimits } = require('./config')
5
+ const workerPath = path.resolve(__dirname, 'worker.js')
6
+ const { Errors } = require('../../../../lib/req/response')
7
+ const LOG = cds.log()
8
+
9
+ const _getReqData = req => {
10
+ return {
11
+ data: req.data,
12
+ params: req.params,
13
+ results: req.results,
14
+ messages: req.messages,
15
+ errors: req.errors ?? new Errors()
16
+ }
17
+ }
18
+
19
+ module.exports = async function executeCode(code, req) {
20
+ const reqData = _getReqData(req)
21
+ const srv = this
22
+ const _getTarget = target => {
23
+ switch (target) {
24
+ case 'srv':
25
+ return srv
26
+
27
+ case 'req':
28
+ return req
29
+
30
+ // no default
31
+ }
32
+ }
33
+ const workerId = cds.utils.uuid()
34
+ const contextId = cds.utils.uuid()
35
+ const worker = new Worker(workerPath, {
36
+ workerData: { id: workerId },
37
+ resourceLimits
38
+ })
39
+
40
+ const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
41
+ worker.on('online', onStarted)
42
+ worker.on('message', onMessageReceived)
43
+ worker.on('error', onError)
44
+ worker.on('exit', onExit)
45
+
46
+ let onStartTimeoutID
47
+
48
+ function onStarted() {
49
+ onStartTimeoutID = setTimeout(() => {
50
+ worker.terminate()
51
+ reject(new Error(`Script execution timed out after ${timeout}ms`))
52
+ }, timeout)
53
+ }
54
+
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)
58
+ switch (message.kind) {
59
+ case 'run':
60
+ run(message)
61
+ return
62
+
63
+ case 'success':
64
+ onSuccess(message)
65
+ cleanup()
66
+ return
67
+
68
+ case 'error':
69
+ onError(message.error)
70
+ return
71
+
72
+ // no default
73
+ }
74
+ }
75
+
76
+ async function onSuccess(message) {
77
+ for (const m of message.postMessages) await run(m)
78
+ req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
79
+ req.results = message.req.results
80
+ resolve(req.results ?? message.result)
81
+ }
82
+
83
+ function onError(error) {
84
+ reject(error)
85
+ }
86
+
87
+ function onExit(exitCode) {
88
+ if (exitCode !== 0) reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
89
+ }
90
+
91
+ async function run(message) {
92
+ try {
93
+ let result = _getTarget(message.target)[message.prop](...message.args)
94
+ if (typeof result?.then === 'function') result = await result
95
+ if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
96
+ } catch (error) {
97
+ if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
98
+ worker.postMessage({ id: message.id, kind: 'cleanup' })
99
+ reject(error)
100
+ }
101
+ }
102
+
103
+ function cleanup() {
104
+ clearTimeout(onStartTimeoutID)
105
+ worker.terminate()
106
+ }
107
+ })
108
+
109
+ // triggers execution of the code in the worker thread
110
+ const message = { contextId, workerId, kind: 'start', code, reqData }
111
+ worker.postMessage(message)
112
+ return executePromise
113
+ }
@@ -0,0 +1,49 @@
1
+ const cds = require('../../cds')
2
+ const executeCode = require('./execute')
3
+
4
+ const CODE_ANNOTATION = '@extension.code'
5
+
6
+ module.exports = cds.service.impl(function () {
7
+ const getCodeFromAnnotation = async (defName, operation, registration) => {
8
+ // REVISIT: tenant info in not in this.model and cds.context.model is undefined for single tenancy
9
+ const model = cds.context.model || this.model
10
+ const el = model.definitions[defName]
11
+ const boundEl = el.actions?.[operation]
12
+ const extensionCode = boundEl?.[CODE_ANNOTATION] ?? el[CODE_ANNOTATION]
13
+ if (extensionCode) {
14
+ const annotation = extensionCode.filter(element => element[registration] === operation)
15
+ return annotation.length && annotation[0].code
16
+ }
17
+ }
18
+
19
+ this.after('READ', async function (result, req) {
20
+ if (result == null) return // whether result is null or undefined
21
+ const code = await getCodeFromAnnotation(req.target.name, req.event, 'after')
22
+ if (!code) return
23
+ await executeCode.call(this, code, req)
24
+ })
25
+
26
+ this.before(['CREATE', 'UPDATE', 'DELETE'], async function (req) {
27
+ const code = await getCodeFromAnnotation(req.target.name, req.event, 'before')
28
+ if (!code) return
29
+ await executeCode.call(this, code, req)
30
+ })
31
+
32
+ this.on('*', async function (req, next) {
33
+ if (this.name.startsWith('cds.xt')) return next()
34
+ // REVISIT: req.target -> wait until implementation task finished
35
+ let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
36
+ if (!fqn) {
37
+ if (req.target) return next()
38
+ fqn = this.model.definitions[`${this.name}.${req.event}`] // check for bound action/function or event
39
+ }
40
+
41
+ // REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
42
+ // REVISIT: check whether action/function or event is part of an extension
43
+ if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
44
+ const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
45
+ if (!code) return next()
46
+ return await executeCode.call(this, code, req)
47
+ }
48
+ })
49
+ })