@sap/cds 6.0.4 → 6.1.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 (120) hide show
  1. package/CHANGELOG.md +128 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +48 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/bin/build/buildTaskHandler.js +5 -2
  6. package/bin/build/constants.js +4 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  8. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
  9. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  10. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  11. package/bin/build/provider/hana/index.js +8 -7
  12. package/bin/build/provider/java/index.js +18 -8
  13. package/bin/build/provider/mtx/index.js +7 -4
  14. package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
  15. package/bin/build/provider/mtx-extension/index.js +57 -0
  16. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  17. package/bin/build/provider/nodejs/index.js +34 -13
  18. package/bin/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/serve.js +7 -4
  20. package/lib/compile/{index.js → cds-compile.js} +0 -0
  21. package/lib/compile/extend.js +15 -5
  22. package/lib/compile/minify.js +1 -15
  23. package/lib/compile/parse.js +1 -1
  24. package/lib/compile/resolve.js +2 -2
  25. package/lib/compile/to/srvinfo.js +6 -4
  26. package/lib/{deploy.js → dbs/cds-deploy.js} +7 -6
  27. package/lib/env/{index.js → cds-env.js} +1 -17
  28. package/lib/env/{requires.js → cds-requires.js} +24 -3
  29. package/lib/env/defaults.js +7 -1
  30. package/lib/env/schemas/cds-package.json +11 -0
  31. package/lib/env/schemas/cds-rc.json +605 -0
  32. package/lib/index.js +19 -16
  33. package/lib/log/{errors.js → cds-error.js} +1 -1
  34. package/lib/log/{index.js → cds-log.js} +0 -0
  35. package/lib/ql/SELECT.js +1 -1
  36. package/lib/ql/{index.js → cds-ql.js} +0 -0
  37. package/lib/req/context.js +35 -7
  38. package/lib/req/locale.js +5 -1
  39. package/lib/{serve → srv}/adapters.js +23 -19
  40. package/lib/{connect → srv}/bindings.js +0 -0
  41. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  42. package/lib/{serve/index.js → srv/cds-serve.js} +0 -0
  43. package/lib/{serve → srv}/factory.js +1 -1
  44. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  45. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  46. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  47. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  48. package/lib/srv/srv-models.js +206 -0
  49. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  50. package/lib/utils/{tests.js → cds-test.js} +2 -2
  51. package/lib/utils/cds-utils.js +146 -0
  52. package/lib/utils/index.js +2 -145
  53. package/lib/utils/jest.js +43 -0
  54. package/lib/utils/resources/index.js +14 -24
  55. package/lib/utils/resources/tar.js +18 -41
  56. package/libx/_runtime/auth/index.js +13 -10
  57. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  58. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  59. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  69. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  70. package/libx/_runtime/cds-services/util/errors.js +1 -29
  71. package/libx/_runtime/common/i18n/messages.properties +2 -1
  72. package/libx/_runtime/common/perf/index.js +10 -15
  73. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  74. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  75. package/libx/_runtime/common/utils/template.js +1 -1
  76. package/libx/_runtime/db/Service.js +2 -14
  77. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  78. package/libx/_runtime/db/generic/input.js +4 -0
  79. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  80. package/libx/_runtime/extensibility/activate.js +47 -47
  81. package/libx/_runtime/extensibility/add.js +19 -13
  82. package/libx/_runtime/extensibility/addExtension.js +17 -13
  83. package/libx/_runtime/extensibility/defaults.js +25 -30
  84. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  85. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  86. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  87. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  88. package/libx/_runtime/extensibility/linter.js +32 -0
  89. package/libx/_runtime/extensibility/push.js +78 -21
  90. package/libx/_runtime/extensibility/service.js +29 -12
  91. package/libx/_runtime/extensibility/token.js +56 -0
  92. package/libx/_runtime/extensibility/validation.js +6 -9
  93. package/libx/_runtime/fiori/generic/new.js +0 -11
  94. package/libx/_runtime/hana/Service.js +0 -1
  95. package/libx/_runtime/hana/conversion.js +12 -1
  96. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  97. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  98. package/libx/_runtime/hana/pool.js +6 -10
  99. package/libx/_runtime/hana/search2Contains.js +0 -5
  100. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  101. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  102. package/libx/_runtime/messaging/service.js +11 -6
  103. package/libx/_runtime/remote/utils/data.js +5 -0
  104. package/libx/_runtime/sqlite/Service.js +0 -1
  105. package/libx/odata/afterburner.js +79 -2
  106. package/libx/odata/cqn2odata.js +9 -7
  107. package/libx/odata/grammar.pegjs +157 -76
  108. package/libx/odata/index.js +9 -3
  109. package/libx/odata/parser.js +1 -1
  110. package/libx/odata/utils.js +39 -5
  111. package/libx/rest/RestAdapter.js +1 -2
  112. package/libx/rest/middleware/delete.js +4 -5
  113. package/libx/rest/middleware/parse.js +3 -2
  114. package/package.json +3 -3
  115. package/server.js +1 -1
  116. package/srv/extensibility-service.cds +6 -3
  117. package/srv/model-provider.cds +3 -1
  118. package/srv/model-provider.js +84 -104
  119. package/srv/mtx.js +7 -1
  120. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -114,13 +114,13 @@ const getErrorHandler = (crashOnError = true, srv) => {
114
114
 
115
115
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
116
116
  // REVISIT: if for compat reasons, remove once cds^5.1
117
- if (srv._handlers._error) {
117
+ if (srv._handlers._error.length) {
118
118
  let ctx = cds.context
119
119
  if (!ctx) {
120
120
  // > error before req was dispatched
121
121
  ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
122
122
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
123
- } else if (err.getRootCause) {
123
+ } else {
124
124
  // > error after req was dispatched, e.g., serialization error in okra
125
125
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
126
126
  }
@@ -5,10 +5,6 @@ const { toODataResult } = require('../utils/result')
5
5
  const { normalizeError } = require('../../../../common/error/frontend')
6
6
  const getError = require('../../../../common/error')
7
7
 
8
- const _getMetadata4Tenant = async (tenant, locale, service) => {
9
- return await cds.mtx.getEdmx(tenant, service.name, locale)
10
- }
11
-
12
8
  /**
13
9
  * Provide localized metadata handler.
14
10
  *
@@ -23,21 +19,12 @@ const metadata = service => {
23
19
  const locale = odataRes.getContract().getLocale()
24
20
 
25
21
  try {
26
- let edmx
27
-
28
- if (cds.mtx && service._isExtended) {
29
- edmx = await _getMetadata4Tenant(tenant, locale, service)
30
- }
31
-
32
- if (!edmx) {
33
- edmx = cds.localize(
34
- service.model,
35
- locale,
36
- // REVISIT: we could cache this in a weak map
37
- cds.compile.to.edmx(service.model, { service: service.definition.name })
38
- )
39
- }
40
-
22
+ let edmx = cds.localize(
23
+ service.model,
24
+ locale,
25
+ // REVISIT: we could cache this in model._cached
26
+ cds.compile.to.edmx(service.model, { service: service.definition.name })
27
+ )
41
28
  return next(null, toODataResult(edmx))
42
29
  } catch (e) {
43
30
  if (LOG._error) {
@@ -484,10 +484,7 @@ const read = service => {
484
484
  } catch (e) {
485
485
  err = e
486
486
 
487
- if (changeset) {
488
- // for passing into rollback
489
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
490
- } else {
487
+ if (!changeset) {
491
488
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
492
489
  await tx.rollback(e).catch(() => {})
493
490
  }
@@ -6,7 +6,7 @@ module.exports = srv => {
6
6
  const requires = getRequiresAsArray(srv.definition)
7
7
  const restricted = isRestricted(srv)
8
8
 
9
- return (odataReq, odataRes, next) => {
9
+ return function ODataRequestHandler(odataReq, odataRes, next) {
10
10
  const req = odataReq.getBatchApplicationData()
11
11
  ? odataReq.getBatchApplicationData().req
12
12
  : odataReq.getIncomingRequest()
@@ -174,10 +174,7 @@ const update = service => {
174
174
  } catch (e) {
175
175
  err = e
176
176
 
177
- if (changeset) {
178
- // for passing into rollback
179
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
180
- } else {
177
+ if (!changeset) {
181
178
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
182
179
  await tx.rollback(e).catch(() => {})
183
180
  }
@@ -1,7 +1,41 @@
1
- const Dispatcher = require('./Dispatcher')
1
+ const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
2
+ const cds = require('../../../cds')
3
+ const OData = require('./OData')
2
4
 
3
- const to = service => {
4
- return new Dispatcher(service).getService()
5
+ /**
6
+ * This is the express handler for a specific OData endpoint.
7
+ * Note: the same service can be served at different endpoints.
8
+ */
9
+ module.exports = srv => {
10
+ const okra = new OkraAdapter(srv)
11
+ return okra.process.bind(okra)
5
12
  }
6
13
 
7
- module.exports = to
14
+ function OkraAdapter(srv, model = srv.model) {
15
+ const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
16
+ alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
17
+ return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
18
+ }
19
+
20
+ //////////////////////////////////////////////////////////////////////////////
21
+ //
22
+ // REVISIT: Move to ExtensibilityService
23
+ //
24
+ if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
25
+ module.exports = srv => {
26
+ const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
27
+ return function ODataAdapter(req, res) {
28
+ const model = cds.context?.model || srv.model
29
+ if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
30
+
31
+ // Note: cache is attached to model cache so they get disposed when models are evicted from cache
32
+ let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
33
+ let okra = adapters[id]
34
+ if (!okra) {
35
+ const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
36
+ okra = adapters[id] = new OkraAdapter(_srv, model)
37
+ }
38
+ return okra.process(req, res)
39
+ }
40
+ }
41
+ let unique = 0
@@ -22,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
22
22
  const _isNotFoundError = e => Number(e.code) === 404
23
23
  const _isEntityNotReadableError = e => Number(e.code) === 405
24
24
 
25
- const _handleReadError = (err, req) => {
25
+ const _handleReadError = err => {
26
26
  if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
27
- const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
28
- process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
29
27
  }
30
28
 
31
29
  const _getOperationQueryColumns = urlQueryOptions => {
@@ -18,6 +18,10 @@ module.exports = class Differ {
18
18
  _createSelectColumnsForDelete(entity) {
19
19
  const columns = []
20
20
  for (const element of Object.values(entity.elements)) {
21
+ // Don't take into account virtual or computed properties to make the diff result
22
+ // consistent with the ones for UPDATE/CREATE (where we don't have access to that
23
+ // information).
24
+ if (!element.key && (element.virtual || element['@Core.Computed'])) continue
21
25
  if (element.isComposition) {
22
26
  if (element._target._hasPersistenceSkip) continue
23
27
  columns.push({
@@ -9,35 +9,7 @@ const getFeatureNotSupportedError = message => {
9
9
  return getError(501, `Feature is not supported: ${message}`)
10
10
  }
11
11
 
12
- const getAuditLogNotWrittenError = (rootCauseError, phase, event) => {
13
- const errorMessage =
14
- !phase || event === 'READ' ? 'Audit log could not be written' : `Audit log could not be written ${phase}`
15
- const error = new Error(errorMessage)
16
- error.rootCause = rootCauseError
17
- return error
18
- }
19
-
20
- const hasBeenCalledError = (method, query) => {
21
- return new Error(`Method ${method} has been called before. Invalid CQN: ${JSON.stringify(query)}`)
22
- }
23
-
24
- const unexpectedFunctionCallError = (functionName, expectedFunction) => {
25
- return new Error(`Cannot build CQN object. Invalid call of "${functionName}" before "${expectedFunction}"`)
26
- }
27
-
28
- const invalidFunctionArgumentError = (statement, arg) => {
29
- const details = JSON.stringify(arg, (key, value) => (value === undefined ? '__undefined__' : value)).replace(
30
- /"__undefined__"/g,
31
- 'undefined'
32
- )
33
- return new Error(`Cannot build ${statement} statement. Invalid data provided: ${details}`)
34
- }
35
-
36
12
  module.exports = {
37
13
  getModelNotDefinedError,
38
- getFeatureNotSupportedError,
39
- getAuditLogNotWrittenError,
40
- hasBeenCalledError,
41
- unexpectedFunctionCallError,
42
- invalidFunctionArgumentError
14
+ getFeatureNotSupportedError
43
15
  }
@@ -74,8 +74,9 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
74
74
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
75
75
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
76
76
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
77
- EXPAND_COUNT_UNSUPPORTED="$count" is not supported for expand operation
77
+ EXPAND_COUNT_UNSUPPORTED="/$count" is not supported for expand operation
78
78
  ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
79
+ EXPAND_APPLY_UNSUPPORTED="$apply" is not supported for expand operation
79
80
 
80
81
  # rest protocol adapter
81
82
  INVALID_RESOURCE="{0}" is not a valid resource
@@ -4,21 +4,16 @@ const _statisticsRequested = req =>
4
4
  (req.query && req.query['sap-statistics'] === 'true') ||
5
5
  (req.headers && req.headers['sap-statistics'] === 'true' && (!req.query || !req.query['sap-statistics']))
6
6
 
7
- module.exports = app => {
8
- if (app._perf_measured) return
9
- else app._perf_measured = true
7
+ module.exports = function sap_statistics(req, res, next) {
8
+ if (!_statisticsRequested(req)) return next()
10
9
 
11
- app.use((req, res, next) => {
12
- if (!_statisticsRequested(req)) return next()
10
+ const t0 = performance.now()
11
+ const { writeHead } = res
12
+ res.writeHead = function (...args) {
13
+ const total = Number((performance.now() - t0) / 1000).toFixed(2)
14
+ if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
15
+ writeHead.call(this, ...args)
16
+ }
13
17
 
14
- const t0 = performance.now()
15
- const { writeHead } = res
16
- res.writeHead = function (...args) {
17
- const total = Number((performance.now() - t0) / 1000).toFixed(2)
18
- if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
19
- writeHead.call(this, ...args)
20
- }
21
-
22
- next()
23
- })
18
+ next()
24
19
  }
@@ -698,7 +698,6 @@ const _convertToOneEqNullInFilter = (query, target) => {
698
698
  }
699
699
  }
700
700
  }
701
-
702
701
  // eslint-disable-next-line complexity
703
702
  const _convertSelect = (query, model, _options) => {
704
703
  const _4db = _options.service instanceof cds.DatabaseService
@@ -1,25 +1,28 @@
1
1
  const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
2
2
 
3
- const traverseFroms = (cqn, cb) => {
3
+ const traverseFroms = (cqn, cb, aliasForSet) => {
4
4
  while (cqn.SELECT) cqn = cqn.SELECT.from
5
5
 
6
6
  // Do the most likely first -> {ref}
7
7
  if (cqn.ref) {
8
- return cb(cqn)
8
+ return cb(cqn, aliasForSet)
9
9
  }
10
10
 
11
11
  if (cqn.SET) {
12
- return cqn.SET.args.map(a => traverseFroms(a, cb))
12
+ // if a union has an alias, we should use it for the columns we get out of the union
13
+ return cqn.SET.args.map(a => traverseFroms(a, cb, cqn.as))
13
14
  }
14
15
 
15
16
  if (cqn.join) {
16
- return cqn.args.map(a => traverseFroms(a, cb))
17
+ return cqn.args.map(a => traverseFroms(a, cb, aliasForSet))
17
18
  }
18
19
  }
19
20
 
20
21
  const getEntityNameFromCQN = cqn => {
21
22
  const res = []
22
- traverseFroms(cqn, from => res.push({ entityName: from.ref[0].id || from.ref[0], alias: from.as }))
23
+ traverseFroms(cqn, (from, aliasForSet) =>
24
+ res.push({ entityName: from.ref[0].id || from.ref[0], alias: aliasForSet || from.as })
25
+ )
23
26
  return res.length === 1 ? res[0] : res.find(n => n.entityName !== 'DRAFT.DraftAdministrativeData') || {}
24
27
  }
25
28
 
@@ -134,7 +134,7 @@ const getCache = (anything, cache, newCacheFn) => {
134
134
  }
135
135
 
136
136
  module.exports = (usecase, tx, target, ...args) => {
137
- // get model first as it may be added to tx (cf. "_ensureModel")
137
+ // get model first as it may be added to tx (cf. "_ensureModel") // REVISIT: _ensureModel is gone
138
138
  const model = tx.model
139
139
  if (!model) return
140
140
 
@@ -24,28 +24,16 @@ class DatabaseService extends cds.Service {
24
24
  this[`_${each}`] = generic[each]
25
25
  }
26
26
 
27
- // REVISIT: ensures tenant-aware this.model if this is a transaction -> this should be fixed in mtx integration, not here
28
- this._ensureModel = function (req) {
29
- if (this.context) {
30
- // if the tx was initiated in messaging, then this.context._model is not unfolded
31
- // -> use this.context._model._4odata if present
32
- const { _model } = this.context
33
- if (_model) this.model = _model._4odata || _model
34
- else this.model = req._model
35
- }
36
- }
37
- this._ensureModel._initial = true
38
-
39
27
  // REVISIT: how to generic handler registration?
40
28
  }
41
29
 
42
30
  /** Database services don't support custom-defined operations */
43
- operations() {
31
+ get operations() {
44
32
  return []
45
33
  }
46
34
 
47
35
  /** Database services don't support custom-defined events */
48
- events() {
36
+ get events() {
49
37
  return []
50
38
  }
51
39
 
@@ -12,6 +12,8 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
12
12
 
13
13
  const { filterKeys } = require('../../fiori/utils/handler')
14
14
 
15
+ const getError = require('../../common/error')
16
+
15
17
  // Symbols are used to add extra information in response structure
16
18
  const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
17
19
  const TO_MANY = Symbol.for('sap.cds.toMany')
@@ -94,6 +96,16 @@ class JoinCQNFromExpanded {
94
96
  return this._isDraft
95
97
  }
96
98
 
99
+ // There can be a limit/offset for the target entity.
100
+ // The current expand implementation applys a `DISTINCT` on
101
+ // `filterExpand`, which changes the sorting in absence of `ORDER BY`.
102
+ // Therefore, add an implicit `ORDER BY` in those cases.
103
+ _addImplicitOrderBy(cqn, entity, alias) {
104
+ if (cqn.orderBy || !cqn.limit || !entity) return // not needed
105
+ const orderByColumns = cqn.groupBy || getAllKeys(entity).map(key => ({ ref: [alias, key] }))
106
+ cqn.orderBy = orderByColumns
107
+ }
108
+
97
109
  /**
98
110
  * Build first level of expanding regarding to many and all to one if not part of a nested to many expand.
99
111
  *
@@ -123,6 +135,7 @@ class JoinCQNFromExpanded {
123
135
  })
124
136
  // expand to one
125
137
  const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
138
+ this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
126
139
  const givenColumns = readToOneCQN.columns
127
140
  readToOneCQN.columns = []
128
141
  this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
@@ -130,7 +143,7 @@ class JoinCQNFromExpanded {
130
143
  const table = unionTable || this._getRef(SELECT).table
131
144
  const isDraftTree = this._isDraftTree(table)
132
145
  const entity = this._getEntityForTable(table)
133
-
146
+ this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
134
147
  if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
135
148
 
136
149
  readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
@@ -455,14 +468,10 @@ class JoinCQNFromExpanded {
455
468
 
456
469
  if (element.ref) {
457
470
  element.ref[1] = Object.assign({}, element.ref[1])
458
- element.ref[1].args = element.ref[1].args.map(arg => {
459
- return this._mapArg(arg, cqn, tableAlias)
460
- })
471
+ element.ref[1].args = element.ref[1].args.map(arg => this._mapArg(arg, cqn, tableAlias))
461
472
  } else {
462
473
  element.args = element.args.slice(0)
463
- element.args = element.args.map(arg => {
464
- return this._mapArg(arg, cqn, tableAlias)
465
- })
474
+ element.args = element.args.map(arg => this._mapArg(arg, cqn, tableAlias))
466
475
  }
467
476
  }
468
477
 
@@ -684,6 +693,17 @@ class JoinCQNFromExpanded {
684
693
  c => !expandedEntity.keys[c].isAssociation && !(c in DRAFT_COLUMNS_MAP)
685
694
  )
686
695
  const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
696
+
697
+ const assoc = entity.associations[column.ref[0]]
698
+ if (assoc.is2one && assoc.on) {
699
+ const onCond = expandedEntity._relations[assoc.name].join('target', 'source')
700
+ const xpr = onCond[0].xpr
701
+ const fks = (xpr && xpr.filter(e => e.ref && e.ref[0] === 'target').map(e => e.ref[1])) || []
702
+ for (const k of fks) {
703
+ if (!cols.includes(k)) cols.push(k)
704
+ }
705
+ }
706
+
687
707
  const unionFrom = getCQNUnionFrom(cols, expandedEntity.name, expandedEntity.name + '.drafts', ks, user)
688
708
  readToOneCQN.from.args[1] = {
689
709
  SELECT: {
@@ -1162,7 +1182,7 @@ class JoinCQNFromExpanded {
1162
1182
  cqn.orderBy = this._copyOrderBy(column.orderBy, tableAlias, expandedEntity)
1163
1183
  }
1164
1184
 
1165
- if (column.limit) cqn.limit = column.limit
1185
+ if (column.limit) throw getError(501, 'Pagination is not supported in expand')
1166
1186
 
1167
1187
  cqn = this._adaptWhereOrderBy(cqn, tableAlias)
1168
1188
 
@@ -1227,23 +1247,6 @@ class JoinCQNFromExpanded {
1227
1247
  return assoc.target + '_drafts'
1228
1248
  }
1229
1249
 
1230
- _getLimitInSelect(cqn, columns, limit, orderBy, expandedEntity) {
1231
- const select = {
1232
- SELECT: {
1233
- columns: this._copyColumns(columns, 'limitFilter'),
1234
- from: { ref: [cqn.from.args[0].ref[0]], as: 'limitFilter' },
1235
- where: this._convertOnToWhere(cqn.from.on, cqn.from.args[0].as, 'limitFilter'),
1236
- limit: limit
1237
- }
1238
- }
1239
-
1240
- if (orderBy) {
1241
- select.SELECT.orderBy = this._copyOrderBy(orderBy, 'limitFilter', expandedEntity)
1242
- }
1243
-
1244
- return select
1245
- }
1246
-
1247
1250
  _isPathExpressionToOne(ref, entity) {
1248
1251
  const ref0 = ref[0]
1249
1252
  const el = entity.elements[ref0]
@@ -178,6 +178,10 @@ const _pickDraft = element => {
178
178
 
179
179
  if (element.virtual) categories.push('virtual')
180
180
 
181
+ if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
182
+ categories.push({ category: 'default', args: element })
183
+ }
184
+
181
185
  // REVISIT: element._foreignKeys.length seems to be a very broad check
182
186
  if (element.isAssociation && element._foreignKeys.length) {
183
187
  categories.push({ category: 'propagateForeignKeys' })
@@ -98,9 +98,7 @@ class SelectBuilder extends BaseBuilder {
98
98
  this._orderBy(noQuoting)
99
99
  }
100
100
 
101
- if (this._obj.SELECT.limit || this._obj.SELECT.one) {
102
- this._limit()
103
- }
101
+ this._limit()
104
102
 
105
103
  if (this._obj.SELECT.forUpdate) {
106
104
  this._forUpdate()
@@ -322,11 +320,8 @@ class SelectBuilder extends BaseBuilder {
322
320
 
323
321
  _where() {
324
322
  const entityName = this._obj.SELECT.from.ref && this._obj.SELECT.from.ref[0]
325
- const where = new this.ExpressionBuilder(
326
- this._obj.SELECT.where,
327
- entityName ? { ...this._options, entityName } : this._options,
328
- this._csn
329
- ).build()
323
+ const options = entityName ? { ...this._options, entityName } : this._options
324
+ const where = new this.ExpressionBuilder(this._obj.SELECT.where, options, this._csn).build()
330
325
  this._outputObj.sql.push('WHERE', where.sql)
331
326
  this._outputObj.values.push(...where.values)
332
327
  }
@@ -415,6 +410,34 @@ class SelectBuilder extends BaseBuilder {
415
410
  this._outputObj.sql.push(sqls.join(', '))
416
411
  }
417
412
 
413
+ _addRows() {
414
+ if (this._obj.SELECT.limit) {
415
+ if (this._obj.SELECT.limit.rows !== undefined) {
416
+ // limit (no placeholder for statement caching)
417
+ this._outputObj.sql.push('LIMIT', this._obj.SELECT.limit.rows.val)
418
+ } else {
419
+ // rows parameter is mandatory for SQL
420
+ throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
421
+ }
422
+ }
423
+ }
424
+
425
+ _addOne() {
426
+ this._outputObj.sql.push('LIMIT', 1)
427
+ }
428
+
429
+ _addOffset() {
430
+ // offset
431
+ if (this._obj.SELECT.limit && this._obj.SELECT.limit.offset !== undefined) {
432
+ if (typeof this._obj.SELECT.limit.offset.val === 'number' && !this._parameterizedNumbers) {
433
+ this._outputObj.sql.push('OFFSET', this._obj.SELECT.limit.offset.val)
434
+ } else {
435
+ this._outputObj.sql.push('OFFSET', '?')
436
+ this._outputObj.values.push(this._obj.SELECT.limit.offset.val)
437
+ }
438
+ }
439
+ }
440
+
418
441
  /**
419
442
  * sql limit clause will be generated without placeholders.
420
443
  * reason is optimizing paging queries. number of rows does not change.
@@ -423,17 +446,13 @@ class SelectBuilder extends BaseBuilder {
423
446
  * offset will still use placeholders, as it'll change during the paging queries.
424
447
  */
425
448
  _limit() {
426
- // limit (no placeholder for statement caching)
427
- this._outputObj.sql.push('LIMIT', this._obj.SELECT.one ? 1 : this._obj.SELECT.limit.rows.val)
428
- // offset
429
- if (this._obj.SELECT.limit && this._obj.SELECT.limit.offset) {
430
- if (typeof this._obj.SELECT.limit.offset.val === 'number' && !this._parameterizedNumbers) {
431
- this._outputObj.sql.push('OFFSET', this._obj.SELECT.limit.offset.val)
432
- } else {
433
- this._outputObj.sql.push('OFFSET', '?')
434
- this._outputObj.values.push(this._obj.SELECT.limit.offset.val)
435
- }
449
+ if (this._obj.SELECT.one) {
450
+ this._addOne()
451
+ } else {
452
+ this._addRows()
436
453
  }
454
+
455
+ this._addOffset()
437
456
  }
438
457
 
439
458
  _parameters() {
@@ -1,69 +1,69 @@
1
1
  const cds = require('../cds')
2
2
 
3
3
  const handleDefaults = require('./defaults')
4
+ const Extensions = 'cds.xt.Extensions'
4
5
 
5
- const _calculateExtensions = async function (ID, tag, tenant) {
6
+ // REVISIT: Reuse ratio = 0
7
+ const _calculateExtensions = async function (ID, tag) {
6
8
  let active, inactive
7
- await cds.tx({ tenant }, async tx => {
8
- if (tag || ID) {
9
- const inactiveCqn = SELECT.from('cds.xt.Extensions').where({ activated: 'propertyBag' })
10
- if (ID) {
11
- inactiveCqn.where('ID !=', ID)
12
- } else {
13
- inactiveCqn.where('tag !=', tag)
14
- }
15
- inactive = await tx.run(inactiveCqn)
16
- const activeCqn = SELECT.from('cds.xt.Extensions').where({ activated: 'database' })
17
- if (ID) {
18
- activeCqn.or({ ID })
19
- } else {
20
- if (tag) activeCqn.or({ tag })
21
- }
22
- active = await tx.run(activeCqn)
23
- if (inactive.length) {
24
- const deleteCqn = DELETE.from('cds.xt.Extensions').where(inactiveCqn.SELECT.where)
25
- await tx.run(deleteCqn)
26
- }
9
+ if (tag || ID) {
10
+ const inactiveCqn = SELECT.from(Extensions).where({ activated: 'propertyBag' })
11
+ if (ID) {
12
+ inactiveCqn.where('ID !=', ID)
27
13
  } else {
28
- // activate all
29
- inactive = []
30
- active = await tx.run(SELECT.from('cds.xt.Extensions'))
14
+ inactiveCqn.where('(tag !=', tag, 'or tag =', null, ')')
31
15
  }
32
- })
16
+ inactive = await cds.db.run(inactiveCqn)
17
+ const activeCqn = SELECT.from(Extensions).where({ activated: 'database' })
18
+ if (ID) {
19
+ activeCqn.or({ ID })
20
+ } else if (tag) {
21
+ activeCqn.or({ tag })
22
+ }
23
+ active = await cds.db.run(activeCqn)
24
+ if (inactive.length) {
25
+ const deleteCqn = DELETE.from(Extensions).where(inactiveCqn.SELECT.where)
26
+ await cds.db.run(deleteCqn)
27
+ }
28
+ } else {
29
+ // activate all
30
+ inactive = []
31
+ active = await cds.db.run(SELECT.from(Extensions))
32
+ }
33
33
 
34
34
  return { active, inactive }
35
35
  }
36
36
 
37
- const _restoreExtensions = async function (tenant, active, inactive) {
38
- await cds.tx({ tenant }, async tx => {
39
- // delete all extensions
40
- await tx.run(DELETE.from('cds.xt.Extensions'))
41
- // active
42
- active.forEach(row => {
43
- row.csn = row.csn.replace(/,"@cds.extension":true/g, '')
44
- row.activated = 'database'
45
- row.timestamp = '$now'
46
- })
47
- await tx.run(INSERT.into('cds.xt.Extensions').entries(active))
48
- // inactive
49
- if (inactive.length) {
50
- for (const na of inactive) {
51
- for (const extension of JSON.parse(na.csn).extensions) {
52
- await handleDefaults(extension, tx)
53
- }
37
+ // REVISIT: Reuse ratio = 0
38
+ const _restoreExtensions = async function (active, inactive, appCsn) {
39
+ // delete all extensions
40
+ await cds.db.run(DELETE.from(Extensions))
41
+ // active
42
+ active.forEach(row => {
43
+ row.csn = row.csn.replace(/,"@cds.extension":true/g, '')
44
+ row.activated = 'database'
45
+ row.timestamp = '$now'
46
+ })
47
+ await cds.db.run(INSERT.into(Extensions).entries(active))
48
+ // inactive
49
+ if (inactive.length) {
50
+ for (const na of inactive) {
51
+ for (const extension of JSON.parse(na.csn).extensions) {
52
+ await handleDefaults(extension, appCsn, cds.db)
54
53
  }
55
- await tx.run(INSERT.into('cds.xt.Extensions').entries(inactive))
56
54
  }
57
- })
55
+ await cds.db.run(INSERT.into(Extensions).entries(inactive))
56
+ }
58
57
  }
59
58
 
60
- const activate = async function (ID, tag, tenant) {
61
- const { active, inactive } = await _calculateExtensions(ID, tag, tenant)
59
+ // REVISIT: Review with Vitaly: (1) Delete Inactives > (2) DS.extend(t) > (3) Delete All > (4) Restore All ???
60
+ const activate = async function (ID, tag, tenant, appCsn) {
61
+ const { active, inactive } = await _calculateExtensions(ID, tag)
62
62
 
63
63
  const { 'cds.xt.DeploymentService': ds } = cds.services
64
64
  await ds.extend(tenant)
65
65
 
66
- await _restoreExtensions(tenant, active, inactive)
66
+ await _restoreExtensions(active, inactive, appCsn)
67
67
  }
68
68
 
69
69
  module.exports = activate